Skip to content

Make a delivered message something the recipient can act on - #10

Merged
thisisjun786 merged 27 commits into
devfrom
codex/jun-131-pr-report-contract
Sep 16, 2026
Merged

thisisjun786 merged 27 commits into
devfrom
codex/jun-131-pr-report-contract

Conversation

@thisisjun786

@thisisjun786 thisisjun786 commented Sep 16, 2026

Copy link
Copy Markdown
Owner

The problem

A delivered message told its recipient an event id, a relationship id, a generation and a
revision hash, then listed deliverable paths and their sha256. All of that is true and none
of it says which pull request to open, what was verified, what is still unresolved, or what
to do next. A parent receiving one had to go and reconstruct the work before it could act on
the message about it.

Separately, CXC and this relay both have a vocabulary for how an execution ended, and
nothing stated how the two line up. The overlap that matters is the word verified: CXC
glosses DONE as verified success, meaning the child proved its own recorded criteria,
while the relay verified is the parent disposition written through record_verdict
against registered criteria on an acknowledged event. Same English word, two authorities.

Before and after

Before, a completion message opened like this:

[codex-session-relay] verification request
requestId: del-30ad84af94ef-a1
eventId: 30ad84af94ef65bc0db9b43a83936a4a
relationshipId: rel-46d5b5ac690ef861
executionGeneration: 1
...
deliverables: 1
  /tmp/work/out.txt  sha256=91526ba4...

After, when a work report is recorded, it opens like this and the identifiers move to the
bottom where the parent uses them to answer:

[codex-session-relay] verification request
result: delivery messages now lead with the pull request
cxc: DONE - every recorded criterion has fresh proof on this head
  meaning: the child proved every recorded criterion against its own work
  this is the child reporting on its own work. It is not a verification: a DONE report is
  the child proving its own criteria, not the parent verdict

pull request: thisisjun786/codex-relay-workflow#12  (ready)
  base: dev c56576d5be412b5bc352dd93b9eb37ab279a12f6
  head: a1b2c3d4e5f60718293a4b5c6d7e8f9012345678
  criteria: d1e2f3

verification:
  python3 -m pytest -> exit 0  214 passed

unresolved:
  - the CLI verb lands after PR #8 merges
next: review the diff and record a verdict

What changed

cxc.py is new and owns the seam with CXC and nothing else: pinned contract provenance
(version, file paths, rule ids, sha256 for nine rules), the report-status mapping, the
VERDICT: line, and the wait classification. It copies no CXC prose and rebuilds no part of
its state machine; cxc.affected_by() names which rules a changed install touches so a
version bump re-verifies those rules rather than all of them.

A CXC report status never chooses a relay outcome. The outcome still comes from the child
receipt, which is the only thing the frozen contract lets assert one, and the status is
recorded beside it and checked for compatibility. BLOCKED, UNSAFE and NEEDS_HUMAN all
land on blocked_needs_input because the frozen enum has one slot for a human having to
decide something; the original word and its reason are stored and rendered, so the collapse
loses nothing a reader needs. An unknown status is refused by name with the accepted set and
the contract version it was read against.

report.py is new and owns the relay-owned work report plus both directions of rendering.
Revision requests now carry the DISPATCH-TASK-01 fields and lead with the violated criterion
and the anchor that reproduces it. Shortening a message is something it admits to: headings
survive with a count of what is missing, one omitted: line names what was dropped and the
command that shows the rest, and a budget too small to hold the required parts refuses
rather than shipping a message that silently lost them.

Backward compatibility is the same mechanism as correctness here: an event with no work
report renders exactly what it rendered before, because a pre-contract receipt has no pull
request to centre a report on and inventing one would be the guess this package refuses
everywhere else. report.version_of tells the two apart explicitly.

Overlap with #8

#8 is open against the same package and merges first. Two files are shared and the hunks do
not overlap:

Nothing else of #8 is touched. New tests live in tests/test_report_contract.py so
tests/test_delivery.py stays #8 territory.

Validation

validate.py, scripts/ci/tests (33), contracts.py (102/102 fixtures, 46/46 return
sites), secrets.sh (204 commits, no leaks), git diff --check, and
CRW_PACKAGES_TMPDIR=/var/tmp python3 scripts/ci/packages.py - bridge 119 tests, relay 548
tests, both CLIs responded, both wheels built. All exit 0 on this head.

tests/test_report_contract.py adds 27 scenarios written from the recipient side: an unknown
CXC status diagnosed rather than defaulted, a status contradicting its receipt outcome,
DONE refused as verification, stale head and stale generation refused, the same PR number
under two parents staying distinct, more than ten findings never dropped in silence, an
impossible budget refusing, a preview still not being evidence of what was sent, a legacy
receipt rendering unchanged, and a bare timeout becoming neither failure nor re-run
permission.

This is evidence about this source. It says nothing about an installed relay, a running
daemon, an MCP registration or delivery on any host.

Review rounds

Five rounds from the Codex and Devin reviewers, 25 threads, all answered and resolved. The
substantive ones, in the order they mattered:

  • record() trusted its caller for the relationship, generation, revision and outcome, so a
    mistyped call could file a report describing a different execution. Identity is now read
    from the events row and those arguments no longer exist (I-100).
  • render_revision read only the work report review, so a report with no review replaced the
    parent recorded findings with nothing. The revision receipt is now authoritative for which
    criteria a correction names; the review only adds notes and anchors (I-101).
  • Four shapes and lengths could pass record() and fail only at render time. Rendering runs
    inside the delivery claim, so each one rolled the claim back and the delivery never went
    out. All are refused where the caller can fix them (I-102, I-108, I-110, I-111, I-112).
  • Moving the verdict to the final line and appending the omission notice after everything
    cancelled each other out. A section can now declare itself last (I-104).
  • manifestRef was first dropped entirely, then restored inside the first block the composer
    elides. It is now its own essential section (I-106, I-109).
  • The omission notice pointed at show --event, which did not read work_reports, so the
    advertised recovery path could not return what the message dropped (I-107).

Known gap

There is no CLI verb for recording a work report. The adapter entry point is
report.record(store, clock, event_id=..., ...); adding CLI surface means editing
build_parser(), which PR #8 edits at the same place, and this task was instructed to
report before adding it. It lands on top of origin/dev once #8 merges, and JUN-107 owns
wiring Run to these adapters. Until a caller records reports, existing deployments keep
receiving the pre-contract message, which is the designed behaviour rather than a
regression.

cmd_show is the one exception taken in cli.py, reported first, because a shipped message
was pointing at a command that could not answer it. It sits about forty lines clear of the
nearest hunk in #8 and uses a function-local import so the module import block #8 edits is
untouched.

A delivery told its recipient an event id, a relationship id, a generation and
a revision hash, then listed file paths and their digests. All of it true, none
of it saying which pull request to open, what was checked, what is still open,
or what to do next, so a parent had to go and reconstruct the work before it
could act on the message about it.

The message now leads with the result, the repository and pull request, the base
and head commit, the verification evidence, the unresolved items and the next
action. The identifiers keep their place at the bottom: they are how the parent
answers, not how it decides.

None of that fits in a completion receipt, which is frozen with
additionalProperties false, so a work report is a relay-owned record like the
criteria set, bound to the event, generation and revision it describes. A report
about one head cannot answer for a later one, and a pull request number is never
stored or rendered without its repository, so the same number on two projects
stays two pull requests.

The second half is the seam with CXC. Both systems have a word for how an
execution ended and the overlap is dangerous: CXC glosses DONE as verified
success, meaning the child proved its own criteria, while the relay verified is
the parent disposition written against registered criteria on an acknowledged
event. A report status is therefore recorded beside the receipt outcome and
checked against it, never used to derive it, and DONE, an open pull request, a
review PASS and a green check are each listed as evidence for a verdict rather
than one. An unknown status is refused by name with the contract version it was
read against. The mapping points at the installed rules and copies none of them.

Shortening a message is now something it admits to: headings survive with a
count of what is missing, one omitted line names what was dropped and the
command that shows the rest, and a budget too small for the required parts
refuses rather than shipping a message that silently lost them.

An event with no work report renders exactly what it rendered before. A receipt
from before this contract has no pull request to centre a report on, and
inventing one would be the guess this package refuses everywhere else.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-16T09:51:16.672280Z 7354c00 New commits
🔒 Security Review Completed 2026-09-16T07:19:01.734123Z 89aa11e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@thisisjun786

Copy link
Copy Markdown
Owner Author

코디네이터 · 패킷 보정 2건 (착수 직후 전달 실패분)

부모 코디네이터(01a0a6a7-74de-7d63-b03f-ba8753d9c646)입니다. 브리지가 활성 작업에 메시지를 받지 않아 여기로 남깁니다. 독립 감사가 패킷 발송 직후 잡아준 보정이라 원본 패킷에는 없었습니다.

  1. 테스트 파일도 JUN-103과 겹칩니다. PR #8이 packages/codex-session-relay/tests/test_delivery.pytests/test_cli.py를 이미 고치고 있습니다. 둘 다 당신의 tests/** 표면 안입니다. 손대기 전에 확인하고, JUN-103이 먼저 머지된 뒤 합칠 때 충돌이 나면 양쪽 의도를 모두 보존하세요. 한쪽을 통째로 버리지 마세요.

  2. cli.py 경계. 현재 CLI에는 PR 식별자·head SHA·기준 digest 같은 메타데이터를 넘길 수단이 없습니다. 어댑터 진입점을 새로 제공해서 해결하고, 그래도 cli.py를 고쳐야 한다고 판단되면 고치기 전에 먼저 보고하세요. PR #8이 같은 파일을 쓰고 있습니다.

나머지는 패킷 그대로입니다. 머지 순서는 JUN-103(PR #8)이 먼저이고, 그 뒤 git fetch origin && git merge origin/dev로 합치면 됩니다. 리뷰 지적은 각각 수정·근거 있는 반박·범위 밖 분리 중 하나로 처리하고 반드시 스레드에 답글을 달고 해소하세요. 코드만 고치고 스레드를 두면 머지되지 않습니다.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Review of #10 found the hole: record() took the relationship, generation,
revision and outcome as arguments and never compared them with the events row
for that event id. A mistyped or stale caller could file a report describing a
different execution, and delivery would then hand the recipient another pull
request and another instruction. Identity is now READ from the stored event and
there is no argument left through which a caller could say otherwise. A report
against an event nobody accepted is refused.

Six smaller things found in the same review:

A revision request carries no child receipt, so pairing its status with an
asserted outcome forced the caller to invent one. That direction now checks the
status is known and stops there.

Evidence and unresolved entries are validated where they are recorded. Rendering
runs inside the delivery claim, so a shape that only fails at render time rolled
the claim back and the delivery never went out.

The non-verification sentence is chosen by status. Every report was told it was
the child proving its own criteria, which is false for BLOCKED and the opposite
of what it means.

The review verdict moves to the final line, where REVIEW-OUTPUT-01 puts it and
where a scanner reading the tail will find it.

The message budget counts UTF-8 bytes. A transport limit is a byte limit, and
this workflow writes Korean, which costs about three bytes a character, so
counting characters let exactly the messages it actually sends overrun a budget
that looked comfortable.

provenance() no longer claims every report stores its source digests; it stores
the version string, and the digests are pinned in source.
devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Second review round on #10, three real findings.

A revision event already carries the parent findings in its own receipt,
written by record_verdict in the transaction that opened the new generation.
render_revision was reading only the work report review, so a report with no
review replaced those findings with nothing, and one naming a different set
replaced them with something else. Either way the child was corrected against
instructions the parent never gave. The receipt now leads, the review adds notes
and source anchors by id, and anything the review raises on its own is kept and
labelled as not part of the recorded verdict.

An oversized summary or next action passed record() and then failed every
render. Rendering happens inside the delivery claim, so each claim rolled back
and the delivery never went out: the same failure mode as the malformed entries
in the previous round, which is the argument for bounding these fields where the
caller can still fix them rather than where nobody can.

Replacing a report in place after a delivery had already been attempted let a
retry carry different instructions under the same event and the same stated
submission, with no way for the recipient to tell which one it was answering.
The frozen attempt bytes always kept the history; what was missing was the
report admitting it had changed. Correcting a report before anything is sent is
still free, and afterwards it has to be a new submission, which the message
already shows.
devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Third review round on #10.

The omission notice was appended after every section, including the deliberately
last verdict section, so any correction that had to drop something ended on
omitted: instead of its judgment. A consumer following the final-line contract
would stop finding the verdict in exactly the messages that had to shorten. A
section can now declare itself last and the notice goes before it.

A revision event exists because the parent ruled needs_changes, and the review
half of a report is still caller-supplied even now that identity is not. A PASS
verdict on one would approve the work and demand changes in the same message, so
it is refused.

manifestRef is back in the report-backed message. The pre-contract message
always carried it, it is the stable location a receipt points at when live
artifact paths may move, and adding a work report was silently removing it even
with no elision in play.
devin-ai-integration[bot]

This comment was marked as resolved.

An elided message told its recipient to run show --event for the rest, and
cmd_show never read work_reports, so exactly the fields the message had dropped
were the fields that command could not return. The notice was a false promise
rather than an incomplete one, which is worse than eliding nothing.

show now carries the whole report. This touches cli.py, which the parallel
JUN-103 branch is also editing: it was reported before being changed, the added
lines sit inside cmd_show about forty lines from that branch nearest hunk, and
the import is function-local so the module import block it does edit stays
untouched.

Also from the same round: a restore section could name a skill owner nobody has,
and cxc.skill_pointer refuses an unknown name on purpose, so the typo surfaced
as a failed render inside the delivery claim instead of as something a caller
could fix. Validated at record time now, like the other shapes that can only
fail where nobody can act on them.
devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

manifestRef lived inside the deliverables block, which is the first thing the
composer drops, so the reports most likely to be shortened were exactly the ones
that lost the stable location their artifacts can still be verified against. It
is a verification pointer rather than a file listing, so it is now its own
essential section and the listing can go without it.

_check_restore tested entries for dictionary membership before checking their
shape, so an unhashable entry raised TypeError and the caller got a host failure
instead of a named refusal. A validator that crashes on malformed input is not
validating it. Shape first now, for the outer object, the skills list and each
entry, and every invalid form is MALFORMED_RECEIPT.
devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

pr_state was copied without a limit onto the second, non-elidable line of the
pull request section, so a 10 KB value was accepted and then made every preview
and every delivery claim raise. The same was true of pr_url, base_ref, base_sha,
head_sha and criteria_digest. All six are bounded at record time now, and a
non-string value in any of them is a named refusal rather than whatever str()
would have produced.

The malformed-shape guarantee did not hold at the top level either. A mapping
passed as evidence or unresolved iterated as its own keys and was silently
accepted as a list of strings; a scalar raised TypeError out of the validator.
Both are MALFORMED_RECEIPT now.
devin-ai-integration[bot]

This comment was marked as resolved.

A report was keyed on the event alone, so recording submission 2 overwrote the
only full copy of submission 1. That breaks the promise the earlier message
made: its omission notice sent the recipient to show --event for the evidence
and unresolved items it had to elide, and after the overwrite those were gone,
while the frozen attempt bytes hold only the elided text. work_reports is now
keyed on the event and the submission, read() returns the current one, read_all()
returns all of them oldest first, and show carries both.

Two more from the same round:

A review passed as a bare string reached .get() and raised AttributeError, so
the named-refusal guarantee did not hold for that container either. The review
object, its findings list and each finding are shape-checked now.

pr_url was bounded at the label limit, which is the ceiling for lines the
composer cannot shorten. The url is not one of those: it sits on a line that can
be dropped, so a real forge url over 300 bytes was being rejected for a report
that would have rendered fine. It gets a storage ceiling instead.

A tuple stays accepted for evidence and unresolved. The hazard the shape check
exists for is a mapping iterating as its keys and a string iterating as
characters; a tuple has the same semantics as the list it normalises to. The
invariant wording now says ordered sequence, which is what the code does.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

_check_resubmission ran before BEGIN IMMEDIATE, so two recorders could both
approve the same next submission number and a delivery could open an attempt
between the check and the upsert. The rest of this package reads inside the
caller transaction for exactly this reason; this now does too.

The write also stopped naming a conflict target. A target has to match a
constraint in the schema the database was actually created with, and CREATE TABLE
IF NOT EXISTS never reshapes an existing table, so naming one made every report
write depend on which revision of this table a store started life on. Delete then
insert inside the same transaction has no such dependency. The limit that remains
is recorded: no released version has this table, so a store built from an
intermediate revision of this change is recreated rather than upgraded.

Two ordering bugs in the validation:

The PASS conflict check read review.get() before any shape check, so a revision
report carrying a bare string raised AttributeError instead of a named refusal.
Shape is checked first now.

A completion report accepted a review that render_completion never reads, so a
supplied PASS or FAIL was stored and silently never delivered, with no omission
notice either. Only a correction carries a review.
show now returns every preserved submission, so a recipient holding an older
message needs to know which one it is reading. The completion message already
said so; the revision message did not. Both do now, and since the bytes are
frozen per attempt, the message itself is the attempt-to-submission record.

A report with no pull request dropped its baseRef, baseSha, headSha and
criteriaDigest on an early return, with no omission notice. Blocked and
budget-exhausted work is exactly the case that has no PR yet and still has a
branch point and a commit worth naming, so the parent was losing the context it
needed to act. The commit lines are shared between both branches now.
devin-ai-integration[bot]

This comment was marked as resolved.

@thisisjun786

Copy link
Copy Markdown
Owner Author

코디네이터 · 마감 지침

부모 코디네이터(01a0a6a7-74de-7d63-b03f-ba8753d9c646)입니다. 브리지가 활성 작업에 메시지를 받지 않아 여기로 남깁니다.

Jun이 진행 중인 PR까지만 마무리하고 멈추기로 결정했습니다. 이 PR(JUN-131)이 이번 런의 마지막 인도분입니다. 새 범위를 열지 말고 지금 것을 닫는 데 집중하세요.

지금 해야 할 것, 이 순서로:

  1. 열린 리뷰 스레드를 전부 닫으세요. 각 건을 수정 / 근거 있는 반박 / 범위 밖 분리 중 하나로 처리하고 반드시 스레드에 답글을 달고 해소하세요. 코드만 고치고 스레드를 두면 머지되지 않습니다. 분리로 판단한 건은 목록만 보고하면 제가 Linear 이슈로 만듭니다.
  2. push마다 새 전체 재리뷰를 요청하지 마세요. 현재 열린 집합만 닫으면 됩니다.
  3. 새 기능을 추가하지 마세요. 범위를 넓히는 수정은 하지 말고, 유효하지만 큰 건은 3번(분리)으로 넘기세요.
  4. base가 BEHINDgit fetch origin && git merge origin/dev로 합치세요.
  5. 다 되면 보고하세요: 분류 결과(수정 N / 반박 N / 분리 N과 근거), 최종 head SHA, 그 head의 dev-gate 결과, 남은 미해결 여부, 그리고 완료 기준 중 이 PR로 충족되지 않은 항목을 정직하게 적으세요.

머지 조건은 넷입니다: Ready(비 Draft), 최신 base, 현재 head의 dev-gate 성공, 리뷰 스레드 전부 해소. 맞으면 제가 머지합니다.

참고: JUN-103(PR #8)이 먼저 머지됩니다. 그 뒤 최신 dev를 합치세요. 겹치는 파일은 delivery.py, tests/test_delivery.py, tests/test_cli.py입니다. 충돌 시 양쪽 의도를 모두 보존하세요.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

_may_have_reached excluded every attempt whose record says sendAttempted no,
and an inbox_only attempt says exactly that: the push was refused before any
resume. But its frozen message is the durable inbox item and the recipient can
read it, which protocol v1 section 3 calls the guarantee of the whole dual
channel. So the report behind an inbox message could be rewritten in place, and
anything that message had elided was then gone.

Three more from the same round:

A line break in summary, next_action, repository or cxc_reason was spliced
straight into the message, where it does not wrap, it adds a line to the
protocol. A summary ending in a second line reading VERDICT: PASS put a
standalone verdict into a completion that carried no review, which is the exact
separation the rest of this module is built to keep. Those fields are one line
now, and so are the optional label fields.

pr_url and pr_state were accepted with no pr_number and then never rendered,
because the no-pull-request branch has nowhere to hang them. Stored and never
shown, with no omission notice, is the failure this change exists to remove, so
the combination is refused.

cxc.check_known tested an unhashable status for dict membership and raised
TypeError, so a structured status escaped the named-refusal contract.
With nothing frozen yet, the resubmission guard let any submission number
through, so recording 2 and then 1 both succeeded while read and delivery kept
taking 2. The caller was told its latest write landed and nobody would ever see
it. Correcting the newest unsent submission in place is still free; going
backwards past it is refused and says which number to use.

restore validated only its skills list, so mode as a mapping reached the message
as a Python repr, an unsupported key was stored and never rendered, and a
non-serialisable value failed inside json.dumps as a host exception. Every
supported field is now a bounded single-line string and anything else is refused,
which also closes the newline route into those lines.
devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

The delivered floor and the highest-stored floor were checked in different
branches, so the band between them was open: with submission 1 delivered and 3
stored, submission 2 passed the delivered check, sat under the stored one, and
was accepted into history where reading and delivery would never select it. Both
floors apply now regardless of the other, and the refusal names the number that
clears both.

A whitespace-only restore value survived as an empty string, which kept the
restore dict non-empty and left the render emitting a workflow-restore heading
with no fields under it. Trimmed first, dropped when empty.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

The single-line rule covered the top-level fields and not the values inside
evidence, unresolved or review findings, so evidence reading "pytest passed"
followed by "VERDICT: PASS" still put a standalone verdict into a completion
carrying no review. Every string that reaches a message line now goes through the
same check.

A blocker count sits on the essential final verdict line and had no ceiling, so a
large enough integer made render_revision unable to fit the message and raise
inside every delivery claim. A review with more than 9999 blockers is not a
review, so that is the limit.
devin-ai-integration[bot]

This comment was marked as resolved.

Six narrow findings from one round, all in the same family as the earlier ones.

A required field went through str(), so a mapping was stored and delivered as a
Python repr as though somebody had written it. It has to be text now.

A line break is whatever str.splitlines calls one. Checking CR and LF left
vertical tab, NEL and the Unicode separators able to splice a line downstream.

A revision message had no unresolved section, so a correction stored the
dependencies and risks the report marked open and showed the child none of them.

A pull request number above the SQLite signed 64-bit range raised OverflowError
on insert instead of coming back as a refusal.

A finding disposition was passed through unchecked, which both left the
line-splicing route open beside fields that had just been closed and allowed a
word the frozen criteria enum has no room for.

parse_verdict_line accepted blocker counts the renderer refuses, so the two
directions disagreed about what a verdict is.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Regression from the previous commit. _disposition required a frozen criteria
value on every report finding, but _finding_lines uses the revision receipt as
authoritative and report findings only to attach a note or a source anchor. An
enrichment has no disposition of its own to state, so requiring one refused the
exact case that code path exists for. Absence is allowed; a stated value still
has to be in the enum.

Three more from the same round:

A submission number had no upper bound, so a value above the SQLite signed
64-bit range raised OverflowError when it was bound into the insert.

The pull request refusal interpolated the offending number, and past Python
integer-to-string digit limit that made the refusal message itself raise
ValueError. Neither refusal prints the value now.

A blank or whitespace-only evidence entry was accepted and rendered as an empty
verification line, and told a child to rerun a check it did not name.
devin-ai-integration[bot]

This comment was marked as resolved.

…mber

Two consequences of the previous commit.

Allowing an enrichment finding to omit its disposition meant a review-only
finding, one the receipt does not own, rendered as "c-9: None", telling the
reader a criterion had a judgment called None. The line now omits the
disposition when there is none, so an enrichment reads as an id and its note.

The positive-integer branch of the pull request check still interpolated the
value, and a huge negative number reaches that branch, so formatting it raised
ValueError out of the refusal. Both branches name the type and the range instead
of printing the value.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

The contract map says a report carries its CXC status and reason, and the
revision renderer built its header without either, so a correction dropped both
and produced no omission notice for them. It carries them now, like a completion
does.

A lone surrogate is one line by every line rule and still cannot be encoded as
UTF-8, so a required field raised UnicodeEncodeError out of _size while a nested
value was stored and then failed inside every delivery claim. Every line value
passes through _single_line, so the encodability check lives there.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

A legacy relationship can reach needs_changes with no registered criteria, so
the review findings are all there is and _finding_lines fell back to them
unlabelled, under the same violated-criteria heading a recorded verdict uses.
The child would read them as a verdict it could look up. The fallback now says
they come from the review and that the assignment has no recorded criteria set.

An evidence check name went through str(), so a mapping was stored and presented
as verification of a Python repr, and the revision direction told the recipient
to rerun that repr as though it were a command.
… checks

Shortening pops one line at a time and re-joined and re-encoded every remaining
line on each pass, which is quadratic in the length of the list being shortened.
The joined size is the sum of the encoded lines plus one separator between each
pair, so it is counted directly now.

The unresolved path still had the two holes the evidence path had already had
closed: a blank entry was accepted and rendered as an empty bullet, and a
mapping supplied as id or note went through str() and was stored as a Python
repr. Both are refused. The evidence check name had the same coercion left in
its mapping form and now has to be a string.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Four findings from the review round on the delivered head.

The renderer keys enrichment on the criterion id, so two findings for the same
criterion meant the second replaced the first and its note or anchor vanished
with no omission notice. One criterion carries one finding now, refused at
record time the way a registered criteria set already refuses a duplicate id.

over_budget still summed every remaining line on each pop, so shortening a long
list was still quadratic, and it runs inside the claim transaction where that
holds the single SQLite writer for the duration. The byte total and line count
are carried and adjusted by each mutation instead.

SCOPE mixed variable data with fixed protocol prose, and its floor let the
preserve boundary be shortened away. The omission marker points at show, which
returns the receipt and the work report but not template text, so those lines
were recoverable nowhere. They are their own section with a floor covering them.

A finding note or anchor still went through str(), so a mapping rendered as a
Python repr and a falsey non-text value such as 0 silently became absent.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

Four more coercion routes, all on producer input.

An evidence detail and a review finding id both went through str(), so a mapping
became a repr and a falsey value such as 0 or False silently became absence. A
fake identifier is worse than a rejected one: it cannot enrich the authoritative
finding it was meant for, so it arrives as a separate review item.

An exit code had no range, and past the integer-to-string digit limit json.dumps
raised ValueError while serialising the row. No process exits with a number that
size, so the bound says that rather than naming a serialisation limit.

A manifestRef carrying a lone surrogate made _size raise while measuring, so
every delivery claim rolled back once a report was attached. The receipt is
contract-validated and has no encodability rule for that field, so refusing the
delivery would punish the recipient for the producer. It renders as present and
not renderable, with the record as the place to read it.
@thisisjun786
thisisjun786 merged commit 83b020a into dev Sep 16, 2026
9 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7354c00645

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 303 to +306
record = self.intake.get(event_id) or {}
message = self._render_for(row, record, request_id)
report = read_work_report(self.store, event_id)
message = self._render_for(
row, record, request_id, report

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record reports before delivery becomes claimable

In the documented CLI flow, cmd_emit enqueues the event without accepting or recording a work report, and there is no report-recording CLI command. Consequently a daemon can claim the event here while read_work_report() still returns None, permanently freezing and sending the legacy message; a later API call to report.record() cannot update an already dispatched attempt. Record the report atomically with receipt acceptance/enqueueing, or otherwise keep the delivery ineligible until the report has been attached.

Useful? React with 👍 / 👎.

Comment on lines +102 to +105
if outcome == REVISION_OUTCOME:
# The parent-to-child direction carries no child receipt, so there is no asserted
# outcome to pair the status with. Demanding one would make the caller invent it.
cxc.check_known(cxc_status)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject success statuses on revision requests

For a revision_request, this accepts every known status, including DONE and NOOP. render_revision() then emits cxc: DONE together with the hard-coded meaning that the child proved every recorded criterion, in the same instruction that says the parent ruled needs_changes and demands corrections. Reject success statuses for this direction or render a direction-specific parent-review meaning so the correction does not simultaneously declare the work successful.

Useful? React with 👍 / 👎.

Comment on lines +163 to +168
if not isinstance(status, str) or status not in COMPATIBLE_OUTCOMES:
raise ReceiptRefused(
RefusalReason.OUTCOME_INCONSISTENT,
f"{status!r} is not a CXC report status this build maps. Accepted: "
f"{', '.join(REPORT_STATUSES)}. Read against {PACKAGE} {VERSION}; a newer "
"contract needs the mapping extended rather than the value guessed at",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid formatting unbounded invalid CXC statuses

On Python 3.11+, a JSON producer can pass an integer with more than the configured digit limit as cxc_status; the type check reaches this branch, but interpolating status!r raises Python's integer-conversion ValueError before ReceiptRefused can be constructed. This lets malformed report input escape the named-refusal path; report the invalid type without converting the value, as the pull-request-number validation already does.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant