Summary
Split out from #494 (Requested fix 3). #494 / PR #511 fixed the deny-induced retry and double-execution paths, but the third cost factor from the same incident remains: the adapter's per-turn full-history replay is unbounded, and large injected directive blocks compound in it until the prompt exceeds the context window.
Incident data (2026-07-29 KST)
One session's first-message prompt, replayed into a fresh CLI session every turn, over 28 minutes:
| time (KST) |
prompt bytes |
<ultrawork-mode> copies |
share of prompt |
| 23:28 |
167,725 |
21 |
15% |
| 23:49 |
575,091 |
65 |
— |
| 23:56 |
875,961 |
70 |
73% (637,732 B) |
Each copy is a ~24,871-byte block injected once per matching user input by the ultrawork extension — correct, conditional behavior on the extension side. The compounding comes from buildPromptBlocks() (prompt-bridge.ts) re-serializing the entire message history every turn: every past injection is re-sent forever, and each additional matching input adds another full copy. By 23:56 KST the prompt (~220K tokens) exceeded the model context window; auto-compaction did not intervene during the error-churn window.
Why this is a design gap, not a bug
The replay-per-turn design is documented and intentional (providers.md: "senpi executes every tool itself"; session affinity "keeps Anthropic's prompt cache warm") and is cheap while the cache stays warm. Nothing bounds it once the cache goes cold — limit errors, account failover, provider switches — which is exactly the incident condition.
Options considered
resume (rejected). The SDK exposes resume / forkSession / resumeSessionAt, but adopting them inverts the stateless-bridge design: senpi's session is the single source of truth (forking, compaction, steering, mid-session model switches). A resumed CLI session would carry the interrupted deny artifacts the adapter deliberately discards, would miss turns served by other providers mid-session, and interacts badly with account failover.
Dedupe repeated directive blocks in buildPromptBlocks() (proposed). Keep the first <ultrawork-mode>…</ultrawork-mode> occurrence; replace later byte-identical copies with a short marker; leave non-identical variants untouched. Preserves the documented design and prompt-cache behavior.
Proposed implementation (verified locally against the installed dist)
function dedupeRepeatedDirectiveBlocks(blocks) {
const OPEN = "<ultrawork-mode>";
const CLOSE = "</ultrawork-mode>";
const MARKER = OPEN + "[directive unchanged - identical to the first occurrence above]" + CLOSE;
let first = null;
for (const block of blocks) {
if (block.type !== "text" || !block.text.includes(OPEN)) continue;
const text = block.text;
let out = "";
let i = 0;
let changed = false;
for (;;) {
const s = text.indexOf(OPEN, i);
if (s === -1) { out += text.slice(i); break; }
const e = text.indexOf(CLOSE, s + OPEN.length);
if (e === -1) { out += text.slice(i); break; }
const end = e + CLOSE.length;
const span = text.slice(s, end);
if (first === null) { first = span; out += text.slice(i, end); }
else if (span === first) { out += text.slice(i, s) + MARKER; changed = true; }
else out += text.slice(i, end);
i = end;
}
if (changed) block.text = out;
}
return blocks;
}
// return blocks.length > 0 ? dedupeRepeatedDirectiveBlocks(blocks) : [{ type: "text", text: "" }];
Verification (running locally since 2026-07-30 KST):
- Unit, incident-shaped context (three identical ~25KB injections + one different variant): full copies kept 1, markers 2, payload 76,656 B → 25,788 B; the incident's 70-copy case scales to ~1.7 MB → ~25 KB + 69 markers.
- Non-identical variants and all user text preserved; contexts without the tag byte-identical.
- Cache-prefix stability holds: serializing the first N messages yields an exact prefix of the full serialization (replacement decisions depend only on earlier content), so Anthropic prompt-cache hits are unaffected.
- E2E smoke through the
senpi CLI (claude-agent-sdk provider): capture → in-process execution → replay all normal.
Remaining related gap
Auto-compaction (contextTokens > contextWindow - reserveTokens) is the only bound on replay growth, and it failed to intervene during the incident's error-churn window — worth a separate look at whether errored turns feed the context estimator.
Full transcript archaeology: #494 (comments).
Summary
Split out from #494 (Requested fix 3). #494 / PR #511 fixed the deny-induced retry and double-execution paths, but the third cost factor from the same incident remains: the adapter's per-turn full-history replay is unbounded, and large injected directive blocks compound in it until the prompt exceeds the context window.
Incident data (2026-07-29 KST)
One session's first-message prompt, replayed into a fresh CLI session every turn, over 28 minutes:
<ultrawork-mode>copiesEach copy is a ~24,871-byte block injected once per matching user input by the ultrawork extension — correct, conditional behavior on the extension side. The compounding comes from
buildPromptBlocks()(prompt-bridge.ts) re-serializing the entire message history every turn: every past injection is re-sent forever, and each additional matching input adds another full copy. By 23:56 KST the prompt (~220K tokens) exceeded the model context window; auto-compaction did not intervene during the error-churn window.Why this is a design gap, not a bug
The replay-per-turn design is documented and intentional (
providers.md: "senpi executes every tool itself"; session affinity "keeps Anthropic's prompt cache warm") and is cheap while the cache stays warm. Nothing bounds it once the cache goes cold — limit errors, account failover, provider switches — which is exactly the incident condition.Options considered
resume(rejected). The SDK exposesresume/forkSession/resumeSessionAt, but adopting them inverts the stateless-bridge design: senpi's session is the single source of truth (forking, compaction, steering, mid-session model switches). A resumed CLI session would carry the interrupted deny artifacts the adapter deliberately discards, would miss turns served by other providers mid-session, and interacts badly with account failover.Dedupe repeated directive blocks in
buildPromptBlocks()(proposed). Keep the first<ultrawork-mode>…</ultrawork-mode>occurrence; replace later byte-identical copies with a short marker; leave non-identical variants untouched. Preserves the documented design and prompt-cache behavior.Proposed implementation (verified locally against the installed dist)
Verification (running locally since 2026-07-30 KST):
senpiCLI (claude-agent-sdk provider): capture → in-process execution → replay all normal.Remaining related gap
Auto-compaction (
contextTokens > contextWindow - reserveTokens) is the only bound on replay growth, and it failed to intervene during the incident's error-churn window — worth a separate look at whether errored turns feed the context estimator.Full transcript archaeology: #494 (comments).