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.
Files changed (35) hide show
  1. package/README.md +5 -1
  2. package/artifacts/release/THIRD_PARTY_NOTICES.txt +218 -228
  3. package/assets/readme/larkin-github-cover.png +0 -0
  4. package/dist/agent/agent-state-store.cjs +1 -2
  5. package/dist/agent/agent-state-store.mjs +1 -2
  6. package/dist/app/agent-config.mjs +73 -6
  7. package/dist/app/agent-readiness.cjs +22 -2
  8. package/dist/app/agent-readiness.mjs +20 -2
  9. package/dist/app/binary-entry.mjs +6 -0
  10. package/dist/app/cli.mjs +4 -0
  11. package/dist/app/run.mjs +8 -0
  12. package/dist/app/runtime-process.mjs +13 -1
  13. package/dist/dashboard/dashboard-view-model.mjs +12 -3
  14. package/dist/feishu/host-shell.cjs +33 -10
  15. package/dist/feishu/host-shell.mjs +34 -11
  16. package/dist/platform/config.cjs +406 -33
  17. package/dist/platform/config.mjs +405 -33
  18. package/dist/platform/process-boundary-trace.cjs +108 -0
  19. package/dist/platform/process-boundary-trace.mjs +102 -0
  20. package/dist/runtime/pi-artifact-provenance.cjs +147 -0
  21. package/dist/runtime/pi-artifact-provenance.mjs +139 -0
  22. package/dist/runtime/pi-compaction-recovery.cjs +120 -40
  23. package/dist/runtime/pi-compaction-recovery.mjs +117 -40
  24. package/dist/runtime/pi-model-catalog.cjs +2 -2
  25. package/dist/runtime/pi-model-catalog.mjs +2 -2
  26. package/dist/runtime/pi-profile-migration.cjs +556 -0
  27. package/dist/runtime/pi-profile-migration.mjs +515 -0
  28. package/dist/runtime/pi-provider-config.cjs +1 -1
  29. package/dist/runtime/pi-provider-config.mjs +1 -1
  30. package/dist/runtime/runtime-adapters.mjs +97 -11
  31. package/dist/runtime/runtime-host.cjs +162 -39
  32. package/dist/runtime/runtime-host.mjs +163 -40
  33. package/dist/runtime/runtime-readiness.cjs +6 -4
  34. package/dist/runtime/runtime-readiness.mjs +6 -4
  35. 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 isPiNativeCompactionRequired(contextTokens) {
56
- return Number.isFinite(contextTokens) && contextTokens > exports.PI_COMPACTION_THRESHOLD;
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
- if (contextWindow !== exports.PI_CONTEXT_WINDOW)
61
- throw new Error(`Pi effective context window must be exactly ${exports.PI_CONTEXT_WINDOW}`);
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 !== exports.COMPACTION_RESERVE_TOKENS) {
65
- throw new Error(`Pi reserveTokens must be exactly ${exports.COMPACTION_RESERVE_TOKENS}`);
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 !== exports.COMPACTION_KEEP_RECENT_TOKENS) {
68
- throw new Error(`Pi keepRecentTokens must be exactly ${exports.COMPACTION_KEEP_RECENT_TOKENS}`);
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 writeOwnedPiSettings(directory) {
154
- assertOwnedDirectory(directory);
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 (!/(?:^|\D)0\.83\.0(?:$|\D)/.test(capabilities.version || "")) {
204
- throw new Error("trusted Pi version 0.83.0 is required");
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
- if (contextWindow !== exports.PI_CONTEXT_WINDOW)
208
- throw new Error("Pi capability context window is not 272000");
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 !== 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
- }
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 ?? ((operation) => operation());
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.withLock(() => { this.reload(); const record = this.records.get(key); return record ? { ...record } : undefined; });
434
+ return this.runLocked(() => { this.reload(); const record = this.records.get(key); return record ? { ...record } : undefined; });
355
435
  }
356
436
  listNonTerminal() {
357
- return this.withLock(() => { this.reload(); return [...this.records.values()].filter((record) => !TERMINAL_STATES.has(record.state)).map((record) => ({ ...record })); });
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.withLock(() => { this.reload(); const checked = safeRecord({ ...record, updatedAt: this.clock() }); this.records.set(checked.key, checked); this.persist(); });
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.withLock(() => this.transitionUnlocked(key, patch, state));
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.withLock(() => this.pruneUnlocked(consumedKeys));
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.withLock(() => {
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 isPiNativeCompactionRequired(contextTokens) {
37
- return Number.isFinite(contextTokens) && contextTokens > PI_COMPACTION_THRESHOLD;
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
- if (contextWindow !== PI_CONTEXT_WINDOW)
42
- throw new Error(`Pi effective context window must be exactly ${PI_CONTEXT_WINDOW}`);
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 !== COMPACTION_RESERVE_TOKENS) {
46
- throw new Error(`Pi reserveTokens must be exactly ${COMPACTION_RESERVE_TOKENS}`);
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 !== COMPACTION_KEEP_RECENT_TOKENS) {
49
- throw new Error(`Pi keepRecentTokens must be exactly ${COMPACTION_KEEP_RECENT_TOKENS}`);
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 writeOwnedPiSettings(directory) {
135
- assertOwnedDirectory(directory);
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 (!/(?:^|\D)0\.83\.0(?:$|\D)/.test(capabilities.version || "")) {
185
- throw new Error("trusted Pi version 0.83.0 is required");
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
- if (contextWindow !== PI_CONTEXT_WINDOW)
189
- throw new Error("Pi capability context window is not 272000");
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 !== 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
- }
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 ?? ((operation) => operation());
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.withLock(() => { this.reload(); const record = this.records.get(key); return record ? { ...record } : undefined; });
412
+ return this.runLocked(() => { this.reload(); const record = this.records.get(key); return record ? { ...record } : undefined; });
336
413
  }
337
414
  listNonTerminal() {
338
- return this.withLock(() => { this.reload(); return [...this.records.values()].filter((record) => !TERMINAL_STATES.has(record.state)).map((record) => ({ ...record })); });
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.withLock(() => { this.reload(); const checked = safeRecord({ ...record, updatedAt: this.clock() }); this.records.set(checked.key, checked); this.persist(); });
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.withLock(() => this.transitionUnlocked(key, patch, state));
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.withLock(() => this.pruneUnlocked(consumedKeys));
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.withLock(() => {
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"],