Skip to content

fix(acp): make advertised mode changes safe - #3912

Open
probepark wants to merge 14 commits into
Yeachan-Heo:devfrom
probepark:fix/acp-session-mode-e2e
Open

fix(acp): make advertised mode changes safe#3912
probepark wants to merge 14 commits into
Yeachan-Heo:devfrom
probepark:fix/acp-session-mode-e2e

Conversation

@probepark

Copy link
Copy Markdown
Contributor

Summary

  • treat session/set_mode(default) as a safe no-op for ACP sessions
  • return a typed unsupported error for plan mode when no host plan-mode lifecycle exists
  • add production ACP regression coverage proving no unavailable mode.plan.set control is sent

Fixes #3905.

Verification

  • bun test packages/coding-agent/test/sdk-acp-production-path.test.ts
  • bun test packages/coding-agent/test/acp-startup-options.test.ts packages/coding-agent/test/acp/acp-fallback-cancel-completion.test.ts packages/coding-agent/test/sdk-acp-production-path.test.ts
  • bun --cwd=packages/coding-agent run check
  • real gjc --mode acp stdio initialize handshake

@yazzang-homelab yazzang-homelab left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Independent architect review. Stopping before the code: this branch shares no history with dev, so nothing here can be merged or meaningfully reviewed until that is fixed.

$ git merge-base origin/dev dc0f3fec
$ echo $?
1                      # no common ancestor

$ git rev-list --max-parents=0 dc0f3fec
19f8c1f437b1d1e5e4936f8a5d14177dc35aa2d6

$ git rev-list --max-parents=0 origin/dev
23983ef4a2126342a39bfda77dbd6a2ad91f44e0
9e281493210e16e617a275786bde10becf08c02e

Different root commits, 2188 commits on this branch, zero shared ancestry. Consequences:

  1. The diff GitHub shows is not the change. A PR diff is three-dot (base...head), which needs a merge base. There is none, so the +5685 figure is comparing whole trees, not your work.
  2. The contract file is being rewound, not advanced. packages/coding-agent/src/sdk/bus/telegram-daemon-contract.ts on this head has DAEMON_GENERATION = 5 at line 19; dev has 53 at line 84. Merging this would regress the daemon generation by 48 and take the file's shape with it. scripts/telegram-daemon-generation-guard.ts would reject it, and if it somehow landed it would break wire-compat handling for every running daemon.
  3. git merge-tree against dev fails outright rather than reporting file-level conflicts, which is why this shows as CONFLICTING with nothing actionable in the conflict list.

This looks like the branch was built on a rewritten or re-initialized copy of the repository rather than on Yeachan-Heo/gajae-code@dev.

What to do: the actual change appears to be two commits —

075f7a91f fix(acp): make advertised mode changes safe
dc0f3feca test(acp): cover session mode lifecycle behavior

Branch fresh from current dev and cherry-pick those two onto it:

git fetch upstream
git checkout -b fix/acp-advertised-mode-safe upstream/dev
git cherry-pick 075f7a91f dc0f3feca

Then re-open or force-push. Do not merge dev into this branch — with unrelated histories that needs --allow-unrelated-histories and would drag the entire foreign lineage in.

I have not reviewed the ACP logic itself; there is no reliable diff to review against. Ping me once it sits on a real dev base and I will do a full pass.

gajae.pr-review-verdict.v1 merge-blocked sha256:dc0f3feca926de4c99ed77e213433917e4b6d220 reviewer:architect evidence:git merge-base origin/dev dc0f3feca926de4c99ed77e213433917e4b6d220 exits 1 (no common ancestor); disjoint root commits

Add durable register/read/ack Codex handoff MCP tools with wake events
keyed by work-unit:event-seq for cross-process idempotent creation
(exclusive create, EEXIST loser), full durable schema validation, and
bounded allowlisted registration responses.

Publisher enforces unix/loopback-only endpoints, token-file-only
references (no token material persisted), thread/resume then idle-only
turn/start with deterministic clientUserMessageId, pending fallback for
active threads, and prompts that never include final_response text.

Wake pipeline: per-thread FIFO drains across sibling work units,
startup drain of persisted pending/failed wakes, durable failure
diagnostics in codex-wake-errors.log, and production question.opened
events emitted once per canonical pending question.

UserPromptSubmit persists exact $gjc-mcp-delegate-flow host context
without activating any workflow; four bundled workflows unchanged.

Includes focused restart/duplicate/security/fake-transport tests,
red-team suite, mutation-validated assertions, and coordinator
stdio smoke evidence in artifacts/.
…real app-server transport

Delegate creation (gjc_delegate_plan/execute/team) now consumes persisted
$gjc-mcp-delegate-flow host context and auto-registers each new session
to the source Codex thread: N:1 thread-to-work-unit registrations with
validated origin metadata (delegate session/turn, Codex thread/turn,
host session, delegation id, workflow), exclusive hard-link publication
that never overwrites, fresh-first origin-free fallback with stale and
cross-host-context ambiguity failing closed via durable diagnostics, and
codex_handoff {auto_bound, thread_id} in delegate responses. Auto-bind
failures never break delegation.

Real Codex app-server compatibility (verified read-only against the
installed codex-cli 0.144.5 socket): WebSocket upgrade over unix or
loopback TCP with bracketed IPv6 Host, token-file content sent only as
an Authorization Bearer upgrade header (never RPC params), bounded
establishment deadline with end/close rejection, masked frames with
ping/pong and id-matched responses, initialize -> initialized ->
thread/resume (thread.status.type idle-only gate) -> turn/start with
schema-shaped input [{type:'text',text,text_elements:[]}]. Wake prompts
carry identifiers only; no event summary or final_response text.

Wake drains stop on first transport failure, keep pending wakes for the
next trigger or startup drain, and stay serialized per Codex thread.
Delegate-flow prompts never activate workflow skills; host-context
persistence is best-effort with bounded byte caps and validated
enumeration. Hermes list_questions/submit_question_answer remains the
only question channel; exactly four workflows preserved.
…egate auto-bind

When Codex is the MCP host, no ambient .gjc host-context exists (that
file is written only by GJC's own UserPromptSubmit hook), so implicit
inference cannot identify the host Codex session. Add the smallest
explicit correlation input: an optional codex_host_session_id on
gjc_delegate_plan/execute/team carrying the session_id previously
passed to gjc_coordinator_register_codex_handoff.

A valid explicit id resolves the source registration deterministically
via readCodexHandoff and skips ambient enumeration entirely; missing,
malformed, or corrupt sources record a durable
codex_handoff_explicit_source_missing diagnostic and return
auto_bound:false without failing delegation. Absent parameter keeps
the ambient host-context fallback for the GJC-host case.

Installed plugin contract (generated commands + gjc-delegation skill)
now documents the correlation flow: register the app-server handoff,
then pass the same session_id on delegate calls so new GJC sessions
wake the owning Codex thread on completion/questions.
…us write itself fails

Port the corrective patch from feat/codex-gjc-resume-bridge (d57dfae):
publishRecordedCodexWake now appends a bounded per-wake diagnostic line
(wake=<key> error=<code>) to codex-wake-errors.log before recording the
failed status, and guards the updateCodexWakeEvent call so a failing
durable write cannot mask the original transport error — the update
failure is also appended as a diagnostic. The drain still stops on the
first failed outcome and never storms the endpoint.
…nsport RED proof

Regenerated protocol bindings from the installed CLI
(codex app-server generate-ts, codex-cli 0.144.5) and hardened the
WebSocket fixture to enforce them: raw JSONL (non-WebSocket) clients are
destroyed before any exchange, requests before initialize/initialized
receive JSON-RPC -32600, and turn/start bodies using legacy prompt or a
non-conforming input array receive -32602 (TurnStartParams requires
input: [{type:'text', text, text_elements}]; clientUserMessageId is
retained because it is present in the generated schema).

Two regression tests prove the f792165-era transport fails against
this fixture: a raw newline JSON-RPC prompt-based client is rejected
with zero accepted messages, and pre-initialize or prompt-shaped
requests fail while schema-shaped requests succeed. No invented
thread/status method is used anywhere; idle/active derives from the
documented thread/resume response thread.status.

Evidence artifact updated with the generated-schema findings, fixture
hardening notes, RED transcripts, and a fresh read-only GREEN smoke
against the installed app-server socket (initialize returns the real
userAgent; bogus thread/resume yields a JSON-RPC error over WebSocket).
…ched app-server smoke

The generated ClientNotification type is { "method": "initialized" }
with no params member, so the initialized notification now omits params
entirely (transport notify accepts optional params and drops the member
when absent).

Evidence adds a full end-to-end smoke against a self-launched installed
`codex app-server --listen unix://...`: initialize -> initialized ->
thread/start (status.type idle) -> turn/start with the exact generated
TurnStartParams shape ({threadId, clientUserMessageId, input:
[{type:'text', text, text_elements:[]}]}) ACCEPTED with a real turn id,
then turn/interrupt cleanup. Cross-scope thread/resume returning
-32600 "no rollout found" is documented as the failed-wake path.
…lan names

Rename gjc_coordinator_ack_codex_wake to gjc_coordinator_ack_codex_handoff
across contract, server, tests, docs, and the generated plugin skill/
commands (tool count stays 22; wake_key argument unchanged).

Expose the approved public lifecycle via a schema-versioned mapping
instead of a durable-state migration: codexWakeLifecycle maps the
durable statuses (pending/published/acked/failed, schema_version 1
on disk) to requested/delivered/acknowledged/failed; read responses
carry lifecycle_schema {version, mapping} and every returned wake event
is decorated with its lifecycle label. attempts/last_error remain the
documented failure/retry metadata.

Capability-gate the heartbeat honestly: no callable automation_update
exists in this environment, so register/read responses report
heartbeat {supported:false, reason:"automation_update_unavailable"}
and no scheduling code exists or is claimed. Wake delivery stays
event-driven with startup drain, keeping parent-turn wakes durable
while heartbeat is unavailable.
…rver_timeout) and GREEN on the same live launched app-server socket
…h hostile-injection e2e

buildCodexWakePrompt already carries only the resume instruction plus
work_unit/wake_key and optional turn/question identifiers (summary was
removed in the transport rewrite); this locks the contract end-to-end.

New red-team test appends a real coordinator event whose summary
contains instruction injection (IGNORE ALL PREVIOUS INSTRUCTIONS,
rm -rf), question text, delegated-output and final_response sentinels,
and a 50KB log dump, then asserts the app-server turn/start input text
contains none of it - identifiers and the fixed instruction only,
bounded under 500 chars - while the summary persists solely as bounded
(<=240 char) durable diagnostic metadata. Mutation proof recorded:
reintroducing the summary line into the prompt fails the test.
…y token capture tests

The reviewed params-token merge was already removed (token flows solely
as Authorization: Bearer in the HTTP Upgrade handshake); a new
handshake-capture test locks it: the header is present only when
token_file is configured, absent otherwise, and the token string never
appears in any JSON-RPC frame payload.

Manual framing is retained (no maintained WebSocket client in the tree
supports unix sockets) and now assembles fragmented messages per RFC
6455: a FIN=0 text frame followed by opcode-0x0 continuation frames is
buffered and delivered as one message, so thread/resume cannot time out
on normal fragmentation. RED captured with a fixture emitting a legal
fragmented response split across TCP chunks with a notification frame
first (pre-fix client timed out); GREEN after the fix.

Real self-launched app-server smoke extended to record the initialize
response shape, thread/start idle status, accepted schema-shaped
turn/start, and a live active-status thread/resume during the running
turn (interrupted for cleanup).
…d desktop control socket; confirm Bearer-only auth and disposable-thread turn/start policy
…ping

The reported swap (Codex host context ids written into gjc_session_id/
gjc_turn_id with codex_turn_id null) existed only in the first auto-bind
iteration and was already corrected: gjc_session_id is the new delegate
coordinator session, gjc_turn_id the accepted GJC turn id,
codex_thread_id equals source.thread_id (enforced by the binder),
codex_turn_id carries context.turn_id, and codex_host_session_id is the
dedicated Codex host session field.

New assertions prove two concurrent delegates get DISTINCT GJC
session+turn identities while sharing one Codex thread and preserving
identical codex_host_session_id/codex_turn_id correlation, and that GJC
ids never equal Codex host ids. Mutation proof recorded: reintroducing
the swapped mapping fails the test; the corrected mapping passes.
@Yeachan-Heo
Yeachan-Heo force-pushed the fix/acp-session-mode-e2e branch from dc0f3fe to bfea3a8 Compare August 6, 2026 11:46
@Yeachan-Heo

Copy link
Copy Markdown
Owner

Closing during the emergency maintenance freeze. This PR is not in the retained critical or maintainer-owned set. Do not open a replacement PR unless a maintainer explicitly directs it.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo Yeachan-Heo closed this Aug 6, 2026
@Yeachan-Heo Yeachan-Heo reopened this Aug 6, 2026
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.

3 participants