-
Notifications
You must be signed in to change notification settings - Fork 134
feat: harness reliability — run termination protocol, context-safety margins, compaction fidelity #1171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
anandgupta42
wants to merge
31
commits into
main
Choose a base branch
from
feat/harness-reliability
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
feat: harness reliability — run termination protocol, context-safety margins, compaction fidelity #1171
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
8380ceb
fix(session): head-truncation fallback for unrecoverable context over…
anandgupta42 361f7c9
feat(harness): proactive overflow estimation + agent finish protocol
anandgupta42 3a5a09b
fix(session): turn-boundary truncation + accurate tail token estimation
anandgupta42 b6f478b
feat(harness): Wave 1 structural fixes — summarizer integrity, trunca…
anandgupta42 0bfa719
feat(harness): Wave 2 core-loop fixes — termination path, task pinnin…
anandgupta42 d48d674
feat(harness): Wave 2 config schema — starvation/idle-done/pin knobs
anandgupta42 20c1661
feat(harness): Wave 3 reliability — context estimator safety margin, …
anandgupta42 3e1e988
test(harness): pin compaction.test.ts isOverflow suite to safety frac…
anandgupta42 b510f46
fix(harness): review-driven hardening — termination false-positives, …
anandgupta42 77abbf0
chore: neutralize internal fixture identifiers and planning shorthand…
anandgupta42 51feb09
fix(harness): track code-fence marker char+length in completion detec…
anandgupta42 3d01d49
fix(harness): compaction breaker returns stop and clears attempts; pr…
anandgupta42 e7b3a48
fix(harness): terminal finish outcomes take precedence over compaction
anandgupta42 5780502
fix(harness): head-truncation fallback cuts only at user boundaries, …
anandgupta42 706084f
fix(harness): run exit-code stickiness — spurious beforeExit can no l…
anandgupta42 f3d35e6
fix(harness): estimator safety fraction applies only to estimated tok…
anandgupta42 ca1f42d
fix(harness): clamp retained tail+ledger below the overflow trigger o…
anandgupta42 cdfb9da
fix(harness): carry the verbatim-tail turn count through the v2 confi…
anandgupta42 6be3d0d
fix(harness): mutation credit on success only, doom-loop stop latched…
anandgupta42 aef20e7
fix(harness): prototype-safe tool-call id tables (Map) + per-processo…
anandgupta42 f333429
fix(harness): scope the idle-done challenge attribution and abort for…
anandgupta42 78ead32
fix(harness): session-state eviction is LRU (refresh on access), not …
anandgupta42 0134e90
fix(harness): batch of small hardening fixes — conservative cap defau…
anandgupta42 8183789
chore: neutralize remaining planning shorthand in comments and test n…
anandgupta42 11b5224
chore: keep the relocated compaction check inside change markers
anandgupta42 c49df38
fix(harness): address external review feedback — small correctness an…
anandgupta42 e9bde73
fix(harness): second-wave review fixes — completion detector and run …
anandgupta42 c9f78ec
fix(harness): extend change markers over the braced tool-input-start …
anandgupta42 2a8850c
fix(harness): close the unpaired change marker in run accounting
anandgupta42 8f765a0
fix(harness): address multi-model review consensus — summarizer starv…
anandgupta42 3137696
fix(harness): fourth-pass review fixes — fence conformance, config bo…
anandgupta42 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # Harness reliability review — deferred follow-ups | ||
|
|
||
| Deferred MED findings from the pre-PR release review (pre-PR adversarial review). | ||
| All 5 HIGH findings plus selected MED/LOW items were fixed on this branch; the | ||
| items below were explicitly deferred and are listed verbatim from the review. | ||
|
|
||
| [MED] packages/opencode/src/tool/truncation.ts:66 — the plain-async truncation path hardcodes 2,000 lines/50KiB while the Effect wrapper honors `tool_output` configuration — MCP output through `prompt.ts` therefore ignores user caps despite the shared-core claim — consolidate the wrappers or pass the resolved configuration through both, with parity tests. | ||
|
|
||
| [MED] packages/opencode/src/session/compaction.ts:663 (renderCarryAnchors) — carry-anchor trimming stops when one item remains — one oversized model-generated "Accomplished" item defeats `maxTokens` and can undo compaction — permit dropping or truncating the final item and assert the rendered result satisfies the cap. | ||
|
|
||
| [MED] packages/opencode/src/cli/cmd/idle-done.ts:157 — every command not recognized as read-only is treated as verification — an exit-zero install, cleanup, deployment, or arbitrary unknown command can satisfy the "green verify" precondition and trigger a false completion challenge — require configured or positively classified verification evidence; unknown commands should be ineligible. | ||
|
|
||
| [MED] packages/opencode/src/session/compaction.ts:70 — observation masks retain the first 80 characters of pruned output, while the ledger retains raw command/path/pattern text — credentials, authorization headers, query data, and signed URLs can survive pruning and be recopied into later synthetic prompts — retain only allowlisted metadata or hashes and apply shared secret redaction. | ||
|
|
||
| [MED] packages/opencode/src/session/compaction.ts:572 (renderLedger) — ledger capping repeatedly joins and re-estimates the whole array while removing one line at a time, after collecting the full session history — this is quadratic in unique writes and adds latency at the critical compaction path — bound collection early and trim using accumulated token costs or a single cutoff search. | ||
|
|
||
| The following items from a later review pass were also considered and deliberately | ||
| deferred (no behavior change on this branch): | ||
|
|
||
| [LOW] packages/opencode/src/cli/cmd/run/run-mode.ts — the run entrypoint writes its process-scoped mode marker into the environment for the process lifetime, and child processes inherit it; a nested interactive server launched from such a session would arm run-mode mechanisms for genuinely interactive clients — clear or scope the marker in the interactive entrypoints (mirroring the existing child-env cleanup for the sibling non-interactive marker). | ||
|
|
||
| [LOW] packages/opencode/src/session/compaction.ts — module-level per-session pin state and per-session tracker read maps grow without bound in a long-lived server process (the pin map has no production eviction path; per-session read tracking keeps one entry per unique path for the session's lifetime) — bound both with the same LRU pattern now used by the session-state stores. | ||
|
|
||
| [LOW] packages/opencode/src/session/prompt.ts — the post-compaction pinned-task reminder re-derives its source by streaming the FULL session history from the database on every generation once a session has compacted — cache the resolved pin source per session or query only the needed boundary messages. | ||
|
|
||
| [LOW] packages/opencode/src/session/termination.ts — the post-compaction three-option completion nudge (including the completion-token instruction) is injected in ALL modes; interactive users can see an occasional bare completion token line with no interactive function — mode-gate the nudge text or document the cosmetic change. | ||
|
|
||
| The following items came from an external multi-reviewer pass over this branch and | ||
| were deliberately deferred (no behavior change on this branch): | ||
|
|
||
| [MED] packages/opencode/src/cli/cmd/run.ts — a prompt retry after a client timeout or connection reset can hit the server AFTER it accepted the original POST, sending the same task again and creating a second user message / duplicate execution — the retry loop has no way to tell "not yet accepted" from "accepted, response lost." Needs a stable idempotency/message key the server honors, or a way to confirm the first attempt was never accepted before retrying. | ||
|
|
||
| [MED] packages/opencode/src/session/compaction.ts (fitHead) — the summarization-request budget reserves a fixed 2,000 tokens for the summary prompt, but the actual assembled prompt (default template + carry anchors + pin-summary addition + first-person reframe, or a plugin-supplied override) can exceed that on an active session — size the reservation from the actual assembled prompt instead of a constant. | ||
|
|
||
| [MED] packages/opencode/src/session/compaction.ts (buildLedger) — on a session's second or later auto-compaction, the ledger is built from the already-filtered/compacted message view, not the full session stream, so verified-write facts from before the first compaction silently drop out of later ledgers — build from the full stream and let selection filter afterward. | ||
|
|
||
| [LOW] packages/opencode/src/session/prompt.ts (uncountedTail) — the proactive overflow estimate sums tool-result tokens on messages AFTER the last-finished assistant message, but a tool call and its result can live on that SAME message when the turn is still mid-flight — those results are excluded from the estimate, so compaction can fire a step later than it should on a heavy tool-output turn. | ||
|
|
||
| [LOW] packages/opencode/src/tool/truncate-core.ts (preview, middle direction) — a degenerate `maxBytes: 1` config (no realistic caller sets this) can still allocate one byte to each of the head/tail halves and exceed the byte budget by a small margin; the equivalent `maxLines: 1` case was already fixed by degrading to tail-only — extend the same degrade to the byte-only case. | ||
|
|
||
| [LOW] packages/opencode/src/session/{termination,nudge,tool-result-cap}.ts, packages/opencode/src/cli/cmd/{run-accounting,idle-done}.ts — these 5 new modules use `export namespace` for organization, which the repo's module-shape convention (`packages/opencode/AGENTS.md`) asks new code to avoid in favor of flat exports + a bottom-of-file self-reexport. ~69 pre-existing files in the package already use the same pattern, so this is consistent with existing debt rather than a regression; fold into a holistic namespace-to-flat-exports cleanup across the package rather than converting these 5 files in isolation. | ||
|
|
||
| [LOW] packages/opencode/test/session/starvation.test.ts — the run-mode "armed" gate test re-implements the gate expression as a local helper instead of importing the real predicate from processor.ts, so a future change to the actual gate (added condition, reordered precedence, renamed exemption) would not be caught by this test — extract the gate into a shared, directly-testable predicate. | ||
|
|
||
| The following items came from a further multi-reviewer pass over this branch and | ||
| were deliberately deferred (no behavior change on this branch): | ||
|
|
||
| [MED] packages/opencode/src/session/llm.ts (addHistoricalToolStubs) — the stub-injection skip was narrowed to `toolChoice === "none" && no tools`, which closes the normal-turn regression, but the compaction summarizer still takes that path with a head that references historical tool calls. The skip rests on the claim that omitting both `tools` and `tool_choice` is universally accepted; the issue the stubs exist to fix concerned validation of referenced historical tool calls, which is orthogonal. Verify against the provider that originally needed the stubs before changing anything — reintroducing stubs here would advertise callable tools on a text-only call, so this is a provider-compatibility question, not a code cleanup. | ||
|
|
||
| [MED] packages/opencode/src/session/starvation.ts (repeatSignature / consecutive-signature counter) — the repeat signature folds the failure message in but the counter accumulates for successful calls too, so three identical SUCCESSFUL calls (re-running the same verification command, polling a status probe) reach the threshold and produce a directive asserting that repeating the call cannot change the result, which is untrue for a polling or verification call. Restricting accumulation to calls that carry a failure message narrows the detector's semantics and its overlap with the identical-args doom-loop ladder; make that change with validation data rather than in review, and note the directive text is prompt-visible. | ||
|
|
||
| [LOW] packages/opencode/src/cli/cmd/run-accounting.ts (termination) — `why_model_stopped` falls back to "stop" when no step-finish reason was ever recorded (an aborted run that never completed a step), so the run record attributes an ordinary stop to a session that never reported one. Distinguishing it needs a new value in the published record's enum, which is an output-contract change for downstream consumers — batch it with the next deliberate revision of the run record schema. | ||
|
|
||
| [LOW] packages/opencode/src/session/prompt.ts (post-compaction pin reminder) and packages/opencode/test/cli/idle-done.test.ts — the idle-done unit tests now pin production event ordering (step-finish part before the step's snapshot patch part) in two dedicated cases, but the shared fixtures still emit the patch first; converge the remaining fixtures on production ordering when the file is next touched. | ||
|
|
||
| The following items came from the fourth review pass over this branch and were | ||
| deliberately deferred (no behavior change on this branch): | ||
|
|
||
| [MED] packages/opencode/src/session/processor.ts (dispatch tool-result cap) — the per-result hard cap is applied on the successful `tool-result` branch only. A `tool-error` still persists an unbounded error string, and an interrupted running tool keeps partial output in metadata that `message-v2.ts` later replays as a tool result, so a failed call with very large stderr can still overflow the next request. Applying the cap to error text and interrupted partial output changes what gets PERSISTED on the failure path, which is where diagnostics come from — size it against real failure payloads before truncating them. | ||
|
|
||
| [MED] packages/opencode/src/cli/cmd/idle-done.ts (verify classification) — with no configured verify command, ANY non-read-only bash command is treated as a verification, so a mutating command that is not on the mutating-heads list (a build script that also writes generated sources, say) can register as the green verify it is not. The mutation watermark now advances for the in-place, redirection, and known-mutating-head forms, but the underlying "not read-only implies verification" inference still needs replacing with an explicit verify classifier. | ||
|
|
||
| [LOW] packages/opencode/src/cli/cmd/run.ts (--max-turns) — `--max-turns 0` disables the limit entirely because the guard is a truthiness check, and negative or fractional values are accepted without validation. For a governance control an explicit zero should not mean unlimited; fix it together with the CLI validation pass that gives the other numeric options explicit range errors, so the behavior change is documented in one place. | ||
|
|
||
| [LOW] packages/opencode/src/tool/bash.ts (child env) — `ALTIMATE_RUN_RESUMED` joins `ALTIMATE_RUN_MODE` as a marker that nested processes inherit from the bash tool's merged env. Its leak direction is benign (a nested run would use interactive pin selection), but it belongs in the same holistic decision about which run-scoped markers get stripped for child processes. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,47 @@ | ||
| export * as ConfigCompaction from "./compaction" | ||
|
|
||
| import { Schema } from "effect" | ||
| import { NonNegativeInt } from "../schema" | ||
| import { NonNegativeInt, SAFETY_FRACTION_MIN } from "../schema" | ||
|
|
||
| export class Keep extends Schema.Class<Keep>("ConfigV2.Compaction.Keep")({ | ||
| tokens: NonNegativeInt.pipe(Schema.optional), | ||
| // altimate_change start — V2 parity for the fork's verbatim-tail turn count | ||
| // (V1 compaction.tail_turns; 0 disables the tail entirely). | ||
| turns: NonNegativeInt.pipe(Schema.optional), | ||
| // altimate_change end | ||
| }) {} | ||
|
|
||
| export class Info extends Schema.Class<Info>("ConfigV2.Compaction")({ | ||
| auto: Schema.Boolean.pipe(Schema.optional), | ||
| prune: Schema.Boolean.pipe(Schema.optional), | ||
| keep: Keep.pipe(Schema.optional), | ||
| buffer: NonNegativeInt.pipe(Schema.optional), | ||
| // altimate_change start — V2 parity for the fork compaction keys (estimator | ||
| // safety margin, state ledger/summary carry, task pin). Same names as V1 so | ||
| // ConfigMigrateV1 can carry them through without renames. | ||
| // upstream_fix: Config.load decodes a document authored directly in V2 (not | ||
| // migrated from V1) through THIS schema, so the V1 bounds on these two | ||
| // fractions don't apply here — a direct V2 document could carry an | ||
| // out-of-range value straight through to | ||
| // SessionCompaction.contextSafetyFraction / pinBudget. Bound identically to | ||
| // the V1 schema (packages/core/src/v1/config/config.ts). | ||
| // SAFETY_FRACTION_MIN: Schema.toArbitrary's fast-check generator requires | ||
| // `.check()` bounds to be exact 32-bit floats, and the bound must round DOWN | ||
| // so the documented minimum `0.1` still decodes; see the matching V1 comment. | ||
| context_safety_fraction: Schema.Number.check( | ||
| Schema.isGreaterThanOrEqualTo(SAFETY_FRACTION_MIN), | ||
| Schema.isLessThanOrEqualTo(1), | ||
| ).pipe(Schema.optional), | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| state_ledger: Schema.Boolean.pipe(Schema.optional), | ||
| ledger_max_tokens: NonNegativeInt.pipe(Schema.optional), | ||
| ledger_recent_calls: NonNegativeInt.pipe(Schema.optional), | ||
| summary_carry: Schema.Boolean.pipe(Schema.optional), | ||
| summary_first_person: Schema.Boolean.pipe(Schema.optional), | ||
| pin_task: Schema.Boolean.pipe(Schema.optional), | ||
| pin_max_tokens: NonNegativeInt.pipe(Schema.optional), | ||
| pin_window_fraction: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1)).pipe( | ||
| Schema.optional, | ||
| ), | ||
| pin_card_max_tokens: NonNegativeInt.pipe(Schema.optional), | ||
| // altimate_change end | ||
| }) {} | ||
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
Keep.turnsis added to the V2 schema and the V1 migration mapstail_turnsinto it, but nothing reads it: session compaction still consumescfg.compaction.tail_turns(packages/opencode/src/session/compaction.ts:331), so a migratedturnsvalue lands inkeep.turnsand is silently ignored, falling back to DEFAULT_TAIL_TURNS. Wirekeep.turnsinto the compaction consumer (or drop the V2 schema/migration field) so the verbatim-tail setting actually takes effect for V2 configs.Prompt for AI agents