Skip to content

Build 4/7 — Delivery receipts and settlement: inbox receipt, Codex verified, UNKNOWN never resends #77

Closed

Description

@Simon-AI-coding

Part of #67 · Build group 4 of 7 from the signed port table (amended 2026-08-25: P14, P15 added to this group). Revised after the Codex consistency review.

Rows covered

P2 transitions (simplify) · P7 Claude Answer receipt (adapt: route and receipt both change) · P8 Codex Answer receipt (port already present; verify) · P9 settlement and duplicate safety (port the rule only; one policy edit) · P14 approval outcome wording (adapt to #61 R5) · P15 Companion reply outcome (port the rule, simplify storage; #61 C2).

Legacy citations (verbatim from the table)

  • P2: legacy@1d32845:bridge/store.py:1644-1666,1738-1755,2638-2671,2798-2831.
  • P7: legacy@1d32845:bridge/claude.py:1-21,329-393 — evidence for the superseded channel route, not an implementation to retain.
  • P8: legacy@1d32845:bridge/codex.py:1264-1317,1529-1575 — exact-ID clientUserMessageId readback; v1 already has it (adapters/agent/codex/adapter.py:340-393,426-461,696-735).
  • P9: legacy@1d32845:bridge/delivery.py:28-75; bridge/coordinator.py:1075-1109; bridge/store.py:964-1035,3394-3555,3653-3874 — prove the invariant only.
  • P14: legacy@1d32845:bridge/transcript.py:1633-1713; bridge/daemon.py:1901-2052 — legacy had no approval transport, so it never claimed a verdict arrived.
  • P15: legacy@1d32845:bridge/channel.py:11-13,75-86; bridge/store.py:1517-1614; bridge/daemon.py:830-879.

Shape (binding constraint: P9 is tens of lines in core/relays.py, no new module)

  • P2 transitions (core/sessions.py, ≤ 30 lines): reply_window is derived from the inspection (Build 1/7 — Discovery, roster and reachability: lock the one Session-inspection type #74) — this ticket adds the transitions: a SessionStopped with waiting_for.kind ∈ {question, permission} → OPEN if reach.relay == attached; a new turn observed (state=running) → CLOSED; SessionEnded → CLOSED and pending Relays for that target are settled FAILED with "Session ended". No stored window, no supersession ledger.
  • P9 (core/relays.py:115-181, ≤ 40 lines): DELIVERED completes; proven FAILED may retry under the existing current-Engine policy (ten-minute bound, Parity audit: legacy as the behaviour spec — the gap table that scopes the repair #61 R2); UNKNOWN is never auto-resent — retained as duplicate-risk information, surfaced with the warning, a second attempt needs explicit user authority. No coordinator, no state machine, no persistence.
  • P14 (core/approvals.py:167-208, ≤ 20 lines): only a DELIVERED receipt may announce "approved/denied"; FAILED/UNKNOWN says "not confirmed" and points the user to the original dialog.
  • P15 (core/bridge.py:671-680, ≤ 20 lines): the direct Companion reply reports the Telegram adapter's receipt grade and reason; no resend, no persistence; token and full message never logged.
  • P7 — Claude route (adapters/agent/claude/adapter.py, ≤ 200 lines net after the channel stack is removed): answer_relay targets the inbox socket Build 1/7 — Discovery, roster and reachability: lock the one Session-inspection type #74's registration recorded for the exact target; what earns DELIVERED is Prototype the Claude lane: official roster, built-in inbox socket and user-scope hooks, with no wrapper #71's ruling, copied verbatim into this section before code (socket write accepted / inbound delivered / turn started / correlated observation); everything weaker is UNKNOWN. Inbox hold / refuse and the peer-authority limit are their own honest receipts (FAILED with reason, or UNKNOWN), never mapped onto the old acknowledged_by_claude.
  • P8 — Codex route: unchanged; add the verification test on the daemon route, delete channel-era tests.
  • Removed in the same branch: adapters/agent/claude/channel* (ADR 0006 server, plugin rendering, --channels selection) and their tests; ADR 0006 / 0007 marked Superseded (Prototype the Claude lane: official roster, built-in inbox socket and user-scope hooks, with no wrapper #71 has proved the replacement by then).

Left behind

P2: both Stop ledgers, their recovery transitions, old Reply Windows; an unattached row never gets an open window. P7: queued_for_claude / acknowledged_by_claude, the MCP Channel adapter, the legacy synchronous client, durable acknowledgement queries. P8: the legacy per-TUI lease ledger and synchronous app-server client. P9: delivery-attempt history, confirmed-request history, crash enquiry, cross-restart replay, any new delivery/coordinator module. P14: any wording that reports a verdict on a non-DELIVERED receipt. P15: the durable channel ledger, auto-resend.

Red first

Acceptance steps relay, approval, companion inbound red before; green after, both lanes.

Done when

Steps green on Simon's machine; pytest + ruff green; the core/relays.py diff is a policy edit (reviewer checks: no new class, no new file).

Blocked by

#74 (locked types); #75 (typed waiting_for drives the P2 transitions); #71 (receipt definition).

Activity

  1. Simon-AI-coding commented on Aug 26, 2026

    @Simon-AI-coding
    OwnerAuthor

    Advisor note from #83 (2026-08-26)

    #83's Done-when bullet 4 has two halves. The installation-side half (missing managed binary, unwritable ~/Library/LaunchAgents, launchctl bootstrap refused → explicit Outcome reason through bridge-install status/reconcile) is #83's and ships there. The pre-wire FAILED half — a Relay or Approval against a Codex Session with no shared daemon returns FAILED with a reason before anything is sent — is delivery behaviour and lands here, on the shared-daemon route this ticket builds. #74 supplies the discovery side of the same fact (LaneDiscovery.degraded when the daemon is absent, rows still listed, ruled 2026-08-26). Nothing in #83 builds a second copy of the daemon route.

  2. Simon-AI-coding commented on Aug 26, 2026

    @Simon-AI-coding
    OwnerAuthor

    Advisor note from #75 (2026-08-26)

    Two things #75 settles that P14's wording and P2's transitions build on:

  3. Simon-AI-coding commented on Aug 26, 2026

    @Simon-AI-coding
    OwnerAuthor

    Advisor note from #75 (2026-08-26) — signature change

    #75's pure module is analyse(records: Sequence[Mapping]) -> WaitingFor; the notification: str | None parameter #75's body locked is dropped. v2 installs no Notification hook, so nothing could fill it. Legacy's first-turn invariant (tool name known before the transcript flushes, legacy@1d32845:bridge/daemon.py:143-145,213-223,2049-2051) is adapted onto the PermissionRequest hook: the Claude adapter overlays ApprovalListener.pending() for the exact target after analyse, with fixed precedence — a readable question always wins; a pending request fills approval_id/tool_name on a permission, or turns UNKNOWN/NONE into PERMISSION with caught_up left as the parser reported it. Cite this signature, not the ticket body's.

  4. Simon-AI-coding commented on Aug 26, 2026

    @Simon-AI-coding
    OwnerAuthor

    Advisor: two items added to this ticket's scope from #75's review (2026-08-26)

    Both found and verified by #75's worker, both fenced out of #75 by ruling because they are transitions and Bridge Core policy. They are this ticket's, explicitly.

    1. A Session entering waiting raises no SessionStopped — a question reaches no outlet

    adapters/agent/claude/window.py:293 emits SessionStopped only on was_active and status == "idle", and STATUSES_MEANING_TURN_ACTIVE = {"busy", "waiting"} (:118; pinned by tests/test_claude_reply_window.py:292). So when a Session raises a question or a permission it is still "turn active"; when the user answers and it goes idle, the transcript carries the tool_result, analyse correctly says NONE, and the notice that finally fires says nothing about what it stopped on.

    The permission half already reaches the user by another road (approval.py:489 → AwaitingApproval → core/bridge.py:461 → approvals.opened()). The question half has no announcement route at all: AskUserQuestion raises no PermissionRequest hook (unmeasured on 2.1.246 whether it raises a permission dialog of its own — measure it here). After #75 the question is on the roster row, with prompt, options and recommendation, for as long as the Session waits — visible, not announced. CONTEXT.md's Stop Notice names "the question with its options" as its content, so this is a v1.0 gap, not polish.

    The fix is one edge in window.py (busy → waiting also raises SessionStopped, waiting_for already available from #75's read) plus the policy it opens: a permission would then announce twice, once as AwaitingApproval and once as SessionStopped. Bridge Core decides which announces and which is suppressed; the adapter does not.

    2. Codex WaitingFor projection

    adapters/agent/codex/adapter.py:647 emits AwaitingApproval from the app-server request and never fills WaitingFor(kind=permission, …), so a Codex roster row and a Codex SessionStopped cannot say what the Session stopped on. #75 is Claude-only by its Shape and by the port table ("Codex's existing app-server request path stays outside the module"). The projection from the app-server request into the locked WaitingFor is one row here, beside P8 — no transcript parser for Codex, ever.

  5. Simon-AI-coding commented on Aug 26, 2026

    @Simon-AI-coding
    OwnerAuthor

    Advisor note (2026-08-26) — the shared-daemon client now comes from #76

    Supersedes the sentence above, "the shared-daemon route this ticket builds". #76 builds the read-side client (adapters/agent/codex/shared_daemon.py, _shared_daemon() returning a kept, reconnecting attach() connection or None), because its own progress step on the Codex lane is red by construction without it. #77 consumes that connection for Relay, Approval, the Codex WaitingFor projection and the pre-wire FAILED — it does not open a second one, and it does not move the daemon's lifecycle (join-only, ADR 0012). Ruling and bounds on #76.

  6. Simon-AI-coding commented on Aug 26, 2026

    @Simon-AI-coding
    OwnerAuthor

    Advisor rulings (2026-08-26) — the worker's three questions

    Q1 — the "question/permission → OPEN" transition is dropped. derive_reply_window (seams/agent.py) stays OPEN iff idle and main, and gains no condition in #77. The Shape's sentence predates #74's removal of reach/attached and #71's measurement: the inbox route carries words, never authority, so a question is never answered over it. P2 lands as tests for the two transitions already true in code (running → CLOSED from the derivation; SessionEnded → CLOSED + pending Relays FAILED via core/bridge.py:_session_ended). Still #77's measurement: whether AskUserQuestion raises a PermissionRequest hook on 2.1.246 — the only fact that could give the question half a route (through the approval channel). A throwaway claude probe on this machine is Simon's per-run authorisation in the worker's session; if not run, "unmeasured on 2.1.246" is written here. Probe (i) — a question consuming an inbox message — is not run; #71 makes it moot.

    Q2 — (a): answer_relay / approval_relay move onto SharedDaemon.client(). _threads is populated only by register_session, which nothing in src/ calls, so both verbs are dead in the assembled engine. The shared daemon is Codex reach on this map; "unchanged; verify" was written before the client existed and now means: the receipt protocol (exact-ID clientUserMessageId readback) keeps its rule and moves wire. Pre-wire FAILED = _shared_daemon() is None with the reason. core/relays.py remains a policy edit. self._own stays only for its remaining production callers (Live Call use_app_server, adapters/call/realtime/adapter.py:174; codex/adapter.py:480), listed in the resolution; not deleted here; no second daemon connection.

    Q3 — confirmed. In core/bridge.py:_session_stopped, a SessionStopped with kind is PERMISSION and as_approval_request(target) is not None is suppressed (logged): the AwaitingApproval notice carries the budget, fallback and closing, and already reaches EVERY_OUTLET. A PERMISSION with no approval id is announced, and the notice says the user answers it at the terminal. QUESTION is always announced.

    P7 receipt: #71's ruling verbatim. Base: main at da89dbd (#98 merged; stopped_on now takes the dialog's tool name, adapters forget a Session at their own SessionEnded).

  7. Simon-AI-coding commented on Aug 26, 2026

    @Simon-AI-coding
    OwnerAuthor

    Advisor ruling (2026-08-26) — the question half, after probe (ii) measured true

    AskUserQuestion raises a PermissionRequest hook on 2.1.246 with the whole question in tool_input.questions[] and a prompt_id; a deny with a message is consumed by the Session as the answer (worker's measurement; evidence pasted below by the worker). B in #77: the Claude adapter's existing _overlay fills WaitingFor(kind=QUESTION, prompt, options, approval_id=prompt_id) from that payload — the hook's question (the thing itself) wins over the transcript's reconstruction; Option.recommended has no source here and stays None; as_approval_request still returns None for a QUESTION, so nothing carries a verdict into it and the notice says the question is answered at the terminal; a multi-question payload projects the first and states the count. derive_reply_window gains nothing — this route is not a Relay. C — answering the question from the bridge — is filed as its own ticket, a candidate on #67 pending Simon's call on v1.0. Also to land in this diff: a comment beside approval.py:100-115 recording that the permissionDecision output shape has no effect on 2.1.246 and that approval_ack over our socket, not the hook's exit, is the proof.

  8. 5 remaining items

  9. Simon-AI-coding commented on Aug 26, 2026

    @Simon-AI-coding
    OwnerAuthor

    Advisor (2026-08-26), after the codex-lane review: (1) the procStart finding — the reply key's time is UTC, not a twelve-hour clock as #71 recorded; measured on eleven live Sessions — is posted on #71 as a correction. (2) core/relays.py budget is measured on implementation lines (ast; docstrings/comments excluded), per the standing rule; the number goes in the resolution; the named check (no new class, no new file) holds and is binding; prose is not cut to hit a count. (3) Legacy classifications for the three uncited behaviours go in the resolution: busy→waiting Stop edge adapted from legacy stop detection; permission suppression — legacy has no such behaviour (no approval transport); shared-daemon routing — legacy's per-TUI lease ledger and synchronous client dropped, because the shared daemon owns the threads (ADR 0012). (4) Feature Envy at codex/adapter.py:504 and shared: bool at threads.py:63 are judgement items, left. One re-review follows.

  10. Simon-AI-coding commented on Aug 26, 2026

    @Simon-AI-coding
    OwnerAuthor

    Advisor (2026-08-26), item B, final: the (recommended) marker lives inside the option label (stop_analysis.RECOMMENDED_MARKER), so the hook payload carries it whenever the model wrote it. The earlier clause "recommended False on the hook route" is withdrawn; the shared parser is unchanged and recommended=True appears on either route exactly when the marker was written — a fact read, not an inference; order is never consulted; the label is the spoken words with the marker stripped, as #75 landed. Pinned by a hook-route test. core/relays.py budget: 44 implementation lines added by the defined count (reviewer's method: 62), no new class or file — in scope, nothing cut. Acceptance on Simon's machine: relay, approval, companion inbound green on the Claude lane ("delivered via deliver (delivered); relay.txt contains BRAVO"); Codex lane blocked earlier in the journey, mechanism to be stated in the resolution.

  11. Simon-AI-coding commented on Aug 26, 2026

    @Simon-AI-coding
    OwnerAuthor

    Resolution — Build 4/7, all six rows landed

    Branch delivery-receipts-77, eight commits ahead of main @ da89dbd. pytest (395) and ruff green; the commit messages carry the reasoning and this comment carries what is not in any one of them.

    The rows

    row where it landed class
    P2 transitions 3a56966 — zero lines of source. The ticket's question/permission → OPEN transition was dropped by ruling (Q1 = A): reach went with #68 and #71 proved the inbox can never answer a question. The other two transitions were already true in landed code and are now covered by tests. simplify
    P7 bcdf75e + 43ff6a5 — the Answer Relay moved off ADR 0006's channel onto the Session's own inbox socket. Net −249 lines of source. adapt
    P8 67762ad — the exact-ID clientUserMessageId readback kept its rule and moved wire, onto #76's shared-daemon connection. port already present; verified
    P9 add2b34 — a policy edit; see the budget note below. port the rule only
    P14 3a56966 — only a DELIVERED receipt may announce a verdict. adapt
    P15 3a56966 — the Companion reply is attempted once, graded, never re-sent. port the rule, simplify storage
    B (question projection) c69d562 + 04c14af — from the advisor's ruling after probe (ii). adapted
    Codex WaitingFor projection 67762ad (roster row) + 55995dc (the Stop). new; see classification below
    pre-wire FAILED (from #83) 67762ad new

    Acceptance — the three steps this ticket owns

    Run twice on Simon's machine against the real far side: claude 2.1.246, codex-cli 0.149.1, the real Telegram bot and his own Telegram account, a bundle byte-identical to e553c61.

    Claude lane — all three green (run 20260826T105036Z):

    Also red in that run and not this ticket's: child (#79, which its own evidence names) and the 0b realtime contract probe (0 audio frames in 30 s).

    Codex lane — blocked before those steps, and here is the mechanism. The shared daemon answers both thread/read and thread/resume for the acceptance's freshly-started TUI thread with list_turns is not supported yet:

    codex.discovery: the daemon could not describe thread 01a03dad-…: thread/read failed: list_turns is not supported yet
    codex.adapter:   could not resume 01a03dad-… on the shared daemon: thread/resume failed: list_turns is not supported yet
    

    So stable name (#78) and progress (#76) fail, no turn is driven, and relay / approval / companion inbound are skipped rather than red — unobservable on this lane, not failing.

    The mechanism, with the confidence it deserves: in the same run a different Codex thread on the same daemon — 01a02110-…, one of Simon's own, which had taken turns — was described successfully and delivered a permission request all the way to AwaitingApproval. The acceptance's thread had taken none ("no turn ran"). Hypothesis, consistent with every observation in this run but measured only here: list_turns fails on a thread with no turns. That makes the Codex lane's acceptance a chicken-and-egg — the harness drives a turn only after stable name and progress pass. Reported rather than worked around, as ruled. It rides #78's exit item.

    An incident, and the harness fix it forced

    The first run (20260826T103838Z) granted a permission belonging to a Session that was not the harness's own. _answer_pending_approval took pending_approvals[0] with no filter — safe when it was written, because no Codex approval could ever be pending, and unsafe as of this branch, where the Codex lane reaches every thread on the shared daemon. It ran:

    bridgectl approve item/commandExecution/requestApproval:01a03d68-…:call_1BfHvNrtNOZpCEDbM0CSO5SB allow
    

    The command was itself a probe (create /tmp/vcprobe/approval-probe.txt), so nothing was harmed — and the mechanism would not have known the difference. The lane's own Write then waited four and a half minutes for the next pass, which is why that run reported relay as "a receipt without an effect" and companion inbound as an announcement nobody was left to read: two red steps and one silent grant, from one missing filter. Fixed in 0f03fe2 — the address is the filter, as it is everywhere else in that file. Worth knowing beyond this ticket: an engine now sees, announces and can answer the dialogs of every Codex Session on the machine. That is the product working as designed, and it is a new fact for anything that reads pending_approvals.

    Legacy classifications the commit messages did not state

    Per CLAUDE.md, one line each, at the advisor's direction:

    • adapters/agent/claude/window.py:298 — the busy → waiting Stop edge: adapted from legacy's stop detection (legacy@1d32845:bridge/daemon.py:143-145,213-223) onto the roster's own state transition, because v2 installs no Notification hook.
    • core/bridge.py:592 — suppressing a Stop whose dialog the Approval Relay announces: legacy has no such behaviour. It had no approval transport, so one stop never had two announcers.
    • adapters/agent/codex/adapter.py:419, shared_daemon.py:217, threads.py:63 — the shared-daemon routing, subscription and connection ownership: legacy's per-TUI lease ledger and synchronous client are dropped, because the shared daemon owns the threads (ADR 0012, and this ticket's own "left behind" list). The exact-ID readback beside them is the ported part, cited in 67762ad.

    The reviewer check, and the P9 budget

    The named check holds. The core/relays.py diff adds no class and no file: three module-level functions (may_be_retried, confirmation_for, _report_for) and nothing else.

    The budget, stated honestly because two counts disagree. Raw: 139 added, 16 removed. Implementation lines only — ast-visible tokens, docstrings and comments excluded, which is the standing rule: 44 added / 6 removed, net +38 by my count, 62 added by the reviewer's. The gap is method, not diff: mine collapses identical lines (repeated ), else:), the reviewer's does not. Ruled in scope by the advisor, with nothing cut — prose is not trimmed to hit a number.

    Facts worth carrying out of this ticket

    • procStart is UTC, and Prototype the Claude lane: official roster, built-in inbox socket and user-scope hooks, with no wrapper #71's record of it was wrong — posted there as a correction. It was recorded as "a twelve-hour clock with no AM/PM"; this machine is UTC+12 and that was the offset. The two readings are the same string for a local afternoon and a whole day apart for a local morning, so the ported formula was wrong on the hour and the date at once, for every Relay before local noon, silently. Measured against all eleven live Sessions' key files: eleven matches, no mismatches.
    • The permissionDecision output shape has no effect on 2.1.246 — the evidence comment above, and a note beside approval.py:100-115 in this diff. Drift in that shape is indistinguishable from a hook answering ask, which is why the route's proof is approval_ack over our own socket.
    • AskUserQuestion's recommendation mark lives in the option's label, by the tool's own instructions — so it is carried on the hook route as it is on the transcript route, one parser, one reading. The earlier "always False here" was written believing the payload could not carry it and is withdrawn.
    • The engine's own app-server keeps exactly two production callers, neither touched: the Delegated Turn (engine/composition.py:418 → adapters/call/realtime/adapter.py:174) and adapters/agent/codex/adapter.py's own verify.

    ADRs

    0006 Superseded by ADR 0013, which is new here and records the decision Simon took on #71 — the socket route, on private surface, knowingly, with no version pin and the honest-receipt rule that replaces one. 0007 was already Superseded by ADR 0011. The two are one map: the inbox for words, the hooks for authority.

    What is deliberately not here

    C — answering a question from the bridge — is #103, in v1.0 as Build 8/7 after #80. It leans on two things this diff keeps exactly: approval_id = prompt_id on the projected QUESTION, and the option labels as spoken words. One thing for it to decide first: a flattened multi-question call has one prompt_id for several answers, so #103 has to settle how an answer addresses a group before it sends one.

  12. Simon-AI-coding commented on Aug 26, 2026

    @Simon-AI-coding
    OwnerAuthor

    Closed — merged as e2090aa (#104)

    Squashed onto main; delivery-receipts-77 deleted, local and remote. The resolution comment above holds the row-by-row account, the acceptance evidence and the legacy classifications; this is the closing state and what leaves this ticket for other people to pick up.

    Done-when, item by item

    the ticket asked state
    Steps relay, approval, companion inbound green on Simon's machine Green, Claude lane, against claude 2.1.246 and the real Telegram bot, on a Session started by hand with no wrapper. relay: delivered via deliver (delivered); relay.txt contains BRAVO — the receipt and the effect. Codex lane blocked before those steps; mechanism below.
    pytest + ruff green 395 tests, ruff check, ruff format --check and swift format lint green on the merged main.
    The core/relays.py diff is a policy edit — no new class, no new file Holds. Three module-level functions (may_be_retried, confirmation_for, _report_for) and nothing else. Budget stated honestly in the resolution: 139 raw added lines; 44 added / 6 removed by one implementation-line count, 62 added by the reviewer's — the gap is method, not diff. Ruled in scope, nothing cut.

    What leaves this ticket

    #78 absorbs the Codex lane's blocker. The shared daemon answers both thread/read and thread/resume with list_turns is not supported yet for the acceptance's freshly-started TUI thread, so stable name and progress fail, no turn is driven, and this ticket's three steps are skipped rather than red on that lane — unobservable, not failing. In the same run a different thread on the same daemon, one that had taken turns, was described and delivered a permission all the way to AwaitingApproval. Hypothesis, consistent with every observation here and measured only here: list_turns fails on a thread with no turns, which makes the Codex acceptance a chicken-and-egg, since the harness drives a turn only after those two steps pass.

    #103 (answering a question from the bridge, Build 8/7 after #80) leans on two things this diff keeps exactly: approval_id = prompt_id on the projected QUESTION, and the option labels as the spoken words. One thing for it to settle before it sends anything: a flattened multi-question call carries one prompt_id for several answers.

    A new fact for anything that reads pending_approvals: an engine now sees, announces and can answer the dialogs of every Codex Session on the machine, because the Codex lane reaches every thread on the shared daemon. That is the product working as designed. It is also what made the acceptance harness grant a permission that was not its own on the first run — pending_approvals[0] with no filter, fixed here, and the reason to check any other consumer that assumes the list is its own.

    #54 takes the ADR 0006 channel server's remnants outside src/, per Simon's note.

    #55 still owns the acceptance's one secret. It has no durable home, so it lives in a gitignored .env this checkout reads and nowhere else.

    #71 carries a correction, posted there: procStart is UTC, not "a twelve-hour clock with no AM/PM" — that machine is UTC+12 and the probe saw the offset. Wrong on the hour and the date at once for every Relay before local noon, and silently. Eleven live Sessions' key files reconstructed from ps: eleven matches, no mismatches.

    Cleared

    The probe rig (/tmp/gptvc-77-probe/), its six transcripts under ~/.claude-b/projects/, and the gitignored research file are deleted at Simon's word. Everything they proved is in the evidence comment above. ~/.claude-b/settings.json is byte-for-byte as #71 left it — sha256 1f0117cb63d854d01abf5e3524dd00a8e625a213593cd1f9f7bb82cf05ae1706, verified before and after.

  13. Simon-AI-coding commented on Aug 26, 2026

    @Simon-AI-coding
    OwnerAuthor

    Cross-note: the Codex approval route this ticket built is proven on the real machine by acceptance run 20260826T213402Z — a real item/fileChange/requestApproval, escalated to the Companion Channel (message 3655) before anything answered it, answered through bridgectl approve, with the effect landing outside the sandbox where Codex could not have written it alone. It took #105 to relay an instruction Codex would actually ask about; see #105 and PR #108 for the measurement.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions