larkin 0.4.1 → 0.4.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.
@@ -0,0 +1,623 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PiCompactionRecoveryMachine = exports.PiCompactionBreaker = exports.PI_COMPACTION_RECOVERY_FILE = exports.PI_COMPACTION_RECOVERY_VERSION = exports.PI_COMPACTION_THRESHOLD = exports.COMPACTION_KEEP_RECENT_TOKENS = exports.COMPACTION_RESERVE_TOKENS = exports.PI_CONTEXT_WINDOW = void 0;
7
+ exports.isPiNativeCompactionRequired = isPiNativeCompactionRequired;
8
+ exports.assertEffectivePiCompactionSettings = assertEffectivePiCompactionSettings;
9
+ exports.hasProjectPiCompactionOverride = hasProjectPiCompactionOverride;
10
+ exports.ensureOwnedPiAgentDirectory = ensureOwnedPiAgentDirectory;
11
+ exports.ownedPiSettings = ownedPiSettings;
12
+ exports.prepareOwnedPiDirectory = prepareOwnedPiDirectory;
13
+ exports.writeOwnedPiSettings = writeOwnedPiSettings;
14
+ exports.readOwnedPiSettings = readOwnedPiSettings;
15
+ exports.verifyPiCapabilities = verifyPiCapabilities;
16
+ exports.prepareOwnedPiEnvironment = prepareOwnedPiEnvironment;
17
+ exports.readProjectPiSettings = readProjectPiSettings;
18
+ exports.assertNoProjectPiCompactionOverride = assertNoProjectPiCompactionOverride;
19
+ exports.currentUserLabel = currentUserLabel;
20
+ const node_crypto_1 = __importDefault(require("node:crypto"));
21
+ const node_fs_1 = __importDefault(require("node:fs"));
22
+ const node_os_1 = __importDefault(require("node:os"));
23
+ const node_path_1 = __importDefault(require("node:path"));
24
+ exports.PI_CONTEXT_WINDOW = 272_000;
25
+ exports.COMPACTION_RESERVE_TOKENS = 40_800;
26
+ exports.COMPACTION_KEEP_RECENT_TOKENS = 20_000;
27
+ exports.PI_COMPACTION_THRESHOLD = exports.PI_CONTEXT_WINDOW - exports.COMPACTION_RESERVE_TOKENS;
28
+ exports.PI_COMPACTION_RECOVERY_VERSION = 1;
29
+ exports.PI_COMPACTION_RECOVERY_FILE = "piCompactionRecovery.json";
30
+ const AGENT_ID = /^cli_[A-Za-z0-9]+$/;
31
+ const REQUIRED_PI_EVENTS = ["compaction_start", "compaction_end", "agent_end", "agent_settled"];
32
+ const TERMINAL_STATES = new Set(["closed", "fallback_committed"]);
33
+ const ALLOWED_TRANSITIONS = {
34
+ eligible: ["native_compacting", "native_retry_owned", "settled_for_manual"],
35
+ native_compacting: ["native_retry_owned", "native_succeeded", "native_failed"],
36
+ native_retry_owned: ["native_compacting", "native_succeeded", "native_failed"],
37
+ native_succeeded: ["closed"],
38
+ native_failed: ["fallback_required", "fallback_committed"],
39
+ settled_for_manual: ["manual_sent"],
40
+ manual_sent: ["manual_succeeded", "manual_failed", "manual_ambiguous"],
41
+ manual_succeeded: ["retrying", "second_overflow"],
42
+ manual_failed: ["fallback_required", "fallback_committed"],
43
+ manual_ambiguous: ["fallback_required", "fallback_committed"],
44
+ retrying: ["second_overflow", "closed"],
45
+ second_overflow: ["fallback_required", "fallback_committed"],
46
+ fallback_required: ["fallback_committed"],
47
+ fallback_committed: ["closed"],
48
+ closed: [],
49
+ };
50
+ function requireFiniteBoolean(value, name) {
51
+ if (typeof value !== "boolean")
52
+ throw new Error(`${name} must be a boolean`);
53
+ return value;
54
+ }
55
+ function isPiNativeCompactionRequired(contextTokens) {
56
+ return Number.isFinite(contextTokens) && contextTokens > exports.PI_COMPACTION_THRESHOLD;
57
+ }
58
+ function assertEffectivePiCompactionSettings(settings) {
59
+ const contextWindow = settings.contextWindow ?? settings.model?.contextWindow;
60
+ if (contextWindow !== exports.PI_CONTEXT_WINDOW)
61
+ throw new Error(`Pi effective context window must be exactly ${exports.PI_CONTEXT_WINDOW}`);
62
+ if (settings.compaction?.enabled !== true)
63
+ throw new Error("Pi native compaction must be enabled");
64
+ if (settings.compaction.reserveTokens !== exports.COMPACTION_RESERVE_TOKENS) {
65
+ throw new Error(`Pi reserveTokens must be exactly ${exports.COMPACTION_RESERVE_TOKENS}`);
66
+ }
67
+ if (settings.compaction.keepRecentTokens !== exports.COMPACTION_KEEP_RECENT_TOKENS) {
68
+ throw new Error(`Pi keepRecentTokens must be exactly ${exports.COMPACTION_KEEP_RECENT_TOKENS}`);
69
+ }
70
+ }
71
+ function hasProjectPiCompactionOverride(settings) {
72
+ if (!settings || typeof settings !== "object" || Array.isArray(settings))
73
+ return false;
74
+ const value = settings;
75
+ if (Object.hasOwn(value, "contextWindow") || Object.hasOwn(value, "contextTokens")
76
+ || Object.hasOwn(value, "reserveTokens") || Object.hasOwn(value, "keepRecentTokens"))
77
+ return true;
78
+ const compaction = value.compaction;
79
+ return Boolean(compaction && typeof compaction === "object" && !Array.isArray(compaction)
80
+ && ["enabled", "reserveTokens", "keepRecentTokens"].some((key) => Object.hasOwn(compaction, key)));
81
+ }
82
+ function assertOwnedDirectory(directory) {
83
+ const stat = node_fs_1.default.lstatSync(directory);
84
+ if (!stat.isDirectory() || stat.isSymbolicLink())
85
+ throw new Error(`Pi Agent directory is unsafe: ${directory}`);
86
+ if (process.platform !== "win32" && ((typeof process.getuid === "function" && stat.uid !== process.getuid())
87
+ || (stat.mode & 0o777) !== 0o700))
88
+ throw new Error("Pi Agent directory must be owner-only 0700");
89
+ }
90
+ function ensureDirectory(directory) {
91
+ try {
92
+ const existing = node_fs_1.default.lstatSync(directory);
93
+ if (existing.isSymbolicLink() || !existing.isDirectory())
94
+ throw new Error(`unsafe Pi Agent directory: ${directory}`);
95
+ }
96
+ catch (error) {
97
+ if (error.code !== "ENOENT")
98
+ throw error;
99
+ node_fs_1.default.mkdirSync(directory, { mode: 0o700 });
100
+ }
101
+ node_fs_1.default.chmodSync(directory, 0o700);
102
+ assertOwnedDirectory(directory);
103
+ }
104
+ function ensureDirectoryChain(directory) {
105
+ const resolved = node_path_1.default.resolve(directory);
106
+ const root = node_path_1.default.parse(resolved).root;
107
+ let current = root;
108
+ for (const part of node_path_1.default.relative(root, resolved).split(node_path_1.default.sep).filter(Boolean)) {
109
+ current = node_path_1.default.join(current, part);
110
+ try {
111
+ const stat = node_fs_1.default.lstatSync(current);
112
+ // macOS exposes /tmp and /var as stable system aliases; all deeper
113
+ // Larkin-controlled ancestors remain strictly no-symlink.
114
+ const knownSystemSymlink = current === "/tmp" || current === "/var" || current === "/private/tmp" || current === "/private/var";
115
+ const systemAlias = knownSystemSymlink
116
+ || current === "/var/folders" || /^\/var\/folders\/[^/]+$/.test(current)
117
+ || current === "/private/var/folders" || /^\/private\/var\/folders\/[^/]+$/.test(current);
118
+ if ((!knownSystemSymlink && stat.isSymbolicLink()) || (!systemAlias && !stat.isDirectory())) {
119
+ throw new Error(`unsafe Pi Agent ancestor: ${current}`);
120
+ }
121
+ }
122
+ catch (error) {
123
+ if (error.code !== "ENOENT")
124
+ throw error;
125
+ node_fs_1.default.mkdirSync(current, { mode: 0o700 });
126
+ const created = node_fs_1.default.lstatSync(current);
127
+ if (created.isSymbolicLink() || !created.isDirectory())
128
+ throw new Error(`unsafe Pi Agent ancestor: ${current}`);
129
+ }
130
+ }
131
+ }
132
+ function ensureOwnedPiAgentDirectory(root, agentId) {
133
+ if (!AGENT_ID.test(agentId))
134
+ throw new Error("invalid Pi Agent ID");
135
+ const resolvedRoot = node_path_1.default.resolve(root);
136
+ ensureDirectory(resolvedRoot);
137
+ const parent = node_path_1.default.join(resolvedRoot, "pi-agents");
138
+ ensureDirectory(parent);
139
+ const directory = node_path_1.default.join(parent, agentId);
140
+ ensureDirectory(directory);
141
+ return directory;
142
+ }
143
+ function ownedPiSettings(directory) {
144
+ return node_path_1.default.join(directory, "settings.json");
145
+ }
146
+ function prepareOwnedPiDirectory(directory) {
147
+ ensureDirectoryChain(node_path_1.default.dirname(directory));
148
+ ensureDirectory(node_path_1.default.dirname(directory));
149
+ ensureDirectory(directory);
150
+ writeOwnedPiSettings(directory);
151
+ return directory;
152
+ }
153
+ function writeOwnedPiSettings(directory) {
154
+ assertOwnedDirectory(directory);
155
+ const file = ownedPiSettings(directory);
156
+ try {
157
+ const existing = node_fs_1.default.lstatSync(file);
158
+ if (existing.isSymbolicLink() || !existing.isFile())
159
+ throw new Error("Pi owned settings must not be a symlink");
160
+ }
161
+ catch (error) {
162
+ if (error.code !== "ENOENT")
163
+ throw error;
164
+ }
165
+ const temporary = node_path_1.default.join(directory, `.${node_path_1.default.basename(file)}.${process.pid}.${node_crypto_1.default.randomUUID()}.tmp`);
166
+ try {
167
+ node_fs_1.default.writeFileSync(temporary, `${JSON.stringify({
168
+ compaction: {
169
+ enabled: true,
170
+ reserveTokens: exports.COMPACTION_RESERVE_TOKENS,
171
+ keepRecentTokens: exports.COMPACTION_KEEP_RECENT_TOKENS,
172
+ },
173
+ }, null, 2)}\n`, { flag: "wx", mode: 0o600 });
174
+ node_fs_1.default.renameSync(temporary, file);
175
+ node_fs_1.default.chmodSync(file, 0o600);
176
+ }
177
+ finally {
178
+ try {
179
+ node_fs_1.default.unlinkSync(temporary);
180
+ }
181
+ catch { /* renamed or absent */ }
182
+ }
183
+ }
184
+ function readOwnedPiSettings(directory) {
185
+ const file = ownedPiSettings(directory);
186
+ const stat = node_fs_1.default.lstatSync(file);
187
+ if (stat.isSymbolicLink() || !stat.isFile())
188
+ throw new Error("Pi owned settings must be a regular file");
189
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
190
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
191
+ throw new Error("Pi owned settings are invalid");
192
+ const compaction = parsed.compaction;
193
+ if (!compaction || typeof compaction !== "object" || Array.isArray(compaction))
194
+ throw new Error("Pi owned compaction settings are missing");
195
+ const values = compaction;
196
+ const enabled = requireFiniteBoolean(values.enabled, "Pi owned compaction enabled");
197
+ if (typeof values.reserveTokens !== "number" || typeof values.keepRecentTokens !== "number") {
198
+ throw new Error("Pi owned compaction reserve/keep settings are invalid");
199
+ }
200
+ return { compaction: { enabled, reserveTokens: values.reserveTokens, keepRecentTokens: values.keepRecentTokens } };
201
+ }
202
+ function verifyPiCapabilities(capabilities) {
203
+ if (!/(?:^|\D)0\.83\.0(?:$|\D)/.test(capabilities.version || "")) {
204
+ throw new Error("trusted Pi version 0.83.0 is required");
205
+ }
206
+ const contextWindow = capabilities.contextWindow ?? capabilities.model?.contextWindow;
207
+ if (contextWindow !== exports.PI_CONTEXT_WINDOW)
208
+ throw new Error("Pi capability context window is not 272000");
209
+ if (requireFiniteBoolean(capabilities.autoCompactionEnabled, "Pi autoCompactionEnabled") !== true) {
210
+ throw new Error("Pi effective native compaction is disabled");
211
+ }
212
+ if (capabilities.compactRpc !== true)
213
+ throw new Error("Pi compact RPC capability is missing");
214
+ if (capabilities.trustedProtocol !== true) {
215
+ if (capabilities.reserveTokens !== exports.COMPACTION_RESERVE_TOKENS
216
+ || capabilities.keepRecentTokens !== exports.COMPACTION_KEEP_RECENT_TOKENS) {
217
+ throw new Error("Pi external effective compaction reserve/keep settings are unproven");
218
+ }
219
+ const events = new Set(capabilities.events || []);
220
+ for (const event of REQUIRED_PI_EVENTS)
221
+ if (!events.has(event))
222
+ throw new Error(`Pi capability event is unproven: ${event}`);
223
+ }
224
+ }
225
+ function stateFile(root) {
226
+ return node_path_1.default.join(root, exports.PI_COMPACTION_RECOVERY_FILE);
227
+ }
228
+ function assertStableIdentity(value, name) {
229
+ if (typeof value !== "string" || value.trim() === "" || value.length > 512 || /[\u0000\r\n]/.test(value)) {
230
+ throw new Error(`invalid stable Pi compaction identity: ${name}`);
231
+ }
232
+ }
233
+ function safeRecord(value) {
234
+ if (!value || typeof value !== "object" || Array.isArray(value))
235
+ throw new Error("invalid Pi compaction breaker record");
236
+ const record = value;
237
+ const manualAttempt = record.manualAttempt;
238
+ const sessionGeneration = record.sessionGeneration;
239
+ try {
240
+ assertStableIdentity(record.key, "key");
241
+ assertStableIdentity(record.messageId, "messageId");
242
+ assertStableIdentity(record.deliveryId, "deliveryId");
243
+ assertStableIdentity(record.inputId, "inputId");
244
+ }
245
+ catch {
246
+ throw new Error("invalid Pi compaction breaker identity");
247
+ }
248
+ if (typeof sessionGeneration !== "number" || !Number.isSafeInteger(sessionGeneration)
249
+ || typeof record.state !== "string" || typeof manualAttempt !== "number" || !Number.isSafeInteger(manualAttempt)
250
+ || manualAttempt < 0 || manualAttempt > 1 || typeof record.updatedAt !== "string") {
251
+ throw new Error("invalid Pi compaction breaker identity");
252
+ }
253
+ if (record.compactSentAt !== null && typeof record.compactSentAt !== "string")
254
+ throw new Error("invalid compactSentAt");
255
+ if (record.compactDeadlineAt !== null && typeof record.compactDeadlineAt !== "string")
256
+ throw new Error("invalid compactDeadlineAt");
257
+ if (record.compactFinishedAt !== null && typeof record.compactFinishedAt !== "string")
258
+ throw new Error("invalid compactFinishedAt");
259
+ if (record.retrySubmittedAt !== null && typeof record.retrySubmittedAt !== "string")
260
+ throw new Error("invalid retrySubmittedAt");
261
+ if (record.fallbackReason !== null && typeof record.fallbackReason !== "string")
262
+ throw new Error("invalid fallbackReason");
263
+ if (record.compactResponseReceived !== undefined && typeof record.compactResponseReceived !== "boolean")
264
+ throw new Error("invalid compactResponseReceived");
265
+ if (record.compactionLifecycleSucceeded !== undefined && typeof record.compactionLifecycleSucceeded !== "boolean")
266
+ throw new Error("invalid compactionLifecycleSucceeded");
267
+ if (!(new Set([
268
+ "eligible", "native_compacting", "native_retry_owned", "native_succeeded", "native_failed", "settled_for_manual",
269
+ "manual_sent", "manual_succeeded", "manual_failed", "manual_ambiguous", "retrying", "second_overflow",
270
+ "fallback_required", "fallback_committed", "closed",
271
+ ])).has(record.state))
272
+ throw new Error("unknown Pi compaction breaker state");
273
+ return {
274
+ key: record.key, messageId: record.messageId, deliveryId: record.deliveryId, inputId: record.inputId,
275
+ sessionGeneration: sessionGeneration, state: record.state,
276
+ manualAttempt: manualAttempt, compactSentAt: record.compactSentAt ?? null,
277
+ compactDeadlineAt: record.compactDeadlineAt ?? null, compactFinishedAt: record.compactFinishedAt ?? null,
278
+ retrySubmittedAt: record.retrySubmittedAt ?? null, fallbackReason: record.fallbackReason ?? null,
279
+ ...(record.compactResponseReceived !== undefined ? { compactResponseReceived: record.compactResponseReceived } : {}),
280
+ ...(record.compactionLifecycleSucceeded !== undefined ? { compactionLifecycleSucceeded: record.compactionLifecycleSucceeded } : {}),
281
+ updatedAt: record.updatedAt,
282
+ };
283
+ }
284
+ class PiCompactionBreaker {
285
+ root;
286
+ file;
287
+ clock;
288
+ records;
289
+ withLock;
290
+ constructor(root, options = {}) {
291
+ this.root = root;
292
+ node_fs_1.default.mkdirSync(root, { recursive: true, mode: 0o700 });
293
+ node_fs_1.default.chmodSync(root, 0o700);
294
+ ensureDirectory(root);
295
+ this.file = stateFile(root);
296
+ this.clock = options.now ?? (() => new Date().toISOString());
297
+ this.records = new Map(this.read().records.map((record) => [record.key, record]));
298
+ this.retentionMs = options.retentionMs ?? 30 * 24 * 60 * 60 * 1000;
299
+ this.withLock = options.withLock ?? ((operation) => operation());
300
+ }
301
+ retentionMs;
302
+ read() {
303
+ try {
304
+ const stat = node_fs_1.default.lstatSync(this.file);
305
+ if (stat.isSymbolicLink() || !stat.isFile())
306
+ throw new Error("Pi compaction breaker must be a regular file");
307
+ if (process.platform !== "win32" && ((typeof process.getuid === "function" && stat.uid !== process.getuid())
308
+ || (stat.mode & 0o777) !== 0o600))
309
+ throw new Error("Pi compaction breaker must be owner-only 0600");
310
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(this.file, "utf8"));
311
+ if (parsed.version !== 1 || !Array.isArray(parsed.records))
312
+ throw new Error("unknown Pi compaction breaker version");
313
+ return { version: 1, records: parsed.records.map(safeRecord) };
314
+ }
315
+ catch (error) {
316
+ if (error.code === "ENOENT")
317
+ return { version: 1, records: [] };
318
+ throw error;
319
+ }
320
+ }
321
+ persist() {
322
+ const temporary = node_path_1.default.join(this.root, `.${node_path_1.default.basename(this.file)}.${process.pid}.${node_crypto_1.default.randomUUID()}.tmp`);
323
+ const content = `${JSON.stringify({ version: 1, records: [...this.records.values()] }, null, 2)}\n`;
324
+ let descriptor = null;
325
+ try {
326
+ descriptor = node_fs_1.default.openSync(temporary, "wx", 0o600);
327
+ node_fs_1.default.writeFileSync(descriptor, content);
328
+ node_fs_1.default.fsyncSync(descriptor);
329
+ node_fs_1.default.closeSync(descriptor);
330
+ descriptor = null;
331
+ node_fs_1.default.renameSync(temporary, this.file);
332
+ node_fs_1.default.chmodSync(this.file, 0o600);
333
+ const stat = node_fs_1.default.lstatSync(this.file);
334
+ if (stat.isSymbolicLink() || (process.platform !== "win32" && (stat.mode & 0o777) !== 0o600))
335
+ throw new Error("Pi compaction breaker write verification failed");
336
+ }
337
+ finally {
338
+ if (descriptor !== null) {
339
+ try {
340
+ node_fs_1.default.closeSync(descriptor);
341
+ }
342
+ catch { /* preserve primary error */ }
343
+ }
344
+ try {
345
+ node_fs_1.default.unlinkSync(temporary);
346
+ }
347
+ catch { /* renamed or absent */ }
348
+ }
349
+ }
350
+ reload() {
351
+ this.records = new Map(this.read().records.map((record) => [record.key, record]));
352
+ }
353
+ get(key) {
354
+ return this.withLock(() => { this.reload(); const record = this.records.get(key); return record ? { ...record } : undefined; });
355
+ }
356
+ listNonTerminal() {
357
+ return this.withLock(() => { this.reload(); return [...this.records.values()].filter((record) => !TERMINAL_STATES.has(record.state)).map((record) => ({ ...record })); });
358
+ }
359
+ save(record) {
360
+ this.withLock(() => { this.reload(); const checked = safeRecord({ ...record, updatedAt: this.clock() }); this.records.set(checked.key, checked); this.persist(); });
361
+ }
362
+ transitionUnlocked(key, patch, state) {
363
+ this.reload();
364
+ const current = this.records.get(key);
365
+ if (!current) {
366
+ const sessionGeneration = patch.sessionGeneration;
367
+ if (state !== "eligible" || !patch.messageId || !patch.deliveryId || !patch.inputId
368
+ || typeof sessionGeneration !== "number" || !Number.isSafeInteger(sessionGeneration)) {
369
+ throw new Error("invalid initial Pi compaction breaker transition");
370
+ }
371
+ const record = {
372
+ key, messageId: patch.messageId, deliveryId: patch.deliveryId, inputId: patch.inputId,
373
+ sessionGeneration, state, manualAttempt: 0,
374
+ compactSentAt: null, compactDeadlineAt: null, compactFinishedAt: null, retrySubmittedAt: null,
375
+ fallbackReason: null, updatedAt: this.clock(),
376
+ };
377
+ this.records.set(key, record);
378
+ this.persist();
379
+ return { ...record };
380
+ }
381
+ if (current.state === state)
382
+ throw new Error(`duplicate Pi compaction transition: ${state}`);
383
+ if (TERMINAL_STATES.has(current.state))
384
+ throw new Error(`Pi compaction breaker is terminal: ${current.state}`);
385
+ if (!ALLOWED_TRANSITIONS[current.state].includes(state))
386
+ throw new Error(`invalid Pi compaction transition ${current.state} -> ${state}`);
387
+ const next = { ...current, ...patch, key, state, updatedAt: this.clock() };
388
+ if (next.key !== current.key || next.messageId !== current.messageId || next.deliveryId !== current.deliveryId
389
+ || next.inputId !== current.inputId || next.sessionGeneration !== current.sessionGeneration) {
390
+ throw new Error("Pi compaction breaker stable identity is immutable");
391
+ }
392
+ if (state === "manual_sent") {
393
+ if (current.state !== "settled_for_manual" || current.manualAttempt >= 1)
394
+ throw new Error("manual compact attempt already used");
395
+ next.manualAttempt = 1;
396
+ }
397
+ safeRecord(next);
398
+ this.records.set(key, next);
399
+ this.persist();
400
+ return { ...next };
401
+ }
402
+ transition(key, patch, state) {
403
+ return this.withLock(() => this.transitionUnlocked(key, patch, state));
404
+ }
405
+ transitionInTransaction(key, patch, state) {
406
+ return this.transitionUnlocked(key, patch, state);
407
+ }
408
+ prune(consumedKeys = []) {
409
+ return this.withLock(() => this.pruneUnlocked(consumedKeys));
410
+ }
411
+ pruneUnlocked(consumedKeys = []) {
412
+ this.reload();
413
+ const consumed = new Set(consumedKeys);
414
+ const cutoff = Date.parse(this.clock()) - this.retentionMs;
415
+ let count = 0;
416
+ for (const [key, record] of this.records) {
417
+ if (!TERMINAL_STATES.has(record.state) || !consumed.has(key))
418
+ continue;
419
+ if (!Number.isFinite(cutoff) || Date.parse(record.updatedAt) > cutoff)
420
+ continue;
421
+ this.records.delete(key);
422
+ count += 1;
423
+ }
424
+ if (count)
425
+ this.persist();
426
+ return count;
427
+ }
428
+ forceFallback(key, reason) {
429
+ return this.withLock(() => {
430
+ this.reload();
431
+ const current = this.records.get(key);
432
+ if (!current)
433
+ throw new Error("Pi compaction breaker record is missing");
434
+ if (TERMINAL_STATES.has(current.state))
435
+ return { ...current };
436
+ const next = { ...current, state: "fallback_required", fallbackReason: reason, updatedAt: this.clock() };
437
+ safeRecord(next);
438
+ this.records.set(key, next);
439
+ this.persist();
440
+ return { ...next };
441
+ });
442
+ }
443
+ forceFallbackInTransaction(key, reason) {
444
+ this.reload();
445
+ const current = this.records.get(key);
446
+ if (!current)
447
+ throw new Error("Pi compaction breaker record is missing");
448
+ if (TERMINAL_STATES.has(current.state))
449
+ return { ...current };
450
+ const next = { ...current, state: "fallback_committed", fallbackReason: reason, updatedAt: this.clock() };
451
+ safeRecord(next);
452
+ this.records.set(key, next);
453
+ this.persist();
454
+ return { ...next };
455
+ }
456
+ }
457
+ exports.PiCompactionBreaker = PiCompactionBreaker;
458
+ class PiCompactionRecoveryMachine {
459
+ key;
460
+ record;
461
+ breaker;
462
+ onAction;
463
+ constructor(input) {
464
+ this.key = input.key;
465
+ this.breaker = input.breaker;
466
+ this.onAction = input.onAction ?? (() => { });
467
+ this.record = input.breaker.get?.(input.key) ?? {
468
+ key: input.key, messageId: input.messageId, deliveryId: input.deliveryId, inputId: input.inputId,
469
+ sessionGeneration: input.sessionGeneration, state: "eligible", manualAttempt: 0,
470
+ compactSentAt: null, compactDeadlineAt: null, compactFinishedAt: null, retrySubmittedAt: null,
471
+ fallbackReason: null, updatedAt: new Date().toISOString(),
472
+ };
473
+ }
474
+ get state() { return this.record.state; }
475
+ get recordSnapshot() { return { ...this.record }; }
476
+ change(state, patch = {}) {
477
+ if (TERMINAL_STATES.has(this.record.state) || this.record.state === "fallback_required")
478
+ return;
479
+ this.record = { ...this.record, ...patch, state, updatedAt: new Date().toISOString() };
480
+ if (this.breaker.transition) {
481
+ try {
482
+ this.record = this.breaker.transition(this.key, this.record, state);
483
+ }
484
+ catch (error) {
485
+ if (this.breaker.get?.(this.key))
486
+ throw error;
487
+ this.breaker.save?.(this.record);
488
+ }
489
+ }
490
+ else
491
+ this.breaker.save?.(this.record);
492
+ }
493
+ agentEnd(input) {
494
+ if (!input.exactOverflow || TERMINAL_STATES.has(this.state) || this.state === "fallback_required")
495
+ return;
496
+ if (["native_compacting", "native_retry_owned", "native_succeeded"].includes(this.state))
497
+ return;
498
+ if (this.state === "retrying") {
499
+ this.change("second_overflow", { fallbackReason: "second exact context overflow" });
500
+ this.onAction("fresh_session_fallback");
501
+ return;
502
+ }
503
+ if (input.willRetry)
504
+ this.change("native_retry_owned");
505
+ }
506
+ compactionStart(input) {
507
+ if (input.reason === "overflow" && (this.state === "eligible" || this.state === "native_retry_owned"))
508
+ this.change("native_compacting");
509
+ }
510
+ compactionEnd(input) {
511
+ if (this.state === "fallback_required" || TERMINAL_STATES.has(this.state))
512
+ return;
513
+ if (input.reason === "overflow") {
514
+ if (this.state === "native_retry_owned" || this.state === "native_compacting" || this.state === "eligible") {
515
+ if (input.success && input.willRetry)
516
+ this.change("native_succeeded");
517
+ else {
518
+ this.change("native_failed", { fallbackReason: "native overflow compaction failed" });
519
+ this.onAction("fresh_session_fallback");
520
+ }
521
+ }
522
+ return;
523
+ }
524
+ if (this.state !== "manual_sent")
525
+ return;
526
+ if (!input.success) {
527
+ this.change("manual_failed", { fallbackReason: "manual compaction failed" });
528
+ this.onAction("fresh_session_fallback");
529
+ return;
530
+ }
531
+ this.record = { ...this.record, compactionLifecycleSucceeded: true };
532
+ if (this.record.compactResponseReceived) {
533
+ this.change("manual_succeeded", { compactFinishedAt: new Date().toISOString() });
534
+ this.onAction("retry_input");
535
+ }
536
+ }
537
+ agentSettled() {
538
+ if (this.state !== "eligible")
539
+ return;
540
+ this.change("settled_for_manual");
541
+ this.onAction("arm_manual_compact");
542
+ }
543
+ manualCompactSent(deadlineAt) {
544
+ if (this.state !== "settled_for_manual" || this.record.manualAttempt >= 1)
545
+ return;
546
+ this.change("manual_sent", { manualAttempt: 1, compactSentAt: new Date().toISOString(), compactDeadlineAt: deadlineAt });
547
+ this.onAction("manual_compact");
548
+ }
549
+ compactResponse(input) {
550
+ if (this.state !== "manual_sent")
551
+ return;
552
+ if (!input.success) {
553
+ this.change(input.ambiguous ? "manual_ambiguous" : "manual_failed", {
554
+ fallbackReason: input.ambiguous ? "manual compact response became ambiguous after send" : "manual compact RPC failed",
555
+ });
556
+ this.onAction("fresh_session_fallback");
557
+ return;
558
+ }
559
+ this.record = { ...this.record, compactResponseReceived: true };
560
+ if (this.record.compactionLifecycleSucceeded) {
561
+ this.change("manual_succeeded", { compactFinishedAt: new Date().toISOString() });
562
+ this.onAction("retry_input");
563
+ }
564
+ }
565
+ compactTimeout() {
566
+ if (this.state !== "manual_sent")
567
+ return;
568
+ this.change("manual_ambiguous", { fallbackReason: "manual compact deadline expired" });
569
+ this.onAction("fresh_session_fallback");
570
+ }
571
+ retrySubmitted() {
572
+ if (this.state !== "manual_succeeded")
573
+ return;
574
+ this.change("retrying", { retrySubmittedAt: new Date().toISOString() });
575
+ }
576
+ fallbackRequired(reason) {
577
+ if (TERMINAL_STATES.has(this.state))
578
+ return;
579
+ if (this.breaker.forceFallback)
580
+ this.record = this.breaker.forceFallback(this.key, reason);
581
+ else
582
+ this.change("fallback_required", { fallbackReason: reason });
583
+ this.onAction("fresh_session_fallback");
584
+ }
585
+ fallbackCommitted() {
586
+ if (!["native_failed", "manual_failed", "manual_ambiguous", "second_overflow", "fallback_required"].includes(this.state))
587
+ return;
588
+ this.change("fallback_committed");
589
+ }
590
+ close() {
591
+ if (["native_succeeded", "retrying", "fallback_committed"].includes(this.state))
592
+ this.change("closed");
593
+ }
594
+ }
595
+ exports.PiCompactionRecoveryMachine = PiCompactionRecoveryMachine;
596
+ function prepareOwnedPiEnvironment(input) {
597
+ const directory = ensureOwnedPiAgentDirectory(input.root, input.agentId);
598
+ writeOwnedPiSettings(directory);
599
+ return { ...input.env, PI_CODING_AGENT_DIR: directory };
600
+ }
601
+ function readProjectPiSettings(workspaceDir) {
602
+ const file = node_path_1.default.join(workspaceDir, ".pi", "settings.json");
603
+ try {
604
+ const stat = node_fs_1.default.lstatSync(file);
605
+ if (stat.isSymbolicLink() || !stat.isFile())
606
+ throw new Error("project Pi settings must be a regular file");
607
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
608
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
609
+ throw new Error("project Pi settings are invalid");
610
+ return parsed;
611
+ }
612
+ catch (error) {
613
+ if (error.code === "ENOENT")
614
+ return null;
615
+ throw error;
616
+ }
617
+ }
618
+ function assertNoProjectPiCompactionOverride(workspaceDir) {
619
+ const settings = readProjectPiSettings(workspaceDir);
620
+ if (settings && hasProjectPiCompactionOverride(settings))
621
+ throw new Error("project Pi compaction/context settings override Larkin policy");
622
+ }
623
+ function currentUserLabel() { return node_os_1.default.userInfo().username; }