larkin 0.4.2 → 0.4.6
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 +5 -1
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +218 -228
- package/assets/readme/larkin-github-cover.png +0 -0
- package/dist/agent/agent-state-store.cjs +1 -2
- package/dist/agent/agent-state-store.mjs +1 -2
- package/dist/app/agent-config.mjs +73 -6
- package/dist/app/agent-readiness.cjs +22 -2
- package/dist/app/agent-readiness.mjs +20 -2
- package/dist/app/binary-entry.mjs +6 -0
- package/dist/app/cli.mjs +4 -0
- package/dist/app/run.mjs +8 -0
- package/dist/app/runtime-process.mjs +13 -1
- package/dist/dashboard/dashboard-view-model.mjs +12 -3
- package/dist/feishu/host-shell.cjs +33 -10
- package/dist/feishu/host-shell.mjs +34 -11
- package/dist/platform/config.cjs +406 -33
- package/dist/platform/config.mjs +405 -33
- package/dist/platform/process-boundary-trace.cjs +108 -0
- package/dist/platform/process-boundary-trace.mjs +102 -0
- package/dist/runtime/pi-artifact-provenance.cjs +147 -0
- package/dist/runtime/pi-artifact-provenance.mjs +139 -0
- package/dist/runtime/pi-compaction-recovery.cjs +120 -40
- package/dist/runtime/pi-compaction-recovery.mjs +117 -40
- package/dist/runtime/pi-model-catalog.cjs +2 -2
- package/dist/runtime/pi-model-catalog.mjs +2 -2
- package/dist/runtime/pi-profile-migration.cjs +556 -0
- package/dist/runtime/pi-profile-migration.mjs +515 -0
- package/dist/runtime/pi-provider-config.cjs +1 -1
- package/dist/runtime/pi-provider-config.mjs +1 -1
- package/dist/runtime/runtime-adapters.mjs +97 -11
- package/dist/runtime/runtime-host.cjs +162 -39
- package/dist/runtime/runtime-host.mjs +163 -40
- package/dist/runtime/runtime-readiness.cjs +6 -4
- package/dist/runtime/runtime-readiness.mjs +6 -4
- package/package.json +3 -3
|
@@ -4,14 +4,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
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.calculatePiCompactionSettings = calculatePiCompactionSettings;
|
|
7
8
|
exports.isPiNativeCompactionRequired = isPiNativeCompactionRequired;
|
|
8
9
|
exports.assertEffectivePiCompactionSettings = assertEffectivePiCompactionSettings;
|
|
9
10
|
exports.hasProjectPiCompactionOverride = hasProjectPiCompactionOverride;
|
|
10
11
|
exports.ensureOwnedPiAgentDirectory = ensureOwnedPiAgentDirectory;
|
|
11
12
|
exports.ownedPiSettings = ownedPiSettings;
|
|
12
13
|
exports.prepareOwnedPiDirectory = prepareOwnedPiDirectory;
|
|
14
|
+
exports.mergeOwnedPiSettings = mergeOwnedPiSettings;
|
|
13
15
|
exports.writeOwnedPiSettings = writeOwnedPiSettings;
|
|
14
16
|
exports.readOwnedPiSettings = readOwnedPiSettings;
|
|
17
|
+
exports.parsePiExecutableVersion = parsePiExecutableVersion;
|
|
15
18
|
exports.verifyPiCapabilities = verifyPiCapabilities;
|
|
16
19
|
exports.prepareOwnedPiEnvironment = prepareOwnedPiEnvironment;
|
|
17
20
|
exports.readProjectPiSettings = readProjectPiSettings;
|
|
@@ -21,10 +24,14 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
|
21
24
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
22
25
|
const node_os_1 = __importDefault(require("node:os"));
|
|
23
26
|
const node_path_1 = __importDefault(require("node:path"));
|
|
27
|
+
const pi_provider_config_js_1 = require("./pi-provider-config.cjs");
|
|
28
|
+
/** Fallback values used while importing a Pi profile, before Pi reports its model. */
|
|
24
29
|
exports.PI_CONTEXT_WINDOW = 272_000;
|
|
25
30
|
exports.COMPACTION_RESERVE_TOKENS = 40_800;
|
|
26
31
|
exports.COMPACTION_KEEP_RECENT_TOKENS = 20_000;
|
|
27
32
|
exports.PI_COMPACTION_THRESHOLD = exports.PI_CONTEXT_WINDOW - exports.COMPACTION_RESERVE_TOKENS;
|
|
33
|
+
const COMPACTION_RESERVE_RATIO_PERCENT = 15;
|
|
34
|
+
const MAX_KEEP_RECENT_TOKENS = 20_000;
|
|
28
35
|
exports.PI_COMPACTION_RECOVERY_VERSION = 1;
|
|
29
36
|
exports.PI_COMPACTION_RECOVERY_FILE = "piCompactionRecovery.json";
|
|
30
37
|
const AGENT_ID = /^cli_[A-Za-z0-9]+$/;
|
|
@@ -52,20 +59,44 @@ function requireFiniteBoolean(value, name) {
|
|
|
52
59
|
throw new Error(`${name} must be a boolean`);
|
|
53
60
|
return value;
|
|
54
61
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
62
|
+
function calculatePiCompactionSettings(contextWindow) {
|
|
63
|
+
if (!Number.isSafeInteger(contextWindow) || contextWindow <= 0) {
|
|
64
|
+
throw new Error("Pi effective context window must be a positive safe integer");
|
|
65
|
+
}
|
|
66
|
+
const reserveTokens = Math.min(contextWindow - 1, Math.ceil(contextWindow * COMPACTION_RESERVE_RATIO_PERCENT / 100));
|
|
67
|
+
const threshold = contextWindow - reserveTokens;
|
|
68
|
+
if (reserveTokens < 1 || threshold <= 1)
|
|
69
|
+
throw new Error("Pi effective context window is too small for safe compaction");
|
|
70
|
+
const keepRecentTokens = Math.min(MAX_KEEP_RECENT_TOKENS, threshold - 1);
|
|
71
|
+
return { reserveTokens, keepRecentTokens, threshold };
|
|
72
|
+
}
|
|
73
|
+
function isPiNativeCompactionRequired(contextTokens, contextWindow = exports.PI_CONTEXT_WINDOW) {
|
|
74
|
+
if (!Number.isFinite(contextTokens))
|
|
75
|
+
return false;
|
|
76
|
+
try {
|
|
77
|
+
return contextTokens > calculatePiCompactionSettings(contextWindow).threshold;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
57
82
|
}
|
|
58
83
|
function assertEffectivePiCompactionSettings(settings) {
|
|
59
84
|
const contextWindow = settings.contextWindow ?? settings.model?.contextWindow;
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
const expected = (() => {
|
|
86
|
+
try {
|
|
87
|
+
return calculatePiCompactionSettings(contextWindow);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new Error("Pi effective context window must be a positive safe integer");
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
62
93
|
if (settings.compaction?.enabled !== true)
|
|
63
94
|
throw new Error("Pi native compaction must be enabled");
|
|
64
|
-
if (settings.compaction.reserveTokens !==
|
|
65
|
-
throw new Error(`Pi reserveTokens must be exactly ${
|
|
95
|
+
if (settings.compaction.reserveTokens !== expected.reserveTokens) {
|
|
96
|
+
throw new Error(`Pi reserveTokens must be exactly ${expected.reserveTokens} for context window ${contextWindow}`);
|
|
66
97
|
}
|
|
67
|
-
if (settings.compaction.keepRecentTokens !==
|
|
68
|
-
throw new Error(`Pi keepRecentTokens must be exactly ${
|
|
98
|
+
if (settings.compaction.keepRecentTokens !== expected.keepRecentTokens) {
|
|
99
|
+
throw new Error(`Pi keepRecentTokens must be exactly ${expected.keepRecentTokens} for context window ${contextWindow}`);
|
|
69
100
|
}
|
|
70
101
|
}
|
|
71
102
|
function hasProjectPiCompactionOverride(settings) {
|
|
@@ -132,6 +163,7 @@ function ensureDirectoryChain(directory) {
|
|
|
132
163
|
function ensureOwnedPiAgentDirectory(root, agentId) {
|
|
133
164
|
if (!AGENT_ID.test(agentId))
|
|
134
165
|
throw new Error("invalid Pi Agent ID");
|
|
166
|
+
ensureDirectoryChain(root);
|
|
135
167
|
const resolvedRoot = node_path_1.default.resolve(root);
|
|
136
168
|
ensureDirectory(resolvedRoot);
|
|
137
169
|
const parent = node_path_1.default.join(resolvedRoot, "pi-agents");
|
|
@@ -143,15 +175,34 @@ function ensureOwnedPiAgentDirectory(root, agentId) {
|
|
|
143
175
|
function ownedPiSettings(directory) {
|
|
144
176
|
return node_path_1.default.join(directory, "settings.json");
|
|
145
177
|
}
|
|
146
|
-
function prepareOwnedPiDirectory(directory) {
|
|
178
|
+
function prepareOwnedPiDirectory(directory, compaction) {
|
|
147
179
|
ensureDirectoryChain(node_path_1.default.dirname(directory));
|
|
148
180
|
ensureDirectory(node_path_1.default.dirname(directory));
|
|
149
181
|
ensureDirectory(directory);
|
|
150
|
-
writeOwnedPiSettings(directory);
|
|
182
|
+
writeOwnedPiSettings(directory, compaction);
|
|
151
183
|
return directory;
|
|
152
184
|
}
|
|
153
|
-
function
|
|
154
|
-
|
|
185
|
+
function mergeOwnedPiSettings(value, compaction = {
|
|
186
|
+
reserveTokens: exports.COMPACTION_RESERVE_TOKENS,
|
|
187
|
+
keepRecentTokens: exports.COMPACTION_KEEP_RECENT_TOKENS,
|
|
188
|
+
threshold: exports.PI_COMPACTION_THRESHOLD,
|
|
189
|
+
}) {
|
|
190
|
+
if (value !== undefined && (!value || typeof value !== "object" || Array.isArray(value))) {
|
|
191
|
+
throw new Error("Pi owned settings are invalid");
|
|
192
|
+
}
|
|
193
|
+
const object = (value || {});
|
|
194
|
+
const existing = object.compaction;
|
|
195
|
+
if (existing !== undefined && (!existing || typeof existing !== "object" || Array.isArray(existing))) {
|
|
196
|
+
throw new Error("Pi owned compaction settings are invalid");
|
|
197
|
+
}
|
|
198
|
+
return { ...object, compaction: {
|
|
199
|
+
...existing, enabled: true,
|
|
200
|
+
reserveTokens: compaction.reserveTokens, keepRecentTokens: compaction.keepRecentTokens,
|
|
201
|
+
} };
|
|
202
|
+
}
|
|
203
|
+
function writeOwnedPiSettings(directory, compaction) {
|
|
204
|
+
ensureDirectoryChain(node_path_1.default.dirname(directory));
|
|
205
|
+
ensureDirectory(directory);
|
|
155
206
|
const file = ownedPiSettings(directory);
|
|
156
207
|
try {
|
|
157
208
|
const existing = node_fs_1.default.lstatSync(file);
|
|
@@ -162,15 +213,17 @@ function writeOwnedPiSettings(directory) {
|
|
|
162
213
|
if (error.code !== "ENOENT")
|
|
163
214
|
throw error;
|
|
164
215
|
}
|
|
216
|
+
let value;
|
|
217
|
+
try {
|
|
218
|
+
value = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (error.code !== "ENOENT")
|
|
222
|
+
throw new Error("Pi owned settings are invalid");
|
|
223
|
+
}
|
|
165
224
|
const temporary = node_path_1.default.join(directory, `.${node_path_1.default.basename(file)}.${process.pid}.${node_crypto_1.default.randomUUID()}.tmp`);
|
|
166
225
|
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 });
|
|
226
|
+
node_fs_1.default.writeFileSync(temporary, `${JSON.stringify(mergeOwnedPiSettings(value, compaction), null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
174
227
|
node_fs_1.default.renameSync(temporary, file);
|
|
175
228
|
node_fs_1.default.chmodSync(file, 0o600);
|
|
176
229
|
}
|
|
@@ -182,6 +235,8 @@ function writeOwnedPiSettings(directory) {
|
|
|
182
235
|
}
|
|
183
236
|
}
|
|
184
237
|
function readOwnedPiSettings(directory) {
|
|
238
|
+
ensureDirectoryChain(node_path_1.default.dirname(directory));
|
|
239
|
+
assertOwnedDirectory(directory);
|
|
185
240
|
const file = ownedPiSettings(directory);
|
|
186
241
|
const stat = node_fs_1.default.lstatSync(file);
|
|
187
242
|
if (stat.isSymbolicLink() || !stat.isFile())
|
|
@@ -199,28 +254,48 @@ function readOwnedPiSettings(directory) {
|
|
|
199
254
|
}
|
|
200
255
|
return { compaction: { enabled, reserveTokens: values.reserveTokens, keepRecentTokens: values.keepRecentTokens } };
|
|
201
256
|
}
|
|
257
|
+
function parsePiExecutableVersion(output) {
|
|
258
|
+
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
259
|
+
if (lines.length !== 1)
|
|
260
|
+
throw new Error("Pi executable version output must contain exactly one version line");
|
|
261
|
+
const line = lines[0];
|
|
262
|
+
// Pi's bare version is canonical; permit only the fixed official display prefix.
|
|
263
|
+
const escapedVersion = pi_provider_config_js_1.BUNDLED_PI_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
264
|
+
if (!new RegExp(`^(?:${escapedVersion}|pi(?:-coding-agent)?(?:\\s+version)?\\s+v?${escapedVersion})$`, "i").test(line)) {
|
|
265
|
+
throw new Error(`Pi executable version must be exactly ${pi_provider_config_js_1.BUNDLED_PI_VERSION}`);
|
|
266
|
+
}
|
|
267
|
+
return pi_provider_config_js_1.BUNDLED_PI_VERSION;
|
|
268
|
+
}
|
|
202
269
|
function verifyPiCapabilities(capabilities) {
|
|
203
|
-
if (
|
|
204
|
-
throw new Error(
|
|
270
|
+
if (capabilities.version !== pi_provider_config_js_1.BUNDLED_PI_VERSION)
|
|
271
|
+
throw new Error(`trusted Pi version ${pi_provider_config_js_1.BUNDLED_PI_VERSION} is required`);
|
|
272
|
+
if (capabilities.distribution === "external" && capabilities.trustedProtocol === true) {
|
|
273
|
+
throw new Error("external Pi cannot use the bundled trusted protocol bypass");
|
|
205
274
|
}
|
|
206
275
|
const contextWindow = capabilities.contextWindow ?? capabilities.model?.contextWindow;
|
|
207
|
-
|
|
208
|
-
|
|
276
|
+
const expected = (() => {
|
|
277
|
+
try {
|
|
278
|
+
return calculatePiCompactionSettings(contextWindow);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
throw new Error("Pi capability context window is invalid");
|
|
282
|
+
}
|
|
283
|
+
})();
|
|
209
284
|
if (requireFiniteBoolean(capabilities.autoCompactionEnabled, "Pi autoCompactionEnabled") !== true) {
|
|
210
285
|
throw new Error("Pi effective native compaction is disabled");
|
|
211
286
|
}
|
|
212
287
|
if (capabilities.compactRpc !== true)
|
|
213
288
|
throw new Error("Pi compact RPC capability is missing");
|
|
214
|
-
if (capabilities.trustedProtocol
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
289
|
+
if (capabilities.distribution === "builtin" && capabilities.trustedProtocol === true)
|
|
290
|
+
return;
|
|
291
|
+
if (capabilities.reserveTokens !== expected.reserveTokens
|
|
292
|
+
|| capabilities.keepRecentTokens !== expected.keepRecentTokens) {
|
|
293
|
+
throw new Error(`Pi external effective compaction reserve/keep settings are unproven for context window ${contextWindow}`);
|
|
294
|
+
}
|
|
295
|
+
const events = new Set(capabilities.events || []);
|
|
296
|
+
for (const event of REQUIRED_PI_EVENTS)
|
|
297
|
+
if (!events.has(event))
|
|
298
|
+
throw new Error(`Pi capability event is unproven: ${event}`);
|
|
224
299
|
}
|
|
225
300
|
function stateFile(root) {
|
|
226
301
|
return node_path_1.default.join(root, exports.PI_COMPACTION_RECOVERY_FILE);
|
|
@@ -296,9 +371,14 @@ class PiCompactionBreaker {
|
|
|
296
371
|
this.clock = options.now ?? (() => new Date().toISOString());
|
|
297
372
|
this.records = new Map(this.read().records.map((record) => [record.key, record]));
|
|
298
373
|
this.retentionMs = options.retentionMs ?? 30 * 24 * 60 * 60 * 1000;
|
|
299
|
-
this.withLock = options.withLock
|
|
374
|
+
this.withLock = options.withLock;
|
|
300
375
|
}
|
|
301
376
|
retentionMs;
|
|
377
|
+
runLocked(operation) {
|
|
378
|
+
if (!this.withLock)
|
|
379
|
+
throw new Error("Pi compaction breaker requires the canonical Agent state lock");
|
|
380
|
+
return this.withLock(operation);
|
|
381
|
+
}
|
|
302
382
|
read() {
|
|
303
383
|
try {
|
|
304
384
|
const stat = node_fs_1.default.lstatSync(this.file);
|
|
@@ -351,13 +431,13 @@ class PiCompactionBreaker {
|
|
|
351
431
|
this.records = new Map(this.read().records.map((record) => [record.key, record]));
|
|
352
432
|
}
|
|
353
433
|
get(key) {
|
|
354
|
-
return this.
|
|
434
|
+
return this.runLocked(() => { this.reload(); const record = this.records.get(key); return record ? { ...record } : undefined; });
|
|
355
435
|
}
|
|
356
436
|
listNonTerminal() {
|
|
357
|
-
return this.
|
|
437
|
+
return this.runLocked(() => { this.reload(); return [...this.records.values()].filter((record) => !TERMINAL_STATES.has(record.state)).map((record) => ({ ...record })); });
|
|
358
438
|
}
|
|
359
439
|
save(record) {
|
|
360
|
-
this.
|
|
440
|
+
this.runLocked(() => { this.reload(); const checked = safeRecord({ ...record, updatedAt: this.clock() }); this.records.set(checked.key, checked); this.persist(); });
|
|
361
441
|
}
|
|
362
442
|
transitionUnlocked(key, patch, state) {
|
|
363
443
|
this.reload();
|
|
@@ -400,13 +480,13 @@ class PiCompactionBreaker {
|
|
|
400
480
|
return { ...next };
|
|
401
481
|
}
|
|
402
482
|
transition(key, patch, state) {
|
|
403
|
-
return this.
|
|
483
|
+
return this.runLocked(() => this.transitionUnlocked(key, patch, state));
|
|
404
484
|
}
|
|
405
485
|
transitionInTransaction(key, patch, state) {
|
|
406
486
|
return this.transitionUnlocked(key, patch, state);
|
|
407
487
|
}
|
|
408
488
|
prune(consumedKeys = []) {
|
|
409
|
-
return this.
|
|
489
|
+
return this.runLocked(() => this.pruneUnlocked(consumedKeys));
|
|
410
490
|
}
|
|
411
491
|
pruneUnlocked(consumedKeys = []) {
|
|
412
492
|
this.reload();
|
|
@@ -426,7 +506,7 @@ class PiCompactionBreaker {
|
|
|
426
506
|
return count;
|
|
427
507
|
}
|
|
428
508
|
forceFallback(key, reason) {
|
|
429
|
-
return this.
|
|
509
|
+
return this.runLocked(() => {
|
|
430
510
|
this.reload();
|
|
431
511
|
const current = this.records.get(key);
|
|
432
512
|
if (!current)
|
|
@@ -2,10 +2,14 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { BUNDLED_PI_VERSION } from "./pi-provider-config.mjs";
|
|
6
|
+
/** Fallback values used while importing a Pi profile, before Pi reports its model. */
|
|
5
7
|
export const PI_CONTEXT_WINDOW = 272_000;
|
|
6
8
|
export const COMPACTION_RESERVE_TOKENS = 40_800;
|
|
7
9
|
export const COMPACTION_KEEP_RECENT_TOKENS = 20_000;
|
|
8
10
|
export const PI_COMPACTION_THRESHOLD = PI_CONTEXT_WINDOW - COMPACTION_RESERVE_TOKENS;
|
|
11
|
+
const COMPACTION_RESERVE_RATIO_PERCENT = 15;
|
|
12
|
+
const MAX_KEEP_RECENT_TOKENS = 20_000;
|
|
9
13
|
export const PI_COMPACTION_RECOVERY_VERSION = 1;
|
|
10
14
|
export const PI_COMPACTION_RECOVERY_FILE = "piCompactionRecovery.json";
|
|
11
15
|
const AGENT_ID = /^cli_[A-Za-z0-9]+$/;
|
|
@@ -33,20 +37,44 @@ function requireFiniteBoolean(value, name) {
|
|
|
33
37
|
throw new Error(`${name} must be a boolean`);
|
|
34
38
|
return value;
|
|
35
39
|
}
|
|
36
|
-
export function
|
|
37
|
-
|
|
40
|
+
export function calculatePiCompactionSettings(contextWindow) {
|
|
41
|
+
if (!Number.isSafeInteger(contextWindow) || contextWindow <= 0) {
|
|
42
|
+
throw new Error("Pi effective context window must be a positive safe integer");
|
|
43
|
+
}
|
|
44
|
+
const reserveTokens = Math.min(contextWindow - 1, Math.ceil(contextWindow * COMPACTION_RESERVE_RATIO_PERCENT / 100));
|
|
45
|
+
const threshold = contextWindow - reserveTokens;
|
|
46
|
+
if (reserveTokens < 1 || threshold <= 1)
|
|
47
|
+
throw new Error("Pi effective context window is too small for safe compaction");
|
|
48
|
+
const keepRecentTokens = Math.min(MAX_KEEP_RECENT_TOKENS, threshold - 1);
|
|
49
|
+
return { reserveTokens, keepRecentTokens, threshold };
|
|
50
|
+
}
|
|
51
|
+
export function isPiNativeCompactionRequired(contextTokens, contextWindow = PI_CONTEXT_WINDOW) {
|
|
52
|
+
if (!Number.isFinite(contextTokens))
|
|
53
|
+
return false;
|
|
54
|
+
try {
|
|
55
|
+
return contextTokens > calculatePiCompactionSettings(contextWindow).threshold;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
38
60
|
}
|
|
39
61
|
export function assertEffectivePiCompactionSettings(settings) {
|
|
40
62
|
const contextWindow = settings.contextWindow ?? settings.model?.contextWindow;
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
const expected = (() => {
|
|
64
|
+
try {
|
|
65
|
+
return calculatePiCompactionSettings(contextWindow);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new Error("Pi effective context window must be a positive safe integer");
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
43
71
|
if (settings.compaction?.enabled !== true)
|
|
44
72
|
throw new Error("Pi native compaction must be enabled");
|
|
45
|
-
if (settings.compaction.reserveTokens !==
|
|
46
|
-
throw new Error(`Pi reserveTokens must be exactly ${
|
|
73
|
+
if (settings.compaction.reserveTokens !== expected.reserveTokens) {
|
|
74
|
+
throw new Error(`Pi reserveTokens must be exactly ${expected.reserveTokens} for context window ${contextWindow}`);
|
|
47
75
|
}
|
|
48
|
-
if (settings.compaction.keepRecentTokens !==
|
|
49
|
-
throw new Error(`Pi keepRecentTokens must be exactly ${
|
|
76
|
+
if (settings.compaction.keepRecentTokens !== expected.keepRecentTokens) {
|
|
77
|
+
throw new Error(`Pi keepRecentTokens must be exactly ${expected.keepRecentTokens} for context window ${contextWindow}`);
|
|
50
78
|
}
|
|
51
79
|
}
|
|
52
80
|
export function hasProjectPiCompactionOverride(settings) {
|
|
@@ -113,6 +141,7 @@ function ensureDirectoryChain(directory) {
|
|
|
113
141
|
export function ensureOwnedPiAgentDirectory(root, agentId) {
|
|
114
142
|
if (!AGENT_ID.test(agentId))
|
|
115
143
|
throw new Error("invalid Pi Agent ID");
|
|
144
|
+
ensureDirectoryChain(root);
|
|
116
145
|
const resolvedRoot = path.resolve(root);
|
|
117
146
|
ensureDirectory(resolvedRoot);
|
|
118
147
|
const parent = path.join(resolvedRoot, "pi-agents");
|
|
@@ -124,15 +153,34 @@ export function ensureOwnedPiAgentDirectory(root, agentId) {
|
|
|
124
153
|
export function ownedPiSettings(directory) {
|
|
125
154
|
return path.join(directory, "settings.json");
|
|
126
155
|
}
|
|
127
|
-
export function prepareOwnedPiDirectory(directory) {
|
|
156
|
+
export function prepareOwnedPiDirectory(directory, compaction) {
|
|
128
157
|
ensureDirectoryChain(path.dirname(directory));
|
|
129
158
|
ensureDirectory(path.dirname(directory));
|
|
130
159
|
ensureDirectory(directory);
|
|
131
|
-
writeOwnedPiSettings(directory);
|
|
160
|
+
writeOwnedPiSettings(directory, compaction);
|
|
132
161
|
return directory;
|
|
133
162
|
}
|
|
134
|
-
export function
|
|
135
|
-
|
|
163
|
+
export function mergeOwnedPiSettings(value, compaction = {
|
|
164
|
+
reserveTokens: COMPACTION_RESERVE_TOKENS,
|
|
165
|
+
keepRecentTokens: COMPACTION_KEEP_RECENT_TOKENS,
|
|
166
|
+
threshold: PI_COMPACTION_THRESHOLD,
|
|
167
|
+
}) {
|
|
168
|
+
if (value !== undefined && (!value || typeof value !== "object" || Array.isArray(value))) {
|
|
169
|
+
throw new Error("Pi owned settings are invalid");
|
|
170
|
+
}
|
|
171
|
+
const object = (value || {});
|
|
172
|
+
const existing = object.compaction;
|
|
173
|
+
if (existing !== undefined && (!existing || typeof existing !== "object" || Array.isArray(existing))) {
|
|
174
|
+
throw new Error("Pi owned compaction settings are invalid");
|
|
175
|
+
}
|
|
176
|
+
return { ...object, compaction: {
|
|
177
|
+
...existing, enabled: true,
|
|
178
|
+
reserveTokens: compaction.reserveTokens, keepRecentTokens: compaction.keepRecentTokens,
|
|
179
|
+
} };
|
|
180
|
+
}
|
|
181
|
+
export function writeOwnedPiSettings(directory, compaction) {
|
|
182
|
+
ensureDirectoryChain(path.dirname(directory));
|
|
183
|
+
ensureDirectory(directory);
|
|
136
184
|
const file = ownedPiSettings(directory);
|
|
137
185
|
try {
|
|
138
186
|
const existing = fs.lstatSync(file);
|
|
@@ -143,15 +191,17 @@ export function writeOwnedPiSettings(directory) {
|
|
|
143
191
|
if (error.code !== "ENOENT")
|
|
144
192
|
throw error;
|
|
145
193
|
}
|
|
194
|
+
let value;
|
|
195
|
+
try {
|
|
196
|
+
value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (error.code !== "ENOENT")
|
|
200
|
+
throw new Error("Pi owned settings are invalid");
|
|
201
|
+
}
|
|
146
202
|
const temporary = path.join(directory, `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
147
203
|
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 });
|
|
204
|
+
fs.writeFileSync(temporary, `${JSON.stringify(mergeOwnedPiSettings(value, compaction), null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
155
205
|
fs.renameSync(temporary, file);
|
|
156
206
|
fs.chmodSync(file, 0o600);
|
|
157
207
|
}
|
|
@@ -163,6 +213,8 @@ export function writeOwnedPiSettings(directory) {
|
|
|
163
213
|
}
|
|
164
214
|
}
|
|
165
215
|
export function readOwnedPiSettings(directory) {
|
|
216
|
+
ensureDirectoryChain(path.dirname(directory));
|
|
217
|
+
assertOwnedDirectory(directory);
|
|
166
218
|
const file = ownedPiSettings(directory);
|
|
167
219
|
const stat = fs.lstatSync(file);
|
|
168
220
|
if (stat.isSymbolicLink() || !stat.isFile())
|
|
@@ -180,28 +232,48 @@ export function readOwnedPiSettings(directory) {
|
|
|
180
232
|
}
|
|
181
233
|
return { compaction: { enabled, reserveTokens: values.reserveTokens, keepRecentTokens: values.keepRecentTokens } };
|
|
182
234
|
}
|
|
235
|
+
export function parsePiExecutableVersion(output) {
|
|
236
|
+
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
237
|
+
if (lines.length !== 1)
|
|
238
|
+
throw new Error("Pi executable version output must contain exactly one version line");
|
|
239
|
+
const line = lines[0];
|
|
240
|
+
// Pi's bare version is canonical; permit only the fixed official display prefix.
|
|
241
|
+
const escapedVersion = BUNDLED_PI_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
242
|
+
if (!new RegExp(`^(?:${escapedVersion}|pi(?:-coding-agent)?(?:\\s+version)?\\s+v?${escapedVersion})$`, "i").test(line)) {
|
|
243
|
+
throw new Error(`Pi executable version must be exactly ${BUNDLED_PI_VERSION}`);
|
|
244
|
+
}
|
|
245
|
+
return BUNDLED_PI_VERSION;
|
|
246
|
+
}
|
|
183
247
|
export function verifyPiCapabilities(capabilities) {
|
|
184
|
-
if (
|
|
185
|
-
throw new Error(
|
|
248
|
+
if (capabilities.version !== BUNDLED_PI_VERSION)
|
|
249
|
+
throw new Error(`trusted Pi version ${BUNDLED_PI_VERSION} is required`);
|
|
250
|
+
if (capabilities.distribution === "external" && capabilities.trustedProtocol === true) {
|
|
251
|
+
throw new Error("external Pi cannot use the bundled trusted protocol bypass");
|
|
186
252
|
}
|
|
187
253
|
const contextWindow = capabilities.contextWindow ?? capabilities.model?.contextWindow;
|
|
188
|
-
|
|
189
|
-
|
|
254
|
+
const expected = (() => {
|
|
255
|
+
try {
|
|
256
|
+
return calculatePiCompactionSettings(contextWindow);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
throw new Error("Pi capability context window is invalid");
|
|
260
|
+
}
|
|
261
|
+
})();
|
|
190
262
|
if (requireFiniteBoolean(capabilities.autoCompactionEnabled, "Pi autoCompactionEnabled") !== true) {
|
|
191
263
|
throw new Error("Pi effective native compaction is disabled");
|
|
192
264
|
}
|
|
193
265
|
if (capabilities.compactRpc !== true)
|
|
194
266
|
throw new Error("Pi compact RPC capability is missing");
|
|
195
|
-
if (capabilities.trustedProtocol
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
267
|
+
if (capabilities.distribution === "builtin" && capabilities.trustedProtocol === true)
|
|
268
|
+
return;
|
|
269
|
+
if (capabilities.reserveTokens !== expected.reserveTokens
|
|
270
|
+
|| capabilities.keepRecentTokens !== expected.keepRecentTokens) {
|
|
271
|
+
throw new Error(`Pi external effective compaction reserve/keep settings are unproven for context window ${contextWindow}`);
|
|
272
|
+
}
|
|
273
|
+
const events = new Set(capabilities.events || []);
|
|
274
|
+
for (const event of REQUIRED_PI_EVENTS)
|
|
275
|
+
if (!events.has(event))
|
|
276
|
+
throw new Error(`Pi capability event is unproven: ${event}`);
|
|
205
277
|
}
|
|
206
278
|
function stateFile(root) {
|
|
207
279
|
return path.join(root, PI_COMPACTION_RECOVERY_FILE);
|
|
@@ -277,9 +349,14 @@ export class PiCompactionBreaker {
|
|
|
277
349
|
this.clock = options.now ?? (() => new Date().toISOString());
|
|
278
350
|
this.records = new Map(this.read().records.map((record) => [record.key, record]));
|
|
279
351
|
this.retentionMs = options.retentionMs ?? 30 * 24 * 60 * 60 * 1000;
|
|
280
|
-
this.withLock = options.withLock
|
|
352
|
+
this.withLock = options.withLock;
|
|
281
353
|
}
|
|
282
354
|
retentionMs;
|
|
355
|
+
runLocked(operation) {
|
|
356
|
+
if (!this.withLock)
|
|
357
|
+
throw new Error("Pi compaction breaker requires the canonical Agent state lock");
|
|
358
|
+
return this.withLock(operation);
|
|
359
|
+
}
|
|
283
360
|
read() {
|
|
284
361
|
try {
|
|
285
362
|
const stat = fs.lstatSync(this.file);
|
|
@@ -332,13 +409,13 @@ export class PiCompactionBreaker {
|
|
|
332
409
|
this.records = new Map(this.read().records.map((record) => [record.key, record]));
|
|
333
410
|
}
|
|
334
411
|
get(key) {
|
|
335
|
-
return this.
|
|
412
|
+
return this.runLocked(() => { this.reload(); const record = this.records.get(key); return record ? { ...record } : undefined; });
|
|
336
413
|
}
|
|
337
414
|
listNonTerminal() {
|
|
338
|
-
return this.
|
|
415
|
+
return this.runLocked(() => { this.reload(); return [...this.records.values()].filter((record) => !TERMINAL_STATES.has(record.state)).map((record) => ({ ...record })); });
|
|
339
416
|
}
|
|
340
417
|
save(record) {
|
|
341
|
-
this.
|
|
418
|
+
this.runLocked(() => { this.reload(); const checked = safeRecord({ ...record, updatedAt: this.clock() }); this.records.set(checked.key, checked); this.persist(); });
|
|
342
419
|
}
|
|
343
420
|
transitionUnlocked(key, patch, state) {
|
|
344
421
|
this.reload();
|
|
@@ -381,13 +458,13 @@ export class PiCompactionBreaker {
|
|
|
381
458
|
return { ...next };
|
|
382
459
|
}
|
|
383
460
|
transition(key, patch, state) {
|
|
384
|
-
return this.
|
|
461
|
+
return this.runLocked(() => this.transitionUnlocked(key, patch, state));
|
|
385
462
|
}
|
|
386
463
|
transitionInTransaction(key, patch, state) {
|
|
387
464
|
return this.transitionUnlocked(key, patch, state);
|
|
388
465
|
}
|
|
389
466
|
prune(consumedKeys = []) {
|
|
390
|
-
return this.
|
|
467
|
+
return this.runLocked(() => this.pruneUnlocked(consumedKeys));
|
|
391
468
|
}
|
|
392
469
|
pruneUnlocked(consumedKeys = []) {
|
|
393
470
|
this.reload();
|
|
@@ -407,7 +484,7 @@ export class PiCompactionBreaker {
|
|
|
407
484
|
return count;
|
|
408
485
|
}
|
|
409
486
|
forceFallback(key, reason) {
|
|
410
|
-
return this.
|
|
487
|
+
return this.runLocked(() => {
|
|
411
488
|
this.reload();
|
|
412
489
|
const current = this.records.get(key);
|
|
413
490
|
if (!current)
|
|
@@ -74,7 +74,7 @@ const discoveryCache = new Map();
|
|
|
74
74
|
async function discoverPiModelCatalog(options) {
|
|
75
75
|
if (!options.spawn) {
|
|
76
76
|
const command = options.command ?? options.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
77
|
-
const key = `${command}|${options.env?.PI_CODING_AGENT_DIR ?? ""}|${options.agentDir ?? ""}`;
|
|
77
|
+
const key = `${command}|${(options.commandArgs ?? []).join("\0")}|${options.env?.PI_CODING_AGENT_DIR ?? ""}|${options.agentDir ?? ""}`;
|
|
78
78
|
const cached = discoveryCache.get(key);
|
|
79
79
|
if (cached)
|
|
80
80
|
return cached;
|
|
@@ -91,7 +91,7 @@ async function discoverPiModelCatalog(options) {
|
|
|
91
91
|
async function discoverPiModelCatalogUncached(options) {
|
|
92
92
|
const spawn = options.spawn ?? ((command, args, spawnOptions) => (0, node_child_process_1.spawn)(command, [...args], spawnOptions));
|
|
93
93
|
const command = options.command ?? options.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
94
|
-
const child = spawn(command, ["--mode", "rpc", "--no-session"], {
|
|
94
|
+
const child = spawn(command, [...(options.commandArgs ?? []), "--mode", "rpc", "--no-session"], {
|
|
95
95
|
cwd: options.cwd,
|
|
96
96
|
env: { ...process.env, ...options.env, ...(options.agentDir ? { PI_CODING_AGENT_DIR: path.resolve(options.agentDir) } : {}), NO_COLOR: "1" },
|
|
97
97
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -33,7 +33,7 @@ const discoveryCache = new Map();
|
|
|
33
33
|
export async function discoverPiModelCatalog(options) {
|
|
34
34
|
if (!options.spawn) {
|
|
35
35
|
const command = options.command ?? options.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
36
|
-
const key = `${command}|${options.env?.PI_CODING_AGENT_DIR ?? ""}|${options.agentDir ?? ""}`;
|
|
36
|
+
const key = `${command}|${(options.commandArgs ?? []).join("\0")}|${options.env?.PI_CODING_AGENT_DIR ?? ""}|${options.agentDir ?? ""}`;
|
|
37
37
|
const cached = discoveryCache.get(key);
|
|
38
38
|
if (cached)
|
|
39
39
|
return cached;
|
|
@@ -50,7 +50,7 @@ export async function discoverPiModelCatalog(options) {
|
|
|
50
50
|
async function discoverPiModelCatalogUncached(options) {
|
|
51
51
|
const spawn = options.spawn ?? ((command, args, spawnOptions) => nodeSpawn(command, [...args], spawnOptions));
|
|
52
52
|
const command = options.command ?? options.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
53
|
-
const child = spawn(command, ["--mode", "rpc", "--no-session"], {
|
|
53
|
+
const child = spawn(command, [...(options.commandArgs ?? []), "--mode", "rpc", "--no-session"], {
|
|
54
54
|
cwd: options.cwd,
|
|
55
55
|
env: { ...process.env, ...options.env, ...(options.agentDir ? { PI_CODING_AGENT_DIR: path.resolve(options.agentDir) } : {}), NO_COLOR: "1" },
|
|
56
56
|
stdio: ["pipe", "pipe", "pipe"],
|