fix(nudge): unify transient-injection budgets into per-turn injection ledger - #228
fix(nudge): unify transient-injection budgets into per-turn injection ledger#228ranxianglei wants to merge 2 commits into
Conversation
… ledger pi rebuilds the outbound array on every LLM call, so any transient injection re-appends per fire unless budgeted. The #223 amplification chain happened because every patch measured a different quantity (Set dedup, failed-call counts, fire budgets) instead of injections. Replace all of them with one ledger in runtime: - noteInjection(turnKey, kind, budget): nudge=1/turn, emergency=MAX_EMERGENCY_NUDGES_PER_TURN(3)/turn; budgets count INJECTIONS, so ignoring/failing/no-op/neutral compress responses cannot extend them; only a genuine user-turn switch resets. - lastUserMessageId skips synthetic user messages (throttle kicks, delegate notifications) so they no longer rotate the turn key. - delete the compress-outcome machinery (noteCompressOutcomes, compressOutcomeSeen, compressFailTurnKey/Count, compressRetryCappedFor, collectCompressOutcomes, isCompressSuccessText/isCompressNoopText, separate nudgeShownTurns) - its only surviving customer was the emergency gate, whose neutral escape (compress([]) -> 'No ranges provided.' isError:false -> counter never moves) dies with it. - one-shot UI notice when the emergency budget exhausts; kernel >=95% truncation remains the mechanical backstop. Adversarial regression: 20 fires alternating failure/no-op/neutral/ silence yield exactly MAX_EMERGENCY_NUDGES_PER_TURN injections. Refs #223, #6, #7, #194, #9, #217
📦 Built Extension ArtifactBranch: Option A — Install from npm PR tag (recommended)pi install npm:billion-context-pi@pr-228Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf billion-context-pi-pr228.tgz
pi install ./packageThis comment is automatically updated on each push. |
The injection-ledger turn key hardcoded "[ACP:provider-throttle]" and "[acp_delegate " in lastUserMessageId() while the injection sites built their own headers independently — two copies of the same magic strings that can silently drift apart. If they ever diverge, synthetic user messages stop being skipped, rotate the turn key, and reset the very per-turn budgets that bound runaway injection loops. Single source of truth now: - export DELEGATE_NOTIFY_PREFIX from delegate-tool.ts; injectResult() builds its header from it - SYNTHETIC_USER_PREFIXES = [THROTTLE_KICK_SENTINEL, DELEGATE_NOTIFY_PREFIX] in tokens.ts; lastUserMessageId() matches against the array - document the contract: any NEW synthetic sendUserMessage injection site MUST register its prefix there
|
Follow-up (review nit 1 & 4): commit 8f390f2 removes the duplicated magic strings — |
|
关闭原因:这个修复是错误的——它偏离了本项目的既定设计,不应合入。 此前 review 给出的结论(LGTM、“修复正确且必要”)是错误的,请勿作为参考。作为 review 方,我未能理解到设计层面的意图,只机械地验证了实现本身的行为,是我太愚蠢,没能理解到这一层。 |
Summary
Implements the comprehensive governance plan from the #223 post-mortem audit (ework #18): one injection ledger replaces the whole family of ad-hoc retry/nudge budgets (#6 breaker, and the unmerged #7 / #9 / #194 / #217 branches).
Root cause of the #223 amplification chain
pi rebuilds the outbound array on every LLM call, so any transient injection re-appends per fire unless budgeted. Every patch in the lineage measured a different quantity — Set dedup, distinct failed calls, fire budgets — none measured injections:
compress({content: []})→"No ranges provided."isError:false → neutral, counter never moves → emergency nudge re-injects foreverThe fix
runtime.noteInjection(turnKey, kind, budget)): nudge = 1/genuine-turn, emergency =MAX_EMERGENCY_NUDGES_PER_TURN(3)/turn. Budgets count injections — ignoring, failing, no-op, or neutral compress responses cannot extend them. Only a genuine user-turn switch resets.lastUserMessageId(src/tokens.ts) now skips synthetic user messages ([ACP:provider-throttle]kicks,[acp_delegate …]notifications) — they no longer reset budgets.noteCompressOutcomes,compressOutcomeSeen,compressFailTurnKey/Count,compressRetryCappedFor,collectCompressOutcomes,isCompressSuccessText/isCompressNoopText, and the separatenudgeShownTurnsSet. The neutral escape dies with it.Adversarial regression test
20 context fires alternating every response shape the old budgets could not reach (hard failure / no-op panel / neutral
"No ranges provided."/ plain silence) → exactly 3 emergency injections. Throttle-kick and delegate messages do not re-arm the budget; a genuine user message does. #223 regression (zero retry prompts ever) retained.Test plan
npm run typecheck✓npm test— 426/426 ✓npm run build✓ (dist 517.77 KB, −1.95 KB)Closes the governance gap behind #223/#6; supersedes the budget logic in unmerged #7/#9/#194/#217 (their branches can be closed after this merges).