refactor: rewrite tool arg translation to handle SSE chunk boundaries#8
Merged
dotCipher merged 2 commits intodotCipher:mainfrom Apr 16, 2026
Conversation
Owner
|
@BNasraoui I'll take a look at this when you complete the |
This PR was previously based on pre-v1.10.2 main. Upstream has since landed PRs dotCipher#9 (OAuth), dotCipher#10 (Windows compat), dotCipher#11 (shared tool mappings), and dotCipher#12 (CI). Rebased onto current main and reworked our improvements to layer cleanly on top of upstream's tool-mapping work. ## What changed from upstream v1.10.2 Upstream dotCipher#11 uses regex-on-raw-bytes to translate tool arguments as they stream in. That works when the whole JSON fits in one chunk, but fails silently when Anthropic splits a tool argument across a TCP boundary — e.g. "file_" in one chunk and "path" in the next. The regex never matches a split key, so the consumer receives untranslated snake_case and produces tool validation errors. This PR replaces that approach with three structural changes: 1. **SSE event framing (src/stream.ts).** New module with a stateful processor that parses SSE frames on \n\n boundaries, buffers input_json_delta fragments per content-block index, and emits a single translated input_json_delta at content_block_stop. Chunk-boundary corruption becomes structurally impossible. Uses start() + async loop instead of pull() — Bun's ReadableStream doesn't reliably re-invoke pull() when the handler resolves without enqueuing (which happens while we're buffering deltas). 2. **JSON.parse for tool arg translation (src/claude-tools.ts).** translateToolArgsJsonString parses the assembled JSON, walks the object, and serializes back. Key renames on parsed objects can't corrupt string values that happen to contain key names (e.g. a TodoWrite item whose content literally says "activeForm", or a Bash command with "file_path=" inside a heredoc). Replaces every regex substitution from dotCipher#11. 3. **Consolidated outbound translation (translateArgsOpencodeToClaude).** The inbound path is now a single function; the outbound path in index.ts body transform was a wall of nested if-blocks. Extracted to a sibling function so inbound/outbound stay in lockstep — next bug fix touches one place. ## What was carried forward from dotCipher#11 All of upstream's tool-mapping fixes are preserved: - Agent: subagent_type required; general-purpose ↔ general; strip model/run_in_background/isolation inbound; strip task_id/command outbound; default subagent_type=general inbound - AskUserQuestion: bidirectional multiSelect ↔ multiple per question - Skill: skill ↔ name; strip args inbound - WebFetch: strip prompt inbound, inject default format=markdown; synthesize prompt from format outbound, strip timeout - Bash: strip run_in_background/dangerouslyDisableSandbox inbound - Read: strip pages inbound - Grep: strip output_mode/-B/-A/-C/context/-n/-i/type/head_limit/ offset/multiline inbound - TodoWrite: bidirectional activeForm ↔ priority; cancelled → completed outbound ## Other changes - context_management and output_config.effort only injected for thinking-capable models (opus, sonnet-4-6). Previously sent to haiku too, which 400s. - Silent try/catch {} blocks now log via debugLog() gated by OPENCODE_CLAUDE_BRIDGE_DEBUG=1. Diagnosable without source changes. - flush() logs a debug warning if a tool_use block is abandoned mid-stream (upstream disconnect between start and stop). ## Tests 81 unit tests (up from 11). Test file imports actual production modules (createSseProcessor from ./stream, translateToolArgsJsonString and translateArgsOpencodeToClaude from ./claude-tools) — not local reimplementations. Assertions parse SSE events and use assert.deepEqual on object shape rather than substring matches. Covers: - Every INBOUND_TOOL_NAME_MAP entry (name mapping) - Every tool's argument translation (including the corruption cases) - Chunk boundary splits (fragmented args, events across chunks) - Interleaved tool_use blocks (per-block state isolation) - Error paths (malformed JSON, translator throws, abandoned blocks) - Byte-exact pass-through for ping/message_start/text_delta - Round-trip symmetry between inbound/outbound translation - All inbound field stripping (Agent/Bash/Read/Grep/Skill) - All outbound field handling (Agent/AskUserQuestion/WebFetch/TodoWrite) ## Verified end-to-end via opencode run Read, Edit, Write, Grep, Glob, Bash, Agent, WebFetch, Skill — all working. WebSearch remains a stub (not in OpenCode's AVAILABLE_TOOLS).
f1e56e7 to
dafe299
Compare
Strip the OPENCODE_CLAUDE_BRIDGE_DEBUG debug logger and revert the catch sites it wired into back to silent catches. The SSE processor's optional debug hook in stream.ts stays in place (used by tests) but is no longer wired from index.ts.
Owner
|
Doing the manual test of the "Verify AskUserQuestion → question mapping works end-to-end" now, I'll look to resolve it once I get a test running if it's not resolved yet, then I'll merge this @BNasraoui |
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
Fixes #7 — three tool mapping bugs:
subagent_typenow required in schema. OpenCode'stasktool validates it as required; omitting it causedinvalid_typeerrors.general-purpose→general(wasbuild). OpenCode has a dedicatedgeneralsubagent per official docs. Also addsgeneral→general-purposeto outbound map.AskUserQuestion↔questionbidirectional name mapping added. Previously stub-only, so the model could never ask users clarifying questions.Changes
src/claude-tools.ts:217:required: ["description", "prompt", "subagent_type"]src/index.ts:592-597: Addgeneral: "general-purpose"to outbound agent mapsrc/index.ts:749:"general-purpose": "general"in inbound agent mapsrc/index.ts:91-92:question/mcp_question→AskUserQuestionoutboundsrc/index.ts:106:AskUserQuestion→questioninboundTest plan
npm run build— cleannpm test— 12/12 passsubagent_type: "explore"resolves correctly viaopencode runsubagent_type: "plan"resolves correctly viaopencode runsubagent_type: "build"resolves correctly viaopencode runAskUserQuestion→questionmapping works end-to-end