Repository navigation
Conversation
…ped in execute
OpenCode 2.x routes MCP tool calls through its built-in `execute` tool, so the
ACP update arrives as rawInput {"code": "tools[\"codeg-mcp\"].delegate_to_agent(
{...})"} instead of JSON args. Registration, frontend classification and cold
replay all keyed on the JSON-args shape, so opencode-as-main-agent delegations
fell back to synthetic unkeyed ids: no delegation card, meta lost on refresh,
and the FIFO pool could hand a call another call's entry.
Adds a conservative JS-source scanner (Rust + TS mirrors) that extracts the
delegation match key from the execute payload under the contract "never mint a
keyed entry the MCP round-trip cannot claim": transparency pre-pass bails on
spread/computed keys/parens/regex/shorthand key words, template cooked text
follows ECMA-262 TV semantics (only CR/CRLF fold to LF; U+2028/2029 kept),
line comments end at every JS line terminator, keys match at brace depth 1
with LAST-wins inside a single object, working_dir present-but-unreadable
bails the whole key, and any assistant_id spelling degrades to unkeyed.
The replay gate in conversations.rs reuses is_delegation_invocation as the
shape predicate so persisted execute blocks rebind on reload via
tool_use_id -> parent_tool_use_id -> task id. Broker pending-overview WARN now
truncates dir like task.
Collapse the empty line after the code-mode doc comment in lifecycle.rs (clippy 1.99's empty_line_after_doc_comments fires under -D warnings and fails every Rust job), and reformat the two delegation-card spots prettier flags: single-quote the template-literal fixture string and unwrap the workingDir ternary that now fits the print width.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
OpenCode 2.x no longer surfaces MCP tool calls as first-class ACP tool calls with JSON args: it routes them through its built-in
executetool, so the update arrives asrawInput: {"code": "tools[\"codeg-mcp\"].delegate_to_agent({agent_type: \"codex\", task:…})"}— raw JS source. Registration, frontend classification and cold replay all keyed on the JSON-args shape, so with opencode as the main agent every delegation:executecard instead of a delegation card,delegation-<uuid>id (unkeyed), losing child binding and the "查看会话" affordance, with meta gone after refresh,What this adds
A conservative JS-source scanner that extracts the delegation match key from the
executepayload, implemented twice (Rust inacp/lifecycle.rs, TS mirror inlib/delegation-card.ts) under one governing contract: never mint a keyed entry the MCP round-trip cannot claim — when the scanner cannot evaluate syntax, the call degrades to the (pre-existing) unkeyed path instead of producing a wrong key.code_mode_call_spanlocates the accessor-chain call; a transparency pre-pass bails on spread/computed keys, nested parens, regex literals and shorthandworking_dir/assistant_ididentifiers (a shorthand's value is a runtime variable — keying its absence would mint an entry no round-trip can claim).\+newline continuations contribute nothing. Line comments end at every JS line terminator (LF/CR/LS/PS), so a trailing comment cannot swallow the real code that follows.working_dirpresent-but-unreadable (template/escape/variable/concatenation) bails the whole key;assistant_idin any spelling degrades to unkeyed (its present value resolves to Assistant on the MCP side and would win overagent_type).commands/conversations.rsreusesis_delegation_invocation(nowpub(crate)) as the shape predicate for a third binding branch, so persistedexecuteblocks rebind on reload viatool_use_id→parent_tool_use_id→ task id, exactly like live registration — the two paths cannot drift.dir=liketask=(log hygiene).The scanner is deliberately shared-shape with the MCP listener's own dispatch (the 对拍 test
code_mode_key_matches_the_mcp_side_for_the_same_callpins Rust scanner output againstDelegationRequest::target()/serde task parsing for identical emissions), so keyed entries always match what the round-trip will claim.Testing
cargo test --lib: 6018 passed / 1 failed (commands::remote_proxy::tests::an_event_socket_gets_past_an_address_that_never_answers, pre-existing environment case on this machine) / 2 ignored — includes 37 newcode_mode_*unit tests (bails, depth, LAST-wins, TV normalization, comment terminators, and the 对拍 pair).pnpm vitest run src/lib/delegation-card.test.ts: 71/71 (adds the TS mirror cases: emission parsing, escape set, LS/PS preservation, shorthand/paren/regex bails, argument-object close, comment terminators).tsc --noEmitclean.tools["codeg-mcp"].delegate_to_agent(...)throughexecuteregisterskeyed=true(log:registering parent … (keyed=true)) with no synthetic-fallback WARN, while the same calls previously fell through unkeyed.Possible follow-ups (not in this PR)
assistant_idvalue and keyAssistant(id)(currently anyassistant_iddegrades to unkeyed; parallel assistant-delegations then rely on the FIFO pool).executeas defense-in-depth (the backend predicate intentionally does not —ToolCallUpdatemay lack a title).resume_delegation/get_delegation_status/cancel_delegationcalls insideexecutestill render as plain execute cards; their results still bind via the replay gate's task-id fallback.