Skip to content

fix: break semantic-level compress loops at low temp (#330) - #331

Open
ranxianglei wants to merge 1 commit into
masterfrom
2026-09-08_issue330-compress-loop-breaker
Open

fix: break semantic-level compress loops at low temp (#330)#331
ranxianglei wants to merge 1 commit into
masterfrom
2026-09-08_issue330-compress-loop-breaker

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Summary

Follow-up to #308 — that generic breaker correctly counted failures and paused the nudge, but the loop in session 01a07b3c still ran to manual abort because the existing breakers stop the tool from doing damage, not the model from generating another ~10K-token repetitive compress turn under a low-temp attractor (99.7%-identical thinking across 5 rounds). This PR adds the input-side counter-signals that actually break the semantic attractor.

Triage (verified against v0.1.59 / acp-kernel 0.0.56)

Issue factor Verdict Action
#1 dangling toolCall after abort Confirmed real bug — an aborted compress call survives into the sent view as an unmatched tool_use (invalid for OpenAI-compatible providers, and the hook that drags the model back into re-issuing it) Fixed (P0)
#2 PAUSED lacks self-heal text Largely already done in v0.1.59 — cappedRejectionText already embeds the correct remaining ranges + snapshot; only missing an explicit acp_status pointer Completed (P0)
#3 KEEP_LAST_ORPHANED hides diagnostics By-design kernel orphan-hiding; changing it risks context bloat Deferred → follow-up issue
#4 mid-loop nudge injection Confirmed — nudge suppression only engaged at the MAX_COMPRESS_ATTEMPTS cap, so it kept injecting at failure 1–2 Fixed (P1)

Root-cause layering: #308/#6/#250 are tool-side breakers; a semantic-level attractor needs an input-side counter-signal. This is the "breaker fired but the loop continued" successor form of #308.

Changes

  • src/messages.ts — drop dangling tool_calls from interrupted turns (stopReason ∈ {aborted, error}) so the sent view carries no unmatched tool_use. Keyed off stopReason, not a missing-result scan, so OMP execution roles and evicted/undo fixtures are untouched.
  • src/index.ts + src/runtime.ts — nudge suppression now engages on the first failed/no-op compress attempt this turn (not only at the cap); an independent [ACP:compress-loop] user-role stop-signal is injected once in-turn failures reach COMPRESS_LOOP_CORRECT_THRESHOLD (2). Both self-clear per user turn.
  • src/compress-tool.ts — capped-rejection text now explicitly points at acp_status.
  • src/system-prompt.ts — documents how to interpret [ACP:compress-loop].

Tests

+10 new assertions across 4 files: dangling-toolCall drop (incl. the deliberate scope guard proving we key off stopReason, not result-presence), compress-loop unit (sentinel/threshold/text), the compressFailCountFor counting contract, and end-to-end correction injection + self-clear through the real context handler. Full suite 633 pass / 0 fail, typecheck clean, build OK.

Not addressed here (follow-ups filed separately)

  • P2: remove compress from the tools list mid-session after repeated same-arg failures (feasibility TBD against Pi's dynamic tools API).
  • Factor fix: release workflow grep lookbehind error #3: kernel orphan-hiding (KEEP_LAST_ORPHANED) swallowing the actionable rejection diagnostics.
  • Factor #4b: nudge targetBlocks vs acp_status block-list consistency (suspected acp-kernel side).

Model-side mitigation (temp 0.1 → 1.0) was applied separately by the owner on 09-08.

Fixes #330

Follow-up to #308's generic breaker: that breaker stops the TOOL from doing
damage but cannot stop the MODEL from generating another ~10K-token repetitive
compress turn under a low-temp attractor (99.7%-identical thinking). Adds the
input-side counter-signals that actually break the semantic attractor:

- messages.ts: drop dangling tool_calls from interrupted (aborted/error) turns
  so the sent view carries no unmatched tool_use — invalid for OpenAI-compat
  providers and the hook that drags the model back into re-issuing the call.
  Keyed off stopReason, not a missing-result scan, so OMP execution roles and
  evicted/undo fixtures are untouched.
- index.ts + runtime.ts: nudge suppression now engages on the FIRST failed/no-op
  compress attempt this turn (not only at MAX_COMPRESS_ATTEMPTS), and an
  independent [ACP:compress-loop] user-role stop-signal is injected once in-turn
  failures reach COMPRESS_LOOP_CORRECT_THRESHOLD (2). Self-clears per turn.
- compress-tool.ts: capped-rejection text now points at acp_status explicitly.
- system-prompt.ts: document how to interpret [ACP:compress-loop].

Fixes #330
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

📦 Built Extension Artifact

Branch: 2026-09-08_issue330-compress-loop-breaker (5f2408d)

Option A — Install from npm PR tag (recommended)

pi install npm:billion-context-pi@pr-331

Each push to this PR publishes a new version under the pr-331 npm tag.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pi-pr331.tgz
pi install ./package

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 PR #331 reviewed — LGTM, ship-ready. I checked out the branch (5f2408d on master f2c5924) and re-ran everything rather than trusting the description.

Verification (all performed locally against the PR head)

  • typecheck: clean (tsc --noEmit, zero output)
  • tests: 633 pass / 0 fail / 3 skipped — exactly as claimed (suite grows 626→636 total with +10 new tests)
  • build: OK, dist/index.js 729.90 KB (kernel still inlined, zero runtime deps)
  • hygiene: no version bump, no CHANGELOG churn (matches repo convention — changelog moves only on release commits), as AgentMessage at src/index.ts:517 follows the existing cast convention (src/index.ts:680), no as any/@ts-ignore

Factor-by-factor audit

#1 dangling toolCall after abort — confirmed real, fix is correctly scoped. Two things I verified beyond the diff:

  • Pi does record stopReason on assistant entries: StopReason = "pending" | "stop" | "length" | "toolUse" | "error" | "aborted" (@earendil-works/pi-ai types.d.ts:282, field at :298). So the key exists in the session log.
  • No orphaning risk from the drop: in pi-agent-core's agent loop (agent-loop.js:108-112), an assistant message landing with stopReason ∈ {aborted, error} returns immediately with toolResults: [] — its tools never ran, so dropping the tool_calls can never strand an existing result. The reverse case (abort during tool execution) keeps stopReason: "toolUse" and synthesizes "Operation aborted" error results (agent-loop.js:412-433), so those stay paired and untouched. Keying off stopReason instead of a missing-result scan is therefore not just conservative, it's the only correct discriminator — and the deliberate scope-guard test locking that in is the right call.

#2 PAUSED text — confirmed: cappedRejectionText already carried ranges+snapshot in v0.1.59; the one-line acp_status pointer addition (src/compress-tool.ts:253) completes it. Verdict "largely already done" checks out.

#4 mid-loop nudge injection — confirmed fixed. Traced the counting path end-to-end: collectCompressOutcomesnoteCompressOutcomes (idempotent per toolCallId via compressOutcomeSeen, src/runtime.ts:338-355; success resets to 0; new turnKey resets) → compressFailCountFor read after noteCompressOutcomes on the same fire (src/index.ts:415-420). Nudge suppression now engages at compressFails >= 1 (src/index.ts:489); the [ACP:compress-loop] sentinel injects once per context event at ≥2 failures and sits outside the turn.nudge?.shouldInject gate (src/index.ts:516-520), so it fires even when the kernel isn't nudging — which is exactly the attractor case. Self-clear is structural (turnKey reset), not timer-based. System-prompt documentation added and byte-stability test updated accordingly.

Root-cause layering — agreed: #308/#6/#250 are tool-side breakers (limit damage); a low-temp semantic attractor needs input-side counter-signals, and both additions here are input-side. Deferred items (#3 orphan-hiding, P2 tool removal, #4b targetBlocks consistency) are appropriately scoped out.

Non-blocking observations

  1. Emergency-nudge trade-off: with suppression at first failure, a long agentic turn where the first compress fails transiently (e.g. typo'd ref) will not get any subsequent nudge — including emergency — for the rest of that turn unless a compress succeeds or the user sends a new message. Mitigations exist (failure toolResult carries actionable refs; the tool itself stays callable; kernel emergency truncation still shrinks context mechanically), and the diff comment shows this was deliberate — just noting the behavioral delta for long turns.
  2. Wording vs mechanism: the sentinel text says compression "is paused until your next user request," but the tool is only mechanically rejected at the MAX_COMPRESS_ATTEMPTS cap (3); at count 2 the signal is purely persuasive. That's fine — it is an input-side counter-signal by design — just flagging it's not literally enforced yet.
  3. Cosmetic: PR body says "+10 new assertions"; it's actually +10 new tests (~28 assert calls) across 4 files. No action needed.

One housekeeping check before merge: the three "follow-ups filed separately" — worth confirming those issues exist so nothing gets lost. Per repo rules I won't merge; ready for human merge once you're satisfied.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Follow-up on my housekeeping check — the three claimed "filed separately" items did not exist in either repo (verified via API: newest bcp item was #331/#330; newest acp-kernel item was #219). I've filed them now, each with a 来源: marker pointing back at this PR / #330:

  • billion-context-pi#332 — P2: remove compress from tools list mid-session after repeated same-arg failures (feasibility vs Pi's dynamic tools API marked TBD in the body)
  • acp-kernel#220 — factor fix: release workflow grep lookbehind error #3: KEEP_LAST_ORPHANED orphan-hiding swallowing the actionable rejection diagnostics (three candidate design directions listed for discussion)
  • acp-kernel#221 — factor #4b: nudge targetBlocks vs acp_status block-list consistency (repro path + suspected drift mechanism listed)

Nothing else outstanding from my side — review stands as posted (LGTM), merge is yours.

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.

压缩循环语义级复发(temp0.1 在役):abort 悬空 toolCall 残留 + PAUSED 无自愈信息 + 中途 nudge 注入(01a07b3c)

1 participant