Skip to content

fix(session): relax successful same-target loop gate - #416

Merged
Astro-Han merged 5 commits into
devfrom
codex/fix-i406-loop-gate-policy
May 4, 2026
Merged

fix(session): relax successful same-target loop gate#416
Astro-Han merged 5 commits into
devfrom
codex/fix-i406-loop-gate-policy

Conversation

@Astro-Han

@Astro-Han Astro-Han commented May 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Stop hard-blocking successful same-target repeats.
  • Gate successful exact-input repeats only when prior completed records have the same output and no intervening parent-turn mutation epoch.
  • Track mutationEpoch across assistant messages under the same parent turn, not per assistant message.
  • Preserve attemptedInput on synthetic loop block/stop diagnostics, with size guarding before storage and export redaction.
  • Export success loop diagnostics without pretending they are failures.

Why

PR #406 made repeated successful tool calls observable, but the same-target hard gate was too broad. A normal debugging session can read different ranges of the same file, which repeats the target while still making progress. The exported quick-river session showed this exact shape: successful read calls against the same file with different offsets were blocked and then stopped.

The follow-up review also identified a second false-positive shape: repeating the same successful input after edits or after output changes can be valid verification. This update keeps hard stops for true no-progress success loops, but requires exact input plus same output within the same parent-turn mutation epoch before escalating.

Related Issue

No issue. Follow-up to the merged #406 behavior after a false-positive exported-session diagnosis.

Human Review Status

Pending. A human should make the final merge decision after reviewing the final diff and verification evidence.

Review Focus

Please focus on loop-gate policy semantics:

  • Successful same-target repeats should not hard block or stop.
  • Successful exact-input repeats hard gate only when the completed output also repeats.
  • Successful exact-input repeats across a mutation epoch should not hard gate.
  • mutationEpoch is scoped to the parent turn across assistant messages.
  • bash file writes produce patch parts and reset exact-input success gating after mutation.
  • Failure same-target and same-input gates still work.
  • Synthetic block/stop diagnostics include attempted input, while large attemptedInput values are compacted and exports still redact them.

Compatibility Notes

  • Success loop exports intentionally do not populate completedFailures. Consumers should use completedCount for success-loop events.
  • completedFailures remains present for failure-loop events.

Risk Notes

Behavior change in the session loop gate. This intentionally reduces hard stops for successful same-target repeats and for repeated successful inputs when output or mutation state changed, while preserving the stricter failure loop gate and same-output success gate. No migrations, dependencies, generated files, permissions, credentials, or visible UI changes.

How To Verify

Review follow-up focused tests: 117 passed
bun --cwd packages/opencode test test/session/loop-gate.test.ts test/session/diagnostics.test.ts test/session/processor-effect.test.ts test/session/prompt-effect.test.ts

Full session tests: 502 passed, 4 skipped, 1 todo, 0 failed
bun --cwd packages/opencode test test/session

Typecheck: passed
bun run --cwd packages/opencode typecheck

Diff check: no whitespace errors
git diff --check

Screenshots or Recordings

Not applicable. No visible UI changes.

Checklist

  • Human review status is stated above as pending, approved, or not required
  • I linked the related issue, or stated why there is no issue
  • This PR has type, scope, and priority labels, or I requested maintainer labeling
  • I described the review focus and any meaningful risks
  • I listed the relevant verification steps and the key result for each
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope
  • I manually checked visible UI or copy changes when needed, with screenshots or recordings
  • I considered macOS and Windows impact for desktop, packaging, updater, signing, paths, shell, or permissions changes
  • I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant
  • I reviewed the final diff for unrelated changes and suspicious dependency changes
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English

Summary by CodeRabbit

  • Improvements

    • Diagnostics now capture output hashes, mutation epochs, and attempted tool inputs; large diagnostic values are compacted/truncated. Reminders now distinguish input vs. target successes. Gate matching refined: successful repeats match by exact input; non-success cases prioritize target then input. Snapshot exports redact attempted inputs.
  • Tests

    • Added/updated tests for diagnostic compaction, snapshot redaction, gate behavior, mutation-epoch handling, and loop-gate scenarios.

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c949094-9769-4afd-8639-14fe2c0f55bb

📥 Commits

Reviewing files that changed from the base of the PR and between 04ec1c2 and a35e693.

📒 Files selected for processing (1)
  • packages/opencode/test/session/prompt-effect.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/opencode/test/session/prompt-effect.test.ts

📝 Walkthrough

Walkthrough

This PR extends loop diagnostics with outputHash, mutationEpoch, and attemptedInput; adds outputHash() and compactDiagnosticValue() helpers; buckets successful repeats by input+output and respects mutation epochs in deriveParentLoopState; changes gate candidate selection in queryGateAction; threads attemptedInput and mutationEpoch through prompt → processor → export and redacts attemptedInput on snapshot export.

Changes

Loop diagnostics + gate-selection wiring

Layer / File(s) Summary
Data Shape
packages/opencode/src/session/diagnostics.ts
Added outputHash?: string and mutationEpoch?: number to ToolCallRecord and LoopMetadata; added attemptedInput?: unknown to LoopMetadata; added SignatureState.outputHash?: string.
Diagnostic Helpers
packages/opencode/src/session/diagnostics.ts
Added exported outputHash(output: unknown) and compactDiagnosticValue(value: unknown) with DIAGNOSTIC_VALUE_BYTE_LIMIT truncation behavior.
Core Logic: Parent-State Derivation
packages/opencode/src/session/diagnostics.ts
deriveParentLoopState now accepts currentMutationEpoch?: number, buckets successful inputs by inputSigKey + outputHash, filters by mutationEpoch, and preserves target-sig handling when materializing signatures.
Core Logic: Gate Candidate Selection
packages/opencode/src/session/diagnostics.ts
queryGateAction considers only the input sigKey for outcome === "success", and [targetKey, inputKey] (target-first) for non-success outcomes.
Reminders/Text
packages/opencode/src/session/diagnostics.ts
consumeReminders emits distinct success reminders for input vs target repeats (legacy fallback preserved).
Processor Implementation
packages/opencode/src/session/processor.ts
buildLoopContext and loopRecords track/increment mutationEpoch (increment on "patch" parts); ToolCallRecords now include mutationEpoch; completeToolCall merges outputHash into diagnostics; recordSyntheticBlock/recordSyntheticStop accept and persist attemptedInput.
Prompt Wiring
packages/opencode/src/session/prompt.ts
applyLoopGate forwards currentMutationEpoch into deriveParentLoopState and passes attemptedInput: SessionDiagnostics.compactDiagnosticValue(args) into processor.recordSyntheticBlock/recordSyntheticStop.
Export & Sanitization
packages/opencode/src/session/export.ts
Export.Snapshot.runtime_context.diagnostics.loop.last gains attemptedInput?: unknown; deriveSnapshotDiagnostics exposes attemptedInput and sources completedFailures from loop.loopCompletedFailures; added dataValue helper and sanitizeDiagnostics to redact loop.last.attemptedInput during sanitizeSnapshot.
Tests
packages/opencode/test/session/*
Tests updated/added to cover outputHash/mutationEpoch/attemptedInput plumbing: new helpers, adjusted gating expectations (some same-target/quarantine → observe), exact-input block tests, output-change and mutation-epoch behavior tests, export/sanitize attemptedInput redaction tests, and compactDiagnosticValue / consumeReminders tests.

Sequence Diagram

sequenceDiagram
    participant Client as Client/Prompt
    participant Gate as applyLoopGate
    participant Diag as SessionDiagnostics
    participant Proc as Processor
    participant Export as Snapshot Export

    Client->>Gate: applyLoopGate(args, loopCtx)
    Gate->>Diag: deriveParentLoopState(..., currentMutationEpoch)
    Gate->>Diag: queryGateAction(outcome, inputKey, targetKey)
    alt outcome === "success"
        Diag-->>Gate: evaluate candidates for inputKey (bucketed by input+outputHash)
    else non-success
        Diag-->>Gate: evaluate targetKey then inputKey
    end
    alt decision is "block"
        Gate->>Proc: recordSyntheticBlock({attemptedInput: compactDiagnosticValue(args), ...})
        Proc->>Diag: persist diagnostics.loop.attemptedInput and loop.mutationEpoch
    else decision is "stop"
        Gate->>Proc: recordSyntheticStop({attemptedInput: compactDiagnosticValue(args), ...})
        Proc->>Diag: persist diagnostics.loop.attemptedInput and loop.mutationEpoch
    end
    Note over Export,Diag: sanitizeSnapshot calls sanitizeDiagnostics to redact attemptedInput
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels

bug, P2, harness

Poem

🐇 I nudged the loop and kept a trace,

A tiny hash to know the place.
I saved the tried input—trimmed and neat,
Then redacted it before retreat.
Hops and hashes, tidy and fleet 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: relaxing the successful same-target loop gate, which is the primary behavioral fix across multiple modified files.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all template sections with detailed explanations of changes, rationale, risks, verification steps, and a completed checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-i406-loop-gate-policy

Review rate limit: 9/10 reviews remaining, refill in 6 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request updates the loop gating mechanism to allow successful repeats of the same target, such as reading different parts of a file, while maintaining hard-blocking for exact-input repeats. It also introduces an attemptedInput field across diagnostic interfaces and session exports to improve debugging. Feedback was provided regarding a potential security risk where the new attemptedInput field in session exports might leak sensitive data because it is not currently processed by the sanitizeSnapshot function.

Comment thread packages/opencode/src/session/export.ts

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/session/export.ts`:
- Around line 145-146: Update Export.sanitizeSnapshot() to redact or remove
diagnostics.loop.last.attemptedInput (so raw tool args can't leak) by
normalizing that key to a safe placeholder (e.g., "[REDACTED]") or omitting it;
perform this as part of a single sanitizer pass that runs over both state.error
and loop/metadata fields (including diagnostics.loop.* and any top-level
metadata) rather than only runtime_context/session—modify the
Export.sanitizeSnapshot() implementation to explicitly locate
diagnostics.loop.last and sanitize attemptedInput and to apply the same
redaction logic to state.error so both sources are covered in one pass.

In `@packages/opencode/test/session/loop-gate.test.ts`:
- Around line 200-224: The test currently only asserts the gate decision but not
that the successful records actually map to the same target-signature bucket;
update the test (around the use of SessionDiagnostics.deriveParentLoopState and
the three successfulToolCallRecord entries) to assert that all three success
records resolve to the same target signature key (e.g., compare the result of
the same target-signature helper you use in production or recompute via
targetHashForInput/inputHashFor) and/or assert that the state's grouping or the
computed query hash equals targetSigKey/targetHash—specifically verify that each
successfulToolCallRecord produces the same bucket key (targetSigKey) before
calling SessionDiagnostics.queryGateAction and keep the existing
expect(decision.action).toBe("observe").
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e27b8f04-a3f3-494c-b646-6ec505861696

📥 Commits

Reviewing files that changed from the base of the PR and between a87364b and 34f5811.

📒 Files selected for processing (7)
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts

Comment thread packages/opencode/src/session/export.ts
Comment thread packages/opencode/test/session/loop-gate.test.ts

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/session/processor.ts`:
- Around line 219-240: The mutationEpoch is being reset to 0 for each assistant
message, causing epochs to be compared only per-message rather than per-parent;
move the mutationEpoch counter out of the per-message scope so it is tracked per
parentID (parent-scoped) and incremented when encountering "patch" parts across
all messages for that parent; update both places where mutationEpoch is
declared/used (the block around message.parts handling starting at the shown
diff and the similar block at lines ~328-360) to read/increment the same
parent-scoped mutationEpoch and ensure loop.mutationEpoch falls back to that
parent counter when undefined so deriveParentLoopState() sees consistent epochs
across all records for the same parentID.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d71f2619-7c6a-4d8e-85d6-d93a2c944019

📥 Commits

Reviewing files that changed from the base of the PR and between 883cf7a and b0bbe62.

📒 Files selected for processing (6)
  • packages/opencode/src/session/diagnostics.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/diagnostics.test.ts
  • packages/opencode/test/session/loop-gate.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/opencode/test/session/diagnostics.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/opencode/test/session/loop-gate.test.ts

Comment thread packages/opencode/src/session/processor.ts Outdated
@Astro-Han
Astro-Han merged commit f296398 into dev May 4, 2026
23 checks passed
@Astro-Han
Astro-Han deleted the codex/fix-i406-loop-gate-policy branch May 4, 2026 04:27
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.

1 participant