zelari-code 1.41.0 → 1.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,48 @@
1
1
  /**
2
- * Optional LLM pass for history compaction summaries.
3
- * Falls back to null on any failure so callers use extractive summary.
2
+ * LLM compaction via CACHE-AWARE PREFIX REPLAY (v1.36.0, P9).
4
3
  *
5
- * Disable with ZELARI_LLM_COMPACT=0.
4
+ * Pre-1.36 this module built a COLD request: its own COMPACT_SYSTEM, a
5
+ * flattened transcript, and a raw fetch to the chat-completions endpoint.
6
+ * That request diverged from the live conversation from token 0 — zero
7
+ * prompt-cache reuse — and the rewritten history invalidated the warm
8
+ * prefix for the NEXT conversation turn too.
6
9
  *
7
- * @since v1.21.0
10
+ * The replay approach sends:
11
+ *
12
+ * SYSTEM(original) + TOOLS(original) + DROPPED PREFIX + COMPACTION_INSTRUCTION
13
+ *
14
+ * so everything up to the trailing instruction is byte-identical to the
15
+ * previous routed request and hits the provider prefix cache (DeepSeek/
16
+ * OpenAI/GLM bill cached tokens at ~1/10). Tools stay advertised even
17
+ * though the summarizer must not call them: removing them would change
18
+ * the prefix token sequence and kill cache reuse.
19
+ *
20
+ * Fallback: any failure → null → caller uses the extractive summary.
21
+ * Disable entirely with ZELARI_LLM_COMPACT=0.
22
+ *
23
+ * @since v1.21.0 (cold-request version)
24
+ * @updated v1.36.0 — replay-based, providerStream-routed
8
25
  */
9
- import { getModelForProvider, getProviderConfig, getCustomEndpoint, } from '../providerConfig.js';
10
- import { resolveApiKeyWithMeta } from '../keyStore.js';
11
- import { PROVIDER_ENDPOINTS, } from '../provider/openai-compatible.js';
12
- const COMPACT_SYSTEM = `You compress earlier turns of a coding-agent session into a dense continuity brief.
13
- Output plain text (no markdown fences) with these sections:
14
- 1) Goal — what the user wants
15
- 2) Decisions — choices already made
16
- 3) Done — completed work / files changed
17
- 4) Open — remaining tasks / blockers
18
- 5) Constraints — important rules the agent must keep
26
+ export const COMPACTION_INSTRUCTION = `
27
+ You are now acting as a compaction engine for this coding-agent session.
19
28
 
20
- Be factual and concise. Max ~400 words. Do not invent work that was not present.`;
29
+ Condense the conversation ABOVE into a compact checkpoint sufficient to continue the task.
30
+
31
+ Preserve:
32
+ - user's goal and evolving intent
33
+ - decisions already made
34
+ - exact file paths and identifiers
35
+ - code changes already completed
36
+ - commands/errors that still matter
37
+ - constraints
38
+ - unfinished work
39
+ - the single most likely next action
40
+
41
+ Do not call tools.
42
+ Do not mention this summarization request.
43
+ Output only the checkpoint.
44
+ Be concise.
45
+ `.trim();
21
46
  export function isLlmCompactEnabled() {
22
47
  const v = process.env.ZELARI_LLM_COMPACT?.trim().toLowerCase();
23
48
  if (v === '0' || v === 'false' || v === 'off' || v === 'no')
@@ -25,90 +50,79 @@ export function isLlmCompactEnabled() {
25
50
  // default on when env unset
26
51
  return true;
27
52
  }
53
+ /** Explicit model override for the summarizer (ZELARI_COMPACT_MODEL). */
54
+ export function compactModelOverride() {
55
+ const v = process.env.ZELARI_COMPACT_MODEL?.trim();
56
+ return v ? v : undefined;
57
+ }
58
+ /** Hard ceiling so a stuck summarizer can't hang the dispatch loop. */
59
+ const REPLAY_TIMEOUT_MS = 60_000;
28
60
  /**
29
- * Ask the active provider for a continuity summary.
30
- * Returns null if disabled, no key, or request fails.
61
+ * Summarize `droppedMessages` by replaying the ORIGINAL request prefix and
62
+ * appending the compaction instruction as the final user message.
63
+ *
64
+ * Returns `summary: null` when disabled, empty, failed, or when the model
65
+ * emitted a tool call (the summarizer must never act — only condense).
31
66
  */
32
- export async function llmSummarizeHistory(input) {
67
+ export async function llmSummarizeHistoryReplay(input) {
68
+ const override = input.overrideModel ?? compactModelOverride();
69
+ const model = override ?? input.model;
70
+ const cacheReuseExpected = !override;
33
71
  if (!isLlmCompactEnabled())
34
- return null;
35
- if (!input.droppedTranscript.trim())
36
- return null;
37
- let config = null;
38
- try {
39
- config = await resolveCompactProviderConfig();
40
- }
41
- catch {
42
- return null;
72
+ return { summary: null, model, cacheReuseExpected };
73
+ if (input.droppedMessages.length === 0) {
74
+ return { summary: null, model, cacheReuseExpected };
43
75
  }
44
- if (!config)
45
- return null;
46
- const model = process.env.ZELARI_COMPACT_MODEL?.trim() || config.model;
76
+ // Replay = original prefix + instruction tail. The prefix must be
77
+ // byte-identical to the previous routed request for cache reuse.
78
+ const messages = [
79
+ ...input.systemMessages,
80
+ ...input.droppedMessages,
81
+ {
82
+ role: 'user',
83
+ content: COMPACTION_INSTRUCTION,
84
+ },
85
+ ];
47
86
  const controller = new AbortController();
48
- const timeout = setTimeout(() => controller.abort(), 45_000);
87
+ const timeout = setTimeout(() => controller.abort(), REPLAY_TIMEOUT_MS);
49
88
  const onOuterAbort = () => controller.abort();
50
89
  input.signal?.addEventListener('abort', onOuterAbort, { once: true });
51
90
  try {
52
- const url = `${config.baseUrl.replace(/\/$/, '')}/chat/completions`;
53
- const res = await fetch(url, {
54
- method: 'POST',
91
+ let text = '';
92
+ let emittedToolCall = false;
93
+ for await (const delta of input.providerStream({
94
+ provider: input.provider,
95
+ model,
96
+ messages,
97
+ // Tools stay advertised: dropping them would change the prefix token
98
+ // sequence and destroy cache reuse (explicit DSH decision). They are
99
+ // sorted canonically (same discipline as the live routed request and
100
+ // the snapshot fingerprints) so the replay prefix is byte-identical.
101
+ tools: [...input.tools].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
55
102
  signal: controller.signal,
56
- headers: {
57
- 'content-type': 'application/json',
58
- authorization: `Bearer ${config.apiKey}`,
59
- },
60
- body: JSON.stringify({
61
- model,
103
+ generation: {
104
+ purpose: 'compaction',
62
105
  temperature: 0.1,
63
- max_tokens: 900,
64
- stream: false,
65
- messages: [
66
- { role: 'system', content: COMPACT_SYSTEM },
67
- {
68
- role: 'user',
69
- content: `Extractive sketch (may be incomplete):\n${input.extractive.slice(0, 2_500)}\n\n` +
70
- `Transcript of dropped turns:\n${input.droppedTranscript}`,
71
- },
72
- ],
73
- }),
74
- });
75
- if (!res.ok)
76
- return null;
77
- const json = (await res.json());
78
- const text = json.choices?.[0]?.message?.content?.trim();
79
- if (!text)
80
- return null;
81
- return ('[history-summary · llm]\n' +
82
- text +
83
- '\n\nContinue from the recent messages below; honor decisions already made above.');
106
+ maxTokens: 900,
107
+ },
108
+ })) {
109
+ if (delta.kind === 'text')
110
+ text += delta.delta;
111
+ if (delta.kind === 'tool_call')
112
+ emittedToolCall = true;
113
+ }
114
+ if (emittedToolCall)
115
+ return { summary: null, model, cacheReuseExpected };
116
+ if (!text.trim())
117
+ return { summary: null, model, cacheReuseExpected };
118
+ return { summary: text.trim(), model, cacheReuseExpected };
84
119
  }
85
120
  catch {
86
- return null;
121
+ return { summary: null, model, cacheReuseExpected };
87
122
  }
88
123
  finally {
89
124
  clearTimeout(timeout);
90
125
  input.signal?.removeEventListener('abort', onOuterAbort);
91
126
  }
92
127
  }
93
- async function resolveCompactProviderConfig() {
94
- const active = getProviderConfig().activeProviderId;
95
- const meta = await resolveApiKeyWithMeta(active);
96
- const apiKey = meta?.apiKey;
97
- if (!apiKey)
98
- return null;
99
- const custom = getCustomEndpoint(active);
100
- let baseUrl = custom ||
101
- (active === 'openai-compatible' || active === 'custom'
102
- ? process.env.OPENAI_BASE_URL ?? PROVIDER_ENDPOINTS[active]
103
- : PROVIDER_ENDPOINTS[active]);
104
- if (!baseUrl)
105
- return null;
106
- const model = getModelForProvider(active);
107
- return {
108
- apiKey,
109
- baseUrl,
110
- model,
111
- providerId: active,
112
- };
113
- }
114
128
  //# sourceMappingURL=llmCompact.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"llmCompact.js","sourceRoot":"","sources":["../../../src/cli/budget/llmCompact.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"llmCompact.js","sourceRoot":"","sources":["../../../src/cli/budget/llmCompact.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * requestMeter — full-request occupancy estimation with provider-usage
3
+ * anchoring (v1.36.0 context/cache upgrade; P4/P5).
4
+ *
5
+ * The legacy `estimateHistoryTokens` measured ONLY the rolling history
6
+ * (`content` + `toolCalls` args). It ignored the system prompt, tool
7
+ * schemas, `reasoningContent`, and per-message role overhead — tens of
8
+ * thousands of tokens in tool-heavy sessions — so occupancy read low and
9
+ * compaction fired too late (or never).
10
+ *
11
+ * This meter measures the WHOLE request surface:
12
+ *
13
+ * estimatedPromptTokens ≈ system + tools-schema + conversation
14
+ *
15
+ * and anchors to provider-reported usage when the header fingerprint
16
+ * matches the last routed request: provider usage is ground truth for
17
+ * the stable header, so we only re-estimate the conversation DELTA.
18
+ *
19
+ * `contextPressureTokens` NEVER subtracts cached tokens: the provider
20
+ * must still HOLD the whole prefix (cached or not) in the context window.
21
+ *
22
+ * @since v1.36.0
23
+ */
24
+ import { stableStringify, sha256Hex, canonicalTools, } from '@zelari/core/harness';
25
+ /** Local chars→tokens estimate (same heuristic as tokenBudget; local copy to
26
+ * avoid a tokenBudget ⇄ requestMeter import cycle). */
27
+ function estimateTokensLocal(text) {
28
+ if (!text)
29
+ return 0;
30
+ return Math.max(1, Math.ceil(text.length / 4));
31
+ }
32
+ /** ~4 tokens of wire overhead per message (role/id framing). */
33
+ const MESSAGE_OVERHEAD_TOKENS = 4;
34
+ /** Estimate one message across ALL its token-bearing fields. */
35
+ export function estimateMessageTokens(m) {
36
+ let n = MESSAGE_OVERHEAD_TOKENS + estimateTokensLocal(m.content ?? '');
37
+ if (m.toolCalls) {
38
+ for (const tc of m.toolCalls) {
39
+ n += estimateTokensLocal(tc.name) + estimateTokensLocal(tc.id);
40
+ n += estimateTokensLocal(JSON.stringify(tc.args ?? {}));
41
+ }
42
+ }
43
+ if (m.reasoningContent)
44
+ n += estimateTokensLocal(m.reasoningContent);
45
+ if (m.toolCallId)
46
+ n += estimateTokensLocal(m.toolCallId);
47
+ return n;
48
+ }
49
+ /** Estimate the tool-schema surface (name + description + parameters). */
50
+ export function estimateToolSchemaTokens(tools) {
51
+ let n = 0;
52
+ for (const t of tools) {
53
+ n += estimateTokensLocal(t.name) + estimateTokensLocal(t.description ?? '');
54
+ n += estimateTokensLocal(JSON.stringify(t.parameters ?? {}));
55
+ }
56
+ // Wire framing per advertised function.
57
+ return n + tools.length * MESSAGE_OVERHEAD_TOKENS;
58
+ }
59
+ /** Estimate the system-prompt surface. */
60
+ export function estimateSystemTokens(systemMessages) {
61
+ let n = 0;
62
+ for (const m of systemMessages)
63
+ n += estimateMessageTokens(m);
64
+ return n;
65
+ }
66
+ /** Estimated tokens of the conversation tail. */
67
+ export function estimateConversationTokens(conversation) {
68
+ let n = 0;
69
+ for (const m of conversation)
70
+ n += estimateMessageTokens(m);
71
+ return n;
72
+ }
73
+ /**
74
+ * Same fingerprint discipline as requestSnapshot: provider + model + system
75
+ * + canonical tools. Cheap enough to recompute per turn.
76
+ */
77
+ export function headerFingerprintOf(input) {
78
+ return createFingerprintOnly(input);
79
+ }
80
+ function createFingerprintOnly(input) {
81
+ return sha256Hex(stableStringify({
82
+ provider: input.provider,
83
+ model: input.model,
84
+ systemMessages: input.systemMessages,
85
+ tools: canonicalTools(input.tools),
86
+ }));
87
+ }
88
+ /**
89
+ * Measure the full request surface.
90
+ *
91
+ * Anchoring: when `anchor.snapshot`'s headerFingerprint equals the current
92
+ * header fingerprint AND its usage arrived, the stable header is measured
93
+ * as `usage.promptTokens - estimatedConversationTokens(anchor.conversation)`
94
+ * (clamped to ≥0) — provider ground truth beats chars/4. The current
95
+ * conversation is always freshly estimated.
96
+ */
97
+ export function measureRequest(input) {
98
+ const estimatedHeaderTokens = estimateSystemTokens(input.systemMessages) + estimateToolSchemaTokens(input.tools);
99
+ const currentConversationTokens = estimateConversationTokens(input.conversation);
100
+ let headerAnchored = false;
101
+ let headerTokens = estimatedHeaderTokens;
102
+ const anchorUsage = input.anchor?.usage;
103
+ const anchorSnapshot = input.anchor?.snapshot;
104
+ if (anchorUsage && anchorSnapshot) {
105
+ const currentHeaderFp = createFingerprintOnly({
106
+ provider: anchorSnapshot.provider,
107
+ model: anchorSnapshot.model,
108
+ systemMessages: input.systemMessages,
109
+ tools: input.tools,
110
+ });
111
+ if (currentHeaderFp === anchorSnapshot.headerFingerprint) {
112
+ // Provider saw header + its conversation; subtract the conversation
113
+ // estimate to isolate the header ground truth.
114
+ const anchorConv = estimateConversationTokens(anchorSnapshot.conversation);
115
+ const headerFromUsage = Math.max(0, anchorUsage.promptTokens - anchorConv);
116
+ // v1.36.0 (case 9): the provider truth is EXPECTED to dwarf the
117
+ // chars/4 estimate (chat templating, tool marshalling, hidden
118
+ // framing). Anchoring exists precisely for that gap — so only a
119
+ // non-positive (garbage/empty) value is rejected, never a large one.
120
+ if (headerFromUsage > 0) {
121
+ headerTokens = headerFromUsage;
122
+ headerAnchored = true;
123
+ }
124
+ }
125
+ }
126
+ const estimatedPromptTokens = headerTokens + currentConversationTokens;
127
+ const reservedOutput = input.reservedOutputTokens ?? 0;
128
+ const contextPressureTokens = estimatedPromptTokens + reservedOutput;
129
+ return {
130
+ estimatedPromptTokens,
131
+ estimatedHeaderTokens,
132
+ headerAnchored,
133
+ contextPressureTokens,
134
+ occupancy: Math.min(1, contextPressureTokens / input.contextLimit),
135
+ purpose: input.purpose ?? 'conversation',
136
+ };
137
+ }
138
+ //# sourceMappingURL=requestMeter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestMeter.js","sourceRoot":"","sources":["../../../src/cli/budget/requestMeter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EACL,eAAe,EACf,SAAS,EACT,cAAc,GACf,MAAM,sBAAsB,CAAC;AAG9B;wDACwD;AACxD,SAAS,mBAAmB,CAAC,IAAY;IACvC,IAAI,CAAC,IAAI;QAAE,OAAO,CAAC,CAAC;IACpB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,gEAAgE;AAChE,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAiClC,gEAAgE;AAChE,MAAM,UAAU,qBAAqB,CAAC,CAAe;IACnD,IAAI,CAAC,GAAG,uBAAuB,GAAG,mBAAmB,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;IACvE,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;QAChB,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;YAC7B,CAAC,IAAI,mBAAmB,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YAC/D,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IACD,IAAI,CAAC,CAAC,gBAAgB;QAAE,CAAC,IAAI,mBAAmB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC;IACrE,IAAI,CAAC,CAAC,UAAU;QAAE,CAAC,IAAI,mBAAmB,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IACzD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,wBAAwB,CAAC,KAA+B;IACtE,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,CAAC,IAAI,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,CAAC,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;QAC5E,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/D,CAAC;IACD,wCAAwC;IACxC,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,uBAAuB,CAAC;AACpD,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,oBAAoB,CAClC,cAAuC;IAEvC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,MAAM,CAAC,IAAI,cAAc;QAAE,CAAC,IAAI,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAC9D,OAAO,CAAC,CAAC;AACX,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,0BAA0B,CACxC,YAAqC;IAErC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,MAAM,CAAC,IAAI,YAAY;QAAE,CAAC,IAAI,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAC5D,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAKnC;IACC,OAAO,qBAAqB,CAAC,KAAK,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,qBAAqB,CAAC,KAK9B;IACC,OAAO,SAAS,CACd,eAAe,CAAC;QACd,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC;KACnC,CAAC,CACH,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAC5B,KAIC;IAED,MAAM,qBAAqB,GACzB,oBAAoB,CAAC,KAAK,CAAC,cAAc,CAAC,GAAG,wBAAwB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrF,MAAM,yBAAyB,GAAG,0BAA0B,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAEjF,IAAI,cAAc,GAAG,KAAK,CAAC;IAC3B,IAAI,YAAY,GAAG,qBAAqB,CAAC;IAEzC,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC;IACxC,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC9C,IAAI,WAAW,IAAI,cAAc,EAAE,CAAC;QAClC,MAAM,eAAe,GAAG,qBAAqB,CAAC;YAC5C,QAAQ,EAAE,cAAc,CAAC,QAAQ;YACjC,KAAK,EAAE,cAAc,CAAC,KAAK;YAC3B,cAAc,EAAE,KAAK,CAAC,cAAc;YACpC,KAAK,EAAE,KAAK,CAAC,KAAK;SACnB,CAAC,CAAC;QACH,IAAI,eAAe,KAAK,cAAc,CAAC,iBAAiB,EAAE,CAAC;YACzD,oEAAoE;YACpE,+CAA+C;YAC/C,MAAM,UAAU,GAAG,0BAA0B,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;YAC3E,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,CAAC,YAAY,GAAG,UAAU,CAAC,CAAC;YAC3E,gEAAgE;YAChE,8DAA8D;YAC9D,gEAAgE;YAChE,qEAAqE;YACrE,IAAI,eAAe,GAAG,CAAC,EAAE,CAAC;gBACxB,YAAY,GAAG,eAAe,CAAC;gBAC/B,cAAc,GAAG,IAAI,CAAC;YACxB,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,qBAAqB,GAAG,YAAY,GAAG,yBAAyB,CAAC;IACvE,MAAM,cAAc,GAAG,KAAK,CAAC,oBAAoB,IAAI,CAAC,CAAC;IACvD,MAAM,qBAAqB,GAAG,qBAAqB,GAAG,cAAc,CAAC;IAErE,OAAO;QACL,qBAAqB;QACrB,qBAAqB;QACrB,cAAc;QACd,qBAAqB;QACrB,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC,YAAY,CAAC;QAClE,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,cAAc;KACzC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * requestSnapshotStore — in-memory per-session store of routed request
3
+ * snapshots (v1.36.0 context/cache upgrade).
4
+ *
5
+ * Keeps the LAST snapshot per session (that's all cache-aware compaction
6
+ * needs: the most recent routed request = the warmest provider prefix)
7
+ * plus the provider-reported usage that answered it.
8
+ *
9
+ * Usage flow:
10
+ * - AgentHarness `onRequestSnapshot` → `recordRequestSnapshot(sessionId, s)`
11
+ * - `message_end` usage in useChatTurn → `recordRequestUsage(sessionId, usage)`
12
+ * (associated to the snapshot that is currently pending for that session)
13
+ * - `applyBudgetPolicyAsync` → `getRequestSnapshot(sessionId)` → replay base
14
+ * - `/clear` | `/new` → `clearRequestSnapshots(sessionId)` /
15
+ * `clearAllRequestSnapshots()` (CLI is single-session per process).
16
+ *
17
+ * @since v1.36.0
18
+ */
19
+ const store = new Map();
20
+ /** Track the most recently routed request for a session. */
21
+ export function recordRequestSnapshot(sessionId, snapshot) {
22
+ // Keep the pending slot: usage recorded next will bind to this snapshot.
23
+ store.set(sessionId, { snapshot });
24
+ }
25
+ /**
26
+ * Attach provider-reported usage to the session's latest snapshot.
27
+ * Called when the `message_end` usage delta lands — after the snapshot.
28
+ */
29
+ export function recordRequestUsage(sessionId, usage) {
30
+ const entry = store.get(sessionId);
31
+ if (!entry)
32
+ return;
33
+ entry.usage = usage;
34
+ }
35
+ /** Last routed snapshot (+ usage, when reported). Null when none yet. */
36
+ export function getRequestSnapshot(sessionId) {
37
+ return store.get(sessionId)?.snapshot ?? null;
38
+ }
39
+ /** Last snapshot together with its provider-reported usage. */
40
+ export function getRequestSnapshotWithUsage(sessionId) {
41
+ return store.get(sessionId) ?? null;
42
+ }
43
+ /** Drop one session's snapshot (session switch). */
44
+ export function clearRequestSnapshots(sessionId) {
45
+ store.delete(sessionId);
46
+ }
47
+ /** Drop everything (/clear, /new — the CLI is mono-session per process). */
48
+ export function clearAllRequestSnapshots() {
49
+ store.clear();
50
+ }
51
+ /** Test-only reset. */
52
+ export function _resetRequestSnapshotStoreForTests() {
53
+ store.clear();
54
+ }
55
+ //# sourceMappingURL=requestSnapshotStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestSnapshotStore.js","sourceRoot":"","sources":["../../../src/cli/budget/requestSnapshotStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAkBH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAC;AAE9C,4DAA4D;AAC5D,MAAM,UAAU,qBAAqB,CACnC,SAAiB,EACjB,QAA+B;IAE/B,yEAAyE;IACzE,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,SAAiB,EACjB,KAAyB;IAEzB,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACnC,IAAI,CAAC,KAAK;QAAE,OAAO;IACnB,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;AACtB,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,kBAAkB,CAAC,SAAiB;IAClD,OAAO,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,QAAQ,IAAI,IAAI,CAAC;AAChD,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,2BAA2B,CACzC,SAAiB;IAEjB,OAAO,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC;AACtC,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,qBAAqB,CAAC,SAAiB;IACrD,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AAC1B,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,wBAAwB;IACtC,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC;AAED,uBAAuB;AACvB,MAAM,UAAU,kCAAkC;IAChD,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC"}