fix(opencode): recover silent and post-tool no-output completions - #1342
fix(opencode): recover silent and post-tool no-output completions#1342masterkunm wants to merge 5 commits into
Conversation
Maintainer direction — WELCOME / FORMAL REVIEWTriaged at exact HEAD The direction is welcome: recovering persisted text for a Maintainer five-question gate:
The new Formal review will focus on read-only session/message correlation, missing-path/schema degradation, deny-all tool enforcement, exactly-once/non-recursive finalization, same-session ordering, abort/timeout cleanup, replacement semantics, fallback privacy, and preservation of original errors and silent-completion diagnostics. This is a review route, not an approval or merge authorization. GitHub currently reports no checks for this HEAD; exact-HEAD tests/checks and a formal review verdict are still required. [小太阳·砚砚/GPT-5.6 Sol🐾] |
zts212653
left a comment
There was a problem hiding this comment.
Changes requested — exact HEAD 4e9163fa25363958a8d9efe4fb84c33a304ffc61
The direction remains WELCOME, but this HEAD is not merge-ready. The happy-path tests pass; independent red probes reproduce three safety/behavior gaps, and the repository check is red.
Maintainer five-question gate
- Useful to clowder-ai? Yes. It addresses accepted bug #1341 without replaying the original tool-bearing turn.
- What changed? A read-only OpenCode SQLite recovery path, one same-session no-tool finalizer, deterministic fallback/diagnostics, regression tests, and a public-target incident note (
3 files, +649/-2). - Merge this HEAD? No — changes required. The findings below break the stated fail-closed, privacy, concurrency, and path-compatibility proof obligations.
- Intake to cat-cafe? Conditional after merge. Code/tests are safe-cherry-pick candidates with no #1224-class migration; the incident document remains manual-port/publication-gated.
- Cleaner architecture? Yes. Keep the two recovery slices, but extract a small recovery boundary with: the runtime's canonical DB-path resolver, a central safe tool-output projector, a session-scoped single-flight guard, and a fail-closed finalizer state machine. The provider file is now 1,106 lines, so this is also the right seam to avoid another fallback cluster.
Blocking findings
-
P1 — the finalizer does not fail closed on a tool attempt, and the deny-all config is not proven authoritative.
- In
runPostToolFinalizer, atool_useonly recordstool_use_blockedand continues; any later text is still emitted as the answer (OpenCodeAgentService.ts:838-865). A reviewer regression probe emittedtool_use -> text; the provider returnedaccepted-after-toolinstead of rejecting that finalizer run. - The patch injects
OPENCODE_CONFIG_CONTENT(:942-954), but upstream OpenCode merges that source before organization/managed config, and managed preferences explicitly override everything (upstream config merge order). Please make deny-all structurally non-overridable, and add an integration-shaped test proving an attempted tool cannot execute and poisons the whole finalizer result.
- In
-
P1 — deterministic fallback exposes raw tool output.
buildOpenCodePostToolFallbackTextstringifies the trace output and places up to 4 KiB directly in user-visible text (:252-262,:878-895). A reviewer probe supplied a synthetic token and private absolute path; both appeared verbatim in the yielded text.- Route fallback content through a safe projection/redaction boundary. Do not expose raw tool payloads, credentials, or absolute paths.
-
P1 — there is no same-session concurrency fence.
- Finalization resumes the same session, but
invoke/runPostToolFinalizerhas no session-scoped lease or single-flight guard. A reviewer probe started two invokes for the same session and observedmaxActive=2. - Serialize or reject overlapping turns for that session, including the transition from the original stream into finalization, and cover release on success, abort, timeout, and spawn failure.
- Finalization resumes the same session, but
-
P1 — SQLite path/correlation support is narrower than the advertised OpenCode contract, and the safety logging regresses F212.
- The default is hard-coded to
~/.local/share/opencode/opencode.db(:248-250). Current OpenCode also honorsOPENCODE_DB, platform/XDG data roots, and channel-specific DB names (upstream DB resolver). The single new SQLite test covers only one happy-path temp DB; it does not prove missing DB, schema drift, malformed/multiple parts, or path variants. - Logs emit full
sessionId/messageId(:645-659,:923-926). F212 requires bounded identifiers and no sensitive paths. Preservesilent_completionwhen recovery fails, but log only the allowed prefix/source category and sanitize errors that may contain paths.
- The default is hard-coded to
-
P2 — the exact repository check is red and the new configuration surface is not registered.
pnpm biome checkfails on import ordering and two formatter deltas inOpenCodeAgentService.ts; thereforepnpm checkcannot pass this HEAD.OPENCODE_DB_PATHis accessed indirectly and is absent from the environment registry, socheck:env-registrycannot discover it. Register/document it if retained, or use OpenCode's canonical path contract.- Add YAML frontmatter to the new
docs/Markdown file, and extract the recovery code so the provider does not grow further past the repository's hard file-size boundary.
Proof required on the next HEAD
- Exact read-only session/message correlation across missing DB, schema drift, malformed and multiple parts, and supported path variants.
- Non-overridable deny-all behavior; a tool attempt must poison the finalizer result before any text can be accepted.
- Exactly-once, non-recursive, abort/timeout-safe finalization; valid pure tool-only completion must remain non-user-visible.
- Same-session concurrency exclusion across the original/finalizer boundary.
textMode: replacewithout duplicate prelude, plus sanitized fallback with no raw tool-output leakage.- Original provider errors and F212
silent_completiondiagnostics remain authoritative.
Exact-HEAD validation
pnpm --dir packages/api run build— PASS- Full OpenCode provider suite — PASS, 41/41
- Targeted SQLite recovery — PASS, 1/1
- Targeted post-tool finalizer — PASS, 1/1
pnpm lint— PASS (pre-existing warnings only)pnpm biome check <three changed files>— FAIL, 2 diagnostics- Independent temporary red probes — FAIL as expected, 0/3 guards passed (raw fallback leak, post-tool text accepted, concurrent same-session invokes); the probe file was removed and the review worktree is clean.
- GitHub checks — none reported for this HEAD
masterkunm remains implementation owner. I did not change the contributor branch or merge this PR.
[小太阳·砚砚/GPT-5.6 Sol🐾]
zts212653
left a comment
There was a problem hiding this comment.
Changes requested — exact HEAD c1d868d7ae095c502686614e428d62debe728e0e
This HEAD is a merge of current main into the contributor branch. The three PR-owned files are byte-for-byte unchanged from the previously reviewed HEAD 4e9163fa25363958a8d9efe4fb84c33a304ffc61; no requested fix was added. I therefore re-ran the exact-HEAD checks and the prior blocking findings remain reproducible.
Maintainer five-question gate
- Useful to clowder-ai? Yes. The accepted #1341 recovery problem remains real and worth solving without replaying the original tool-bearing turn.
- What changed? The PR still changes the same three files (
+649/-2): OpenCode SQLite recovery/finalization, provider tests, and a public-target incident note. This new commit only incorporates base-branch changes. - Merge this HEAD? No — changes required. Fail-closed enforcement, fallback privacy, session concurrency, path compatibility, F212 logging, and repository-format requirements are still unmet.
- Intake to cat-cafe? Still conditional after merge. Risk classification remains two safe-cherry-pick code/test files plus one manual-port document, with no #1224-class migration.
- Cleaner architecture? Still yes. Use the runtime's canonical DB-path contract, a central safe tool-output projector, a session-scoped single-flight guard, and a fail-closed finalizer state machine; extract this boundary rather than extending the now 1,106-line provider and its 12-layer fallback cluster.
Blocking findings still present
- P1 — finalizer tool attempts do not poison later text, and deny-all is not structurally authoritative. A fresh probe again emitted
tool_use -> text;accepted-after-toolwas returned.OPENCODE_CONFIG_CONTENTremains mergeable before later managed configuration. - P1 — deterministic fallback exposes raw tool output. A fresh probe again surfaced a synthetic
sk-*token and private absolute path verbatim. - P1 — same-session invocation is not single-flight. Two concurrent calls using the same session again reached the spawn boundary simultaneously (
maxActive=2). - P1 — SQLite path/correlation and F212 privacy proof remain incomplete. The resolver is still hard-coded to the Linux stable path unless the unregistered
OPENCODE_DB_PATHoverride is supplied; there is still no missing/schema-drift/malformed/multiple-part/path-variant matrix. Fresh provider-test logs again contain full session/message IDs. - P2 — the repository check is still red.
pnpm checkstops on the same Biome import-order and formatting errors. The new Markdown still lacks YAML frontmatter, the environment surface remains absent from the registry, and the provider remains above the repository file-size boundary.
Fresh exact-HEAD evidence
- API build — PASS
- Full OpenCode provider suite — PASS, 41/41
- SQLite recovery targeted test — PASS, 1/1
- Post-tool finalizer targeted test — PASS, 1/1
pnpm lint— PASS (pre-existing warnings)pnpm check/ changed-file Biome check — FAIL, same two diagnostics- Independent temporary safety probes — FAIL as expected, 0/3 guards passed; the probe was deleted and the worktree is clean
git diff --check— PASS- GitHub checks — none reported
Please address the full proof matrix in the previous exact-HEAD review. masterkunm remains implementation owner; I did not modify the contributor branch or merge the PR.
[小太阳·砚砚/GPT-5.6 Sol🐾]
Contributor update for maintainer review - HEAD
|
zts212653
left a comment
There was a problem hiding this comment.
Changes requested — exact HEAD 55e467b6fd33e76e43c296e3c981790499d1ce01
I reviewed this as an external inbound contribution. GitHub identifies masterkunm as the author (FIRST_TIME_CONTRIBUTOR), so fix custody remains with the contributor. I did not modify the branch or merge it.
Maintainer direction gate
- Useful to our project? Yes. Accepted issue #1341 is open and labeled
bug+triaged; recovering a persisted answer is safer than rerunning a turn whose tools may already have had side effects. - What does it change? Six files, +1267/-9: a read-only SQLite recovery module and path resolver, a same-session finalizer/single-flight path in
OpenCodeAgentService, one environment-registry entry, two test files, and an incident document. No new dependency. - Merge into clowder-ai now? No. The direction is useful, but the current exact HEAD has four blocking contract/safety defects below, including a public-suite hang.
- Intake into cat-cafe? Conditional after an upstream merge, not now. The classifier marks the service/recovery code and tests as safe-cherry-pick candidates, the environment registry as high-risk/manual-port, and the incident document as manual-port/publication-gated. There is no #1224-class migration.
- Better architectural slice? Keep SQLite recovery as a read-only boundary, but make completion an explicit event-state contract; resolve paths exactly as upstream does; project fallback output from an allowlisted structured shape; and enforce finalizer capability with an upstream-supported non-overridable boundary rather than config precedence assumptions.
Blocking findings
-
P1 — the finalizer now fires for valid pure tool-only completion and hangs an existing public test.
OpenCodeAgentService.ts:591-629triggers wheneverlastToolEventIndex > lastTextEventIndex, includingtextEventCount === 0;terminalStepFinishAfterLastToolis logged but does not constrain the transition.opencode-agent-service.test.js:1036-1091changes AC-G3 to require a second spawn in this case. That contradicts this PR's own documented rule (textEventCount > 0, lines 56-62), non-goal preserving pure tool-only completion (lines 64-69), and completion contract (lines 80-85).This is observable outside the new test: the public gate reaches
test/opencode-omoc-context.test.js, sees the final Ralph-loop tool event, starts a finalizer that the existing mock never provided, and hangs. A bounded exact-HEAD run exits 1 withRalph Loop: 3 consecutive step cycles ... test timed out after 3000ms. Restore the pure tool-only boundary and add regression coverage in both the focused provider suite and the existing OMOC suite. -
P1 — the claimed deny-all finalizer is not authoritative under OpenCode config precedence or agent lookup.
OpenCodeAgentService.ts:860-880supplies--pure --agent ...plusOPENCODE_CONFIG_CONTENT. In upstream OpenCode,--pureonly setsOPENCODE_PURE/ disables external plugins; config loading continues afterOPENCODE_CONFIG_CONTENT, including org/managed config and macOS managed preferences that override earlier configuration. Upstreamrunagent resolution also falls back to the default agent when the requested agent is absent or a subagent. Observing a latertool_useis too late: the side effect may already have happened.References: config precedence,
--purebehavior, and agent fallback. Prove the effective resolved capability at the process boundary and fail closed before execution if it cannot be guaranteed. -
P1 — fallback privacy is a top-level path denylist, not the claimed absolute-path boundary.
opencode-recovery.ts:78-84redacts only enumerated roots. On the built exact HEAD,projectSafeOpenCodeToolOutput("/secrets/acme/private.pem /custom/private/file.txt")returns both paths verbatim. Those values can enter the finalizer prompt and deterministic user-visible fallback, contradicting lines 100-101 of the incident document. Replace the growing root regex with a fail-closed structured/allowlisted projection and test arbitrary POSIX roots (plus the existing Windows cases). -
P1 — relative
OPENCODE_DBis resolved differently from upstream.opencode-recovery.ts:201-212addsOPENCODE_DBverbatim. WithOPENCODE_DB=custom.db, the first candidate iscustom.db, relative to the clowder process cwd. Upstream resolves a relative value underGlobal.Path.data; see OpenCode database resolution. The current tests cover only an absolute override, so a supported path variant silently misses persisted text. Mirror upstream semantics and add relative-path coverage.
Evidence at this HEAD
pnpm --dir packages/api run build: pass.- Focused provider/recovery suites: 49/49 pass.
pnpm check: pass;pnpm lint: pass with existing web warnings;git diff --check: pass.CAT_CAFE_GATE_TEST_MODE=public bash scripts/pre-merge-check.sh --no-rebase --skip-install: build and typecheck pass, then public tests hang inopencode-omoc-context; the reviewer interrupted the stuck gate. The bounded single-suite reproduction exits 1 with one cancelled timeout. A separately observed F248 worktree-id test passed on isolated rerun.- GitHub reports no checks for this PR; repository state is mergeable but blocked/review-required.
The read-only exact session/message SQLite correlation, missing/schema/malformed/multipart degradation, same-session single-flight, buffered replace path, and preservation of original F212 diagnostics are useful improvements. They do not offset the four blocking failures above. Please update the branch and I will re-review the new exact HEAD.
[小太阳·砚砚/GPT-5.6 Sol🐾]
zts212653
left a comment
There was a problem hiding this comment.
Exact-HEAD maintainer re-review of 1bd5ebdec215b138dfe4a99632027e6245dea885.
CHANGES_REQUESTED
The merge from main does not change any of this PR's six files relative to reviewed head 55e467b6fd33e76e43c296e3c981790499d1ce01. I revalidated the blockers on the new exact head; all four remain:
-
P1 — pure tool-only/OMOC completion is still routed into a finalizer. After a tool event,
invoke()unconditionally starts the no-tool finalizer whenever no later text exists.terminalStepFinishAfterLastToolis recorded but does not gate that branch. That conflicts with accepted issue #1341's requirement to preserve valid pure tool-only completion. The existing publicopencode-omoc-isolation.test.jsreachesOpenCode CLI stopped after tool_use without final text - running no-tool finalizerand then hangs because the test fixture never supplied a second CLI process. The PR-local AC-G3 test was changed to expect the second finalizer instead of preserving the accepted behavior. -
P1 — the finalizer's deny-all policy is not authoritative.
OPENCODE_CONFIG_CONTENTis merged before enterprise managed config, so later managed config can override the inline global or named-agent permissions. OpenCode documents--pureonly as disabling external plugins; it is not a config-isolation switch. Please make tool denial fail closed against those later override layers, and cover the precedence case. Relevant upstream docs: config merge order,--pure. -
P1 — fallback projection still leaks arbitrary absolute paths. The root-name denylist redacts
/Users/...,/tmp/..., etc., but paths under unlisted roots remain verbatim. Exact-head probe output wastoken=[TOKEN_REDACTED] path=/secrets/private/token.txt also /custom/acme/key.pem. This fallback must be safe for arbitrary machine-local absolute paths, not only known root names. -
P1 — relative
OPENCODE_DBresolution still differs from OpenCode.resolveOpenCodeDbCandidates()adds the env value as-is, so a relative value resolves against Clowder's process cwd. OpenCode resolves relative values under its data root (join(Global.Path.data, Flag.OPENCODE_DB)); see upstream database path resolution. Please preserve that parity and add a relative-path test.
Exact-head verification:
pnpm --dir packages/api run build— pass- focused OpenCode provider tests — 44/44 pass
- OpenCode recovery tests — 5/5 pass
pnpm check— passpnpm lint— pass with pre-existing warnings- privacy probe — fail (absolute-path disclosure above)
- focused public OMOC file — hangs after entering the unintended finalizer
- full
pnpm --filter @cat-cafe/api run test:public— fail/hang inopencode-omoc-context.test.js; after loggingterminalStepFinishAfterLastTool:trueit enters the no-tool finalizer and makes no further progress, so the hung gate was interrupted - GitHub checks — none reported for this head
The contributor retains fix custody; I did not modify or merge the PR.
— 小太阳·砚砚 / gpt-5.6-sol
Summary
Fixes #1341.
This PR handles the approved A/B no-output incident write-up for OpenCode + DeepSeek:
step_start-only silent completions from OpenCode SQLite bysessionID + messageIDcat-cafe-no-tool-finalizeragentWhy
The previous adapter logic treated
textEventCount > 0ortoolUseEmittedas enough to avoid silent recovery. That missed the actual failure mode: a turn can emit prelude text, run a tool, and then end without final assistant text. A separate silent shape also showed stdout with onlystep_start, while OpenCode had already persisted the answer in SQLite.We should not rerun the whole OpenCode turn because that can duplicate tool side effects. The recovery paths are deliberately constrained to read-only SQLite recovery or a no-tool finalizer.
Tests
pnpm --dir packages/api run buildnode --import ./packages/api/test/helpers/setup-cat-registry.js --test --test-name-pattern "step_start-only NDJSON recovers" packages/api/test/opencode-agent-service.test.jsnode --import ./packages/api/test/helpers/setup-cat-registry.js --test --test-name-pattern "post-tool completion gap" packages/api/test/opencode-agent-service.test.jsnode --import ./packages/api/test/helpers/setup-cat-registry.js --test packages/api/test/opencode-agent-service.test.js- 41/41 passed