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