@dsh-cc/context-crusher 0.7.1 → 0.8.0-rc.2

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.
Files changed (53) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +4 -0
  3. package/README.zh.md +4 -0
  4. package/lib/config.d.ts +9 -1
  5. package/lib/config.d.ts.map +1 -1
  6. package/lib/config.js +114 -1
  7. package/lib/config.js.map +1 -1
  8. package/lib/defer/counter.d.ts +44 -0
  9. package/lib/defer/counter.d.ts.map +1 -0
  10. package/lib/defer/counter.js +81 -0
  11. package/lib/defer/counter.js.map +1 -0
  12. package/lib/defer/gate.d.ts +41 -0
  13. package/lib/defer/gate.d.ts.map +1 -0
  14. package/lib/defer/gate.js +32 -0
  15. package/lib/defer/gate.js.map +1 -0
  16. package/lib/defer/pass.d.ts +70 -0
  17. package/lib/defer/pass.d.ts.map +1 -0
  18. package/lib/defer/pass.js +288 -0
  19. package/lib/defer/pass.js.map +1 -0
  20. package/lib/defer/residents.d.ts +126 -0
  21. package/lib/defer/residents.d.ts.map +1 -0
  22. package/lib/defer/residents.js +171 -0
  23. package/lib/defer/residents.js.map +1 -0
  24. package/lib/defer/swap.d.ts +57 -0
  25. package/lib/defer/swap.d.ts.map +1 -0
  26. package/lib/defer/swap.js +120 -0
  27. package/lib/defer/swap.js.map +1 -0
  28. package/lib/fold-counters.d.ts +29 -0
  29. package/lib/fold-counters.d.ts.map +1 -0
  30. package/lib/fold-counters.js +62 -0
  31. package/lib/fold-counters.js.map +1 -0
  32. package/lib/index.d.ts +20 -0
  33. package/lib/index.d.ts.map +1 -1
  34. package/lib/index.js +86 -15
  35. package/lib/index.js.map +1 -1
  36. package/lib/receipt.d.ts +45 -0
  37. package/lib/receipt.d.ts.map +1 -0
  38. package/lib/receipt.js +73 -0
  39. package/lib/receipt.js.map +1 -0
  40. package/lib/reducer.d.ts +52 -0
  41. package/lib/reducer.d.ts.map +1 -0
  42. package/lib/reducer.js +202 -0
  43. package/lib/reducer.js.map +1 -0
  44. package/lib/settings.d.ts.map +1 -1
  45. package/lib/settings.js +6 -3
  46. package/lib/settings.js.map +1 -1
  47. package/lib/types.d.ts +52 -1
  48. package/lib/types.d.ts.map +1 -1
  49. package/lib/verifier.d.ts +29 -0
  50. package/lib/verifier.d.ts.map +1 -0
  51. package/lib/verifier.js +57 -0
  52. package/lib/verifier.js.map +1 -0
  53. package/package.json +26 -13
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Deferred-mode orchestration (design §3): the `llm/stream` first-chunk
3
+ * counter, the `agent/pre-step` swap pass, resume-by-fingerprint, and the
4
+ * per-session ledger plumbing. Extracted from the crusher service to keep
5
+ * `index.ts` inside the file-size budget; the crusher wires one instance
6
+ * and forwards its post-execute hooks.
7
+ *
8
+ * @module @dsh-cc/context-crusher/defer/pass
9
+ */
10
+ import { getSessionCwd } from '@dsh-cc/session-cwd';
11
+ import { buildMarker } from "../marker.js";
12
+ import { route } from "../router.js";
13
+ import { shortHash } from "../store.js";
14
+ import { shouldCountRequest, toolResultFingerprints, wrapStreamWithCounting } from "./counter.js";
15
+ import { DeferLedger, ResidentTable, parseTodoCounts, readDeferLedger, rebuildResidents, } from "./residents.js";
16
+ import { attemptSwap } from "./swap.js";
17
+ /**
18
+ * Owns all deferred-externalization state and listeners. Every failure
19
+ * degrades to a log line: observation must never break a request or a turn.
20
+ */
21
+ export class Deferral {
22
+ deps;
23
+ table = new ResidentTable();
24
+ ledgers = new Map();
25
+ constructor(deps) {
26
+ this.deps = deps;
27
+ }
28
+ /**
29
+ * Deferred-mode seams (design §3): an `llm/stream` first-chunk counter
30
+ * (`{ global: true, prepend: true }` — the crusher runs in its own realm),
31
+ * an `agent/pre-step` swap pass, and state cleanup on session disposal.
32
+ */
33
+ registerListeners() {
34
+ const { ctx } = this.deps;
35
+ ctx.on('llm/stream', (options, next) => wrapStreamWithCounting(options, next(), (opts) => this.observeStreamFirstChunk(opts)), { global: true, prepend: true });
36
+ ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
37
+ try {
38
+ await this.preStepPass(agent, signal);
39
+ }
40
+ catch (error) {
41
+ // Never break the turn over observability/compaction bookkeeping.
42
+ ctx.logger.warn(`context-crusher: deferred pre-step pass failed: ${String(error)}`);
43
+ }
44
+ return next();
45
+ });
46
+ ctx.on('session/disposed', (session) => {
47
+ this.table.delete(String(session.id));
48
+ });
49
+ }
50
+ /**
51
+ * First-chunk send counter (§3.2): one send per resident whose stored full
52
+ * text fingerprint appears in the outbound prompt. Main-loop purpose
53
+ * allowlist + owning-session guard live in {@link shouldCountRequest}.
54
+ */
55
+ observeStreamFirstChunk(options) {
56
+ try {
57
+ if (!shouldCountRequest(options, this.table.sessionIds()))
58
+ return;
59
+ const state = this.table.get(String(options.sessionId));
60
+ if (state === undefined)
61
+ return;
62
+ state.streamRequestCount += 1;
63
+ const fingerprints = toolResultFingerprints(options.messages);
64
+ for (const entry of state.residents.values()) {
65
+ if (fingerprints.has(entry.hash))
66
+ entry.sentCount += 1;
67
+ }
68
+ }
69
+ catch (error) {
70
+ this.deps.ctx.logger.warn(`context-crusher: deferred send counting failed: ${String(error)}`);
71
+ }
72
+ }
73
+ /**
74
+ * One pre-step pass (§3.3): sweep aged residents, then evaluate every
75
+ * entry whose sent count crossed the residency threshold in one
76
+ * non-awaiting critical section per entry. No timers exist — eligibility
77
+ * is recomputed at this boundary. A failing entry never aborts the others
78
+ * (microcompact failure ordering).
79
+ */
80
+ async preStepPass(agent, signal) {
81
+ if (signal.aborted)
82
+ return;
83
+ const cfg = this.deps.effectiveConfig();
84
+ const store = this.deps.store();
85
+ const home = this.deps.home();
86
+ if (!cfg.enabled || cfg.deferRequests <= 0 || store === undefined || home === undefined)
87
+ return;
88
+ const session = agent.session;
89
+ const sessionId = String(session.id);
90
+ await this.ensureLoaded(sessionId, agent);
91
+ const state = this.table.get(sessionId);
92
+ if (state === undefined || state.residents.size === 0)
93
+ return;
94
+ const ledger = this.ledgerFor(sessionId);
95
+ const pendingRows = [];
96
+ const swapRow = (entry, outcome, applied, gate) => {
97
+ const row = ledger?.append({
98
+ ts: new Date().toISOString(),
99
+ type: 'swap',
100
+ hash: entry.hash,
101
+ callId: String(entry.callId),
102
+ outcome,
103
+ applied,
104
+ ...(gate === undefined ? {} : { gate }),
105
+ });
106
+ if (row !== undefined)
107
+ pendingRows.push(row);
108
+ };
109
+ const now = Date.now();
110
+ // Age sweep first: decks of stale residents cannot accumulate unboundedly.
111
+ for (const entry of [...state.residents.values()]) {
112
+ if (now - entry.createdAt > cfg.deferMaxAgeMs) {
113
+ state.residents.delete(entry.hash);
114
+ swapRow(entry, 'abandoned', false, { sentCount: entry.sentCount });
115
+ }
116
+ }
117
+ let projectKey;
118
+ try {
119
+ projectKey = shortHash(getSessionCwd(agent));
120
+ }
121
+ catch {
122
+ return;
123
+ }
124
+ // Window-pressure inputs are resolved once per pass and only when the
125
+ // override key is set (measure is O(surface)); without a window source
126
+ // the override stays inactive.
127
+ let sessionTokens;
128
+ let contextWindow;
129
+ if (cfg.deferUrgencyTokens !== undefined) {
130
+ sessionTokens = this.deps.ctx.tokenMeter.measure(session).totalTokens;
131
+ contextWindow = this.contextWindowOf(session);
132
+ }
133
+ for (const entry of [...state.residents.values()]) {
134
+ // Deviation flagged in review: doc §3.3 says `sentCount > residency`,
135
+ // but the title/§5 ("send twice, swap on the third request") and the
136
+ // approved test plan require `>=` — implemented as `>=`.
137
+ if (entry.sentCount < cfg.deferRequests)
138
+ continue;
139
+ try {
140
+ // All I/O happens BEFORE the synchronous critical section.
141
+ const stored = await store.get(projectKey, entry.hash);
142
+ const candidate = stored.ok ? route(stored.text) : null;
143
+ if (!stored.ok || candidate === null) {
144
+ state.residents.delete(entry.hash);
145
+ swapRow(entry, 'stale', false, { sentCount: entry.sentCount });
146
+ continue;
147
+ }
148
+ const tokensBefore = this.estimate(stored.text);
149
+ const tokensAfter = this.estimate(candidate.text);
150
+ const stubText = `${candidate.text}\n${buildMarker(tokensBefore, tokensAfter, entry.hash)}`;
151
+ // ↓ synchronous critical section (staleness re-check + two appends,
152
+ // no yields — an in-flight request cannot interleave with a swap).
153
+ const result = attemptSwap({
154
+ session,
155
+ entry,
156
+ fullText: stored.text,
157
+ stubText,
158
+ apply: cfg.mode === 'on',
159
+ margin: cfg.deferMargin,
160
+ remainingRequestsEstimate: this.table.remainingRequestsEstimate(state),
161
+ ...(cfg.deferUrgencyTokens !== undefined ? { urgencyTokens: cfg.deferUrgencyTokens } : {}),
162
+ ...(sessionTokens !== undefined ? { sessionTokens } : {}),
163
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
164
+ estimateMessage: (message) => this.deps.ctx.tokenMeter.estimateMessage(message),
165
+ });
166
+ if (result.outcome === 'applied') {
167
+ state.residents.delete(entry.hash);
168
+ swapRow(entry, 'applied', true, result.gate);
169
+ }
170
+ else if (result.outcome === 'stale') {
171
+ state.residents.delete(entry.hash);
172
+ swapRow(entry, 'stale', false, { sentCount: entry.sentCount });
173
+ }
174
+ else if (result.outcome === 'dry-run') {
175
+ // §3.6: intent row carries applied:false; the entry stays resident.
176
+ swapRow(entry, 'dry-run', false, result.gate);
177
+ }
178
+ // gate-failed: the entry stays resident; re-evaluation is cheap and
179
+ // happens at every pre-step.
180
+ }
181
+ catch (error) {
182
+ this.deps.ctx.logger.warn(`context-crusher: deferred swap failed for ${entry.hash}: ${String(error)}`);
183
+ }
184
+ }
185
+ // Ledger rows are best-effort but awaited here so the pass is durable
186
+ // before the turn's request fires (still never throws — DeferLedger
187
+ // swallows I/O errors internally).
188
+ await Promise.all(pendingRows);
189
+ }
190
+ /** Record one resident entry (post-execute, deferral applies). */
191
+ async recordResident(exec, hash, tokensSaved) {
192
+ const agent = exec.agent;
193
+ const home = this.deps.home();
194
+ if (agent === undefined || home === undefined)
195
+ return;
196
+ const sessionId = String(agent.session.id);
197
+ const entry = {
198
+ hash,
199
+ callId: exec.callId,
200
+ sentCount: 0,
201
+ tokensSaved,
202
+ createdAt: Date.now(),
203
+ };
204
+ this.table.ensure(sessionId).residents.set(hash, entry);
205
+ await this.ledgerFor(sessionId)?.append({
206
+ ts: new Date().toISOString(),
207
+ type: 'resident',
208
+ hash,
209
+ callId: String(exec.callId),
210
+ tokensSaved,
211
+ createdAt: entry.createdAt,
212
+ });
213
+ }
214
+ /** Latest `todo_write` snapshot → the remaining-requests estimate inputs. */
215
+ observeTodoSnapshot(exec) {
216
+ if (exec.agent === undefined)
217
+ return;
218
+ const counts = parseTodoCounts(exec.arguments);
219
+ if (counts === undefined)
220
+ return;
221
+ const state = this.table.ensure(String(exec.agent.session.id));
222
+ state.todoCompleted = counts.completed;
223
+ state.todoPending = counts.pending;
224
+ }
225
+ /**
226
+ * Resume rebuild (§3.5), once per session: replay the per-session ledger
227
+ * and re-adopt entries whose stored text fingerprints the live surface.
228
+ * Session-id keying alone would strand residents across resume/fork, so
229
+ * the fingerprint — not the row — is the identity.
230
+ */
231
+ async ensureLoaded(sessionId, agent) {
232
+ const state = this.table.ensure(sessionId);
233
+ const store = this.deps.store();
234
+ const home = this.deps.home();
235
+ if (state.loaded || store === undefined || home === undefined)
236
+ return;
237
+ state.loaded = true;
238
+ try {
239
+ const rows = await readDeferLedger(home('ccr', 'defer', `${sessionId}.jsonl`));
240
+ if (rows.length === 0)
241
+ return;
242
+ const rebuilt = await rebuildResidents({
243
+ rows,
244
+ store,
245
+ projectKey: shortHash(getSessionCwd(agent)),
246
+ session: agent.session,
247
+ });
248
+ for (const [hash, entry] of rebuilt)
249
+ state.residents.set(hash, entry);
250
+ }
251
+ catch (error) {
252
+ this.deps.ctx.logger.warn(`context-crusher: deferred resume rebuild failed: ${String(error)}`);
253
+ }
254
+ }
255
+ /** Per-session defer ledger (constructed lazily; all writes never throw). */
256
+ ledgerFor(sessionId) {
257
+ const home = this.deps.home();
258
+ if (home === undefined)
259
+ return undefined;
260
+ let ledger = this.ledgers.get(sessionId);
261
+ if (ledger === undefined) {
262
+ ledger = new DeferLedger(home('ccr', 'defer', `${sessionId}.jsonl`));
263
+ this.ledgers.set(sessionId, ledger);
264
+ }
265
+ return ledger;
266
+ }
267
+ /** Routed model context window via the token-meter pressure projection; absent = no window source. */
268
+ contextWindowOf(session) {
269
+ try {
270
+ const projections = this.deps.ctx.get('sessionProjections');
271
+ const state = projections?.stateOf(session, 'contextPressure');
272
+ return typeof state?.contextWindow === 'number' && state.contextWindow > 0
273
+ ? state.contextWindow
274
+ : undefined;
275
+ }
276
+ catch {
277
+ return undefined;
278
+ }
279
+ }
280
+ /** Token-based sizing, same estimator as the crusher's gates and markers. */
281
+ estimate(text) {
282
+ return this.deps.ctx.tokenMeter.estimateMessage({
283
+ role: 'tool',
284
+ content: [{ type: 'text', text }],
285
+ });
286
+ }
287
+ }
288
+ //# sourceMappingURL=pass.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pass.js","sourceRoot":"","sources":["../../src/defer/pass.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Deferred-resident bookkeeping: the in-memory per-session table, the
3
+ * append-only per-session JSONL ledger (`<dshHome>/ccr/defer/<sessionId>.jsonl`),
4
+ * and resume-by-fingerprint rebuild (design §3.1/§3.5).
5
+ *
6
+ * Rows carry NO session id — the store hash is the identity, which matches
7
+ * the store's own keying rule and keeps rows meaningful across resume/fork.
8
+ * Ledger failures are swallowed per the CCR ledger precedent: durability is
9
+ * best-effort, correctness never depends on it.
10
+ *
11
+ * @module @dsh-cc/context-crusher/defer/residents
12
+ */
13
+ import { ToolCallId } from '@deepseek-ai/dsh-llm';
14
+ import type { Session } from '@deepseek-ai/dsh-session';
15
+ import type { CrusherStore } from '../store.ts';
16
+ /** One resident: stored immediately, sent unmodified, swapped after residency. */
17
+ export interface ResidentEntry {
18
+ readonly hash: string;
19
+ /** Sole locator — the surface event is resolved by callId at pre-step time. */
20
+ readonly callId: ToolCallId;
21
+ /** Counted main-loop sends that included the full text (first-chunk rule). */
22
+ sentCount: number;
23
+ /** tokensBefore − tokensAfter, measured at store time. */
24
+ readonly tokensSaved: number;
25
+ /** Epoch ms when the resident was recorded (sweep reference). */
26
+ readonly createdAt: number;
27
+ }
28
+ /** Row recording that the original was stored and the result sent unmodified. */
29
+ export interface DeferResidentRow {
30
+ readonly ts: string;
31
+ readonly type: 'resident';
32
+ readonly hash: string;
33
+ readonly callId: string;
34
+ readonly tokensSaved: number;
35
+ readonly createdAt: number;
36
+ }
37
+ /** Gate inputs at decision time, recorded with every swap row for the dogfood study. */
38
+ export interface DeferGateFacts {
39
+ readonly sentCount: number;
40
+ readonly suffixTokens?: number;
41
+ readonly projectedSavings?: number;
42
+ readonly margin?: number;
43
+ readonly remainingRequestsEstimate?: number;
44
+ readonly urgencyOverride?: boolean;
45
+ }
46
+ /** Row recording a swap decision: applied, stale drop, age sweep, or dry-run intent. */
47
+ export interface DeferSwapRow {
48
+ readonly ts: string;
49
+ readonly type: 'swap';
50
+ readonly hash: string;
51
+ readonly callId: string;
52
+ readonly outcome: 'applied' | 'stale' | 'abandoned' | 'dry-run';
53
+ /** §3.6: dry-run intent rows carry `applied: false`. */
54
+ readonly applied: boolean;
55
+ readonly gate?: DeferGateFacts;
56
+ }
57
+ export type DeferLedgerRow = DeferResidentRow | DeferSwapRow;
58
+ /** Append-only per-session defer ledger; never throws. */
59
+ export declare class DeferLedger {
60
+ private readonly filePath;
61
+ constructor(filePath: string);
62
+ /** Append one row; never throws (best-effort observability). */
63
+ append(row: DeferLedgerRow): Promise<void>;
64
+ }
65
+ /** Read every ledger row; absent/corrupt files degrade to an empty list. */
66
+ export declare function readDeferLedger(filePath: string): Promise<DeferLedgerRow[]>;
67
+ /**
68
+ * Replay ledger rows into resident candidates: resident rows add, swap rows
69
+ * with a committed outcome (applied/stale/abandoned) remove, dry-run intent
70
+ * rows leave the resident in place (nothing was applied). Candidates always
71
+ * resume at `sentCount: 0` (§3.5: deliberately conservative).
72
+ */
73
+ export declare function foldDeferRows(rows: readonly DeferLedgerRow[]): Map<string, ResidentEntry>;
74
+ export interface RebuildDeps {
75
+ readonly rows: readonly DeferLedgerRow[];
76
+ readonly store: CrusherStore;
77
+ readonly projectKey: string;
78
+ readonly session: Session;
79
+ }
80
+ /**
81
+ * Rebuild residents after a restart by FINGERPRINT MATCH against the live
82
+ * surface (§3.5): for each recorded hash the stored full text is searched
83
+ * among current `tool/result` events (by callId, then byte-equality); found
84
+ * → resume at `sentCount: 0`; not found → drop.
85
+ */
86
+ export declare function rebuildResidents(deps: RebuildDeps): Promise<Map<string, ResidentEntry>>;
87
+ /** Latest `todo_write` snapshot counts for the remaining-requests estimate. */
88
+ export interface TodoCounts {
89
+ readonly completed: number;
90
+ readonly pending: number;
91
+ }
92
+ /**
93
+ * Parse the `todos` array out of `todo_write` arguments (observe-only; the
94
+ * same argument shape the compaction cost gate reads).
95
+ */
96
+ export declare function parseTodoCounts(args: unknown): TodoCounts | undefined;
97
+ /** Per-session defer state (in memory only; never ledgered). */
98
+ export interface DeferSessionState {
99
+ /** Resume rebuild ran (or was attempted) for this session. */
100
+ loaded: boolean;
101
+ readonly residents: Map<string, ResidentEntry>;
102
+ /** Main-loop first-chunk requests observed for this session. */
103
+ streamRequestCount: number;
104
+ /** Latest `todo_write` snapshot counts (cold start: zeroes). */
105
+ todoCompleted: number;
106
+ todoPending: number;
107
+ }
108
+ /**
109
+ * In-memory per-session resident table. The map key IS the owning session id
110
+ * the stream listener's sessionId guard checks against (§3.2).
111
+ */
112
+ export declare class ResidentTable {
113
+ private readonly states;
114
+ get(sessionId: string): DeferSessionState | undefined;
115
+ ensure(sessionId: string): DeferSessionState;
116
+ delete(sessionId: string): void;
117
+ /** Every session id currently holding defer state (stream-listener guard). */
118
+ sessionIds(): Set<string>;
119
+ /**
120
+ * `remainingRequestsEstimate` per the compaction cost-gate doc: observed
121
+ * requests-per-step × pending todo steps, floored at 1. With no todo
122
+ * snapshot ever seen the estimate is 1 — conservative against swapping.
123
+ */
124
+ remainingRequestsEstimate(state: DeferSessionState): number;
125
+ }
126
+ //# sourceMappingURL=residents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"residents.d.ts","sourceRoot":"","sources":["../../src/defer/residents.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Deferred-resident bookkeeping: the in-memory per-session table, the
3
+ * append-only per-session JSONL ledger (`<dshHome>/ccr/defer/<sessionId>.jsonl`),
4
+ * and resume-by-fingerprint rebuild (design §3.1/§3.5).
5
+ *
6
+ * Rows carry NO session id — the store hash is the identity, which matches
7
+ * the store's own keying rule and keeps rows meaningful across resume/fork.
8
+ * Ledger failures are swallowed per the CCR ledger precedent: durability is
9
+ * best-effort, correctness never depends on it.
10
+ *
11
+ * @module @dsh-cc/context-crusher/defer/residents
12
+ */
13
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
14
+ import { dirname } from 'node:path';
15
+ import { ToolCallId } from '@deepseek-ai/dsh-llm';
16
+ import { joinTextBlocks } from "./counter.js";
17
+ /** Append-only per-session defer ledger; never throws. */
18
+ export class DeferLedger {
19
+ filePath;
20
+ constructor(filePath) {
21
+ this.filePath = filePath;
22
+ }
23
+ /** Append one row; never throws (best-effort observability). */
24
+ async append(row) {
25
+ try {
26
+ await mkdir(dirname(this.filePath), { recursive: true });
27
+ await appendFile(this.filePath, `${JSON.stringify(row)}\n`, 'utf8');
28
+ }
29
+ catch {
30
+ // Best-effort observability; never surface into the agent loop.
31
+ }
32
+ }
33
+ }
34
+ /** Read every ledger row; absent/corrupt files degrade to an empty list. */
35
+ export async function readDeferLedger(filePath) {
36
+ let body;
37
+ try {
38
+ body = await readFile(filePath, 'utf8');
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ const rows = [];
44
+ for (const line of body.split('\n')) {
45
+ if (line.trim().length === 0)
46
+ continue;
47
+ try {
48
+ rows.push(JSON.parse(line));
49
+ }
50
+ catch {
51
+ // Skip a torn tail line; the ledger is append-only and best-effort.
52
+ }
53
+ }
54
+ return rows;
55
+ }
56
+ /**
57
+ * Replay ledger rows into resident candidates: resident rows add, swap rows
58
+ * with a committed outcome (applied/stale/abandoned) remove, dry-run intent
59
+ * rows leave the resident in place (nothing was applied). Candidates always
60
+ * resume at `sentCount: 0` (§3.5: deliberately conservative).
61
+ */
62
+ export function foldDeferRows(rows) {
63
+ const out = new Map();
64
+ for (const row of rows) {
65
+ if (row.type === 'resident') {
66
+ out.set(row.hash, {
67
+ hash: row.hash,
68
+ callId: ToolCallId(row.callId),
69
+ sentCount: 0,
70
+ tokensSaved: row.tokensSaved,
71
+ createdAt: row.createdAt,
72
+ });
73
+ }
74
+ else if (row.outcome !== 'dry-run') {
75
+ out.delete(row.hash);
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+ /**
81
+ * Rebuild residents after a restart by FINGERPRINT MATCH against the live
82
+ * surface (§3.5): for each recorded hash the stored full text is searched
83
+ * among current `tool/result` events (by callId, then byte-equality); found
84
+ * → resume at `sentCount: 0`; not found → drop.
85
+ */
86
+ export async function rebuildResidents(deps) {
87
+ const out = new Map();
88
+ for (const candidate of foldDeferRows(deps.rows).values()) {
89
+ const stored = await deps.store.get(deps.projectKey, candidate.hash);
90
+ if (!stored.ok)
91
+ continue;
92
+ for (const seq of deps.session.surface.nodes) {
93
+ const event = deps.session.eventAt(seq);
94
+ if (event?.type !== 'tool/result')
95
+ continue;
96
+ const message = event.data.message;
97
+ if (String(message.source.callId) !== String(candidate.callId))
98
+ continue;
99
+ const block = message.content[0];
100
+ if (block?.type !== 'tool-result')
101
+ continue;
102
+ if (joinTextBlocks(block.content) === stored.text) {
103
+ out.set(candidate.hash, candidate);
104
+ break;
105
+ }
106
+ }
107
+ }
108
+ return out;
109
+ }
110
+ const TODO_STATUSES = ['pending', 'in_progress', 'completed'];
111
+ /**
112
+ * Parse the `todos` array out of `todo_write` arguments (observe-only; the
113
+ * same argument shape the compaction cost gate reads).
114
+ */
115
+ export function parseTodoCounts(args) {
116
+ if (typeof args !== 'object' || args === null)
117
+ return undefined;
118
+ const todos = args.todos;
119
+ if (!Array.isArray(todos))
120
+ return undefined;
121
+ let completed = 0;
122
+ let pending = 0;
123
+ for (const todo of todos) {
124
+ if (typeof todo !== 'object' || todo === null)
125
+ return undefined;
126
+ const item = todo;
127
+ if (typeof item.content !== 'string' || typeof item.status !== 'string'
128
+ || !TODO_STATUSES.includes(item.status))
129
+ return undefined;
130
+ if (item.status === 'completed')
131
+ completed += 1;
132
+ else
133
+ pending += 1;
134
+ }
135
+ return { completed, pending };
136
+ }
137
+ /**
138
+ * In-memory per-session resident table. The map key IS the owning session id
139
+ * the stream listener's sessionId guard checks against (§3.2).
140
+ */
141
+ export class ResidentTable {
142
+ states = new Map();
143
+ get(sessionId) {
144
+ return this.states.get(sessionId);
145
+ }
146
+ ensure(sessionId) {
147
+ let state = this.states.get(sessionId);
148
+ if (state === undefined) {
149
+ state = { loaded: false, residents: new Map(), streamRequestCount: 0, todoCompleted: 0, todoPending: 0 };
150
+ this.states.set(sessionId, state);
151
+ }
152
+ return state;
153
+ }
154
+ delete(sessionId) {
155
+ this.states.delete(sessionId);
156
+ }
157
+ /** Every session id currently holding defer state (stream-listener guard). */
158
+ sessionIds() {
159
+ return new Set(this.states.keys());
160
+ }
161
+ /**
162
+ * `remainingRequestsEstimate` per the compaction cost-gate doc: observed
163
+ * requests-per-step × pending todo steps, floored at 1. With no todo
164
+ * snapshot ever seen the estimate is 1 — conservative against swapping.
165
+ */
166
+ remainingRequestsEstimate(state) {
167
+ const requestsPerStep = Math.max(1, state.streamRequestCount / Math.max(state.todoCompleted, 1));
168
+ return Math.max(1, requestsPerStep * state.todoPending);
169
+ }
170
+ }
171
+ //# sourceMappingURL=residents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"residents.js","sourceRoot":"","sources":["../../src/defer/residents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAGjD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAiD7C,0DAA0D;AAC1D,MAAM,OAAO,WAAW;IACO;IAA7B,YAA6B,QAAgB;QAAhB,aAAQ,GAAR,QAAQ,CAAQ;IAAG,CAAC;IAEjD,gEAAgE;IAChE,KAAK,CAAC,MAAM,CAAC,GAAmB;QAC9B,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YACxD,MAAM,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QACrE,CAAC;QAAC,MAAM,CAAC;YACP,gEAAgE;QAClE,CAAC;IACH,CAAC;CACF;AAED,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAgB;IACpD,IAAI,IAAY,CAAA;IAChB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;IACD,MAAM,IAAI,GAAqB,EAAE,CAAA;IACjC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;YAAE,SAAQ;QACtC,IAAI,CAAC;YACH,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAmB,CAAC,CAAA;QAC/C,CAAC;QAAC,MAAM,CAAC;YACP,oEAAoE;QACtE,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAA+B;IAC3D,MAAM,GAAG,GAAG,IAAI,GAAG,EAAyB,CAAA;IAC5C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC5B,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE;gBAChB,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC;gBAC9B,SAAS,EAAE,CAAC;gBACZ,WAAW,EAAE,GAAG,CAAC,WAAW;gBAC5B,SAAS,EAAE,GAAG,CAAC,SAAS;aACzB,CAAC,CAAA;QACJ,CAAC;aAAM,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YACrC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACtB,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AASD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAiB;IACtD,MAAM,GAAG,GAAG,IAAI,GAAG,EAAyB,CAAA;IAC5C,KAAK,MAAM,SAAS,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;QAC1D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;QACpE,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,SAAQ;QACxB,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;YACvC,IAAI,KAAK,EAAE,IAAI,KAAK,aAAa;gBAAE,SAAQ;YAC3C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAA;YAClC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;gBAAE,SAAQ;YACxE,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;YAChC,IAAI,KAAK,EAAE,IAAI,KAAK,aAAa;gBAAE,SAAQ;YAC3C,IAAI,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC;gBAClD,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;gBAClC,MAAK;YACP,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAQD,MAAM,aAAa,GAAsB,CAAC,SAAS,EAAE,aAAa,EAAE,WAAW,CAAC,CAAA;AAEhF;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAa;IAC3C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,SAAS,CAAA;IAC/D,MAAM,KAAK,GAAI,IAA4B,CAAC,KAAK,CAAA;IACjD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAC3C,IAAI,SAAS,GAAG,CAAC,CAAA;IACjB,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,SAAS,CAAA;QAC/D,MAAM,IAAI,GAAG,IAA+C,CAAA;QAC5D,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ;eAClE,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,OAAO,SAAS,CAAA;QAC3D,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW;YAAE,SAAS,IAAI,CAAC,CAAA;;YAC1C,OAAO,IAAI,CAAC,CAAA;IACnB,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAA;AAC/B,CAAC;AAcD;;;GAGG;AACH,MAAM,OAAO,aAAa;IACP,MAAM,GAAG,IAAI,GAAG,EAA6B,CAAA;IAE9D,GAAG,CAAC,SAAiB;QACnB,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACnC,CAAC;IAED,MAAM,CAAC,SAAiB;QACtB,IAAI,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QACtC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,KAAK,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,EAAE,kBAAkB,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAA;YACxG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QACnC,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,CAAC,SAAiB;QACtB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAC/B,CAAC;IAED,8EAA8E;IAC9E,UAAU;QACR,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;IACpC,CAAC;IAED;;;;OAIG;IACH,yBAAyB,CAAC,KAAwB;QAChD,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,CAAA;QAChG,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,eAAe,GAAG,KAAK,CAAC,WAAW,CAAC,CAAA;IACzD,CAAC;CACF"}