larkin 0.5.2 → 0.5.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.3
4
+
5
+ External Pi initialization now runs with bounded concurrency and recovers from isolated startup-probe timeouts through the existing retry policy. Shutdown waits for pending initialization and closes late sessions. Generic Runtime and delivery failures show unavailable instead of incompatible, while explicit prerequisite failures and useful provider diagnostics are preserved. Recovery workflow tests wait for delivery consumption and Inbox watermarks to converge and isolate temporary state.
6
+
3
7
  ## 0.5.2
4
8
 
5
9
  External pi now runs with the user's own Pi home; compaction settings move to the Agent workspace's `.pi/settings.json`; 0.5.0/0.5.1 started external pi with an empty agent dir and saw no logins. Stock pi 0.84.x does not emit `get_state.compactionCapabilities` (that handshake only existed for the bundled build); Larkin accepts the absence and keeps native compaction via the workspace settings file, while a present handshake must still match exactly.
@@ -57,6 +57,7 @@ const host_channel_business_js_1 = require("./host-channel-business.cjs");
57
57
  const interaction_orchestrator_js_1 = require("./interaction-orchestrator.cjs");
58
58
  const message_policy_js_1 = require("./message-policy.cjs");
59
59
  const runtime_readiness_js_1 = require("../runtime/runtime-readiness.cjs");
60
+ const provider_error_classifier_js_1 = require("../runtime/provider-error-classifier.cjs");
60
61
  const callback_capability_js_1 = require("../platform/callback-capability.cjs");
61
62
  const config_js_1 = require("../platform/config.cjs");
62
63
  const internal_command_js_1 = require("../app/internal-command.cjs");
@@ -119,6 +120,9 @@ function memberNamesFromPayloads(payloads) {
119
120
  return names;
120
121
  }
121
122
  function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
123
+ function safeRuntimeStatusDiagnostic(error) {
124
+ return (0, provider_error_classifier_js_1.safeProviderDiagnostic)(error, "Runtime entered an error state", 500);
125
+ }
122
126
  function agentConfigSignature(agent) {
123
127
  return JSON.stringify({
124
128
  agentId: agent.agentId, runtime: agent.runtime, model: agent.model, effort: agent.effort ?? null,
@@ -233,17 +237,40 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
233
237
  }
234
238
  hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt } });
235
239
  };
240
+ const projectFallbackRuntimeReadiness = (agent, observedAt, fallback, preserveCurrentUnavailableDiagnostic = false) => {
241
+ const failure = unresolvedCurrentAuth(agent);
242
+ if (failure) {
243
+ hostState.updateStatus(agent, {
244
+ runtimeReadiness: { ...(0, runtime_readiness_js_1.readinessForPersistedAuthFailure)(failure), observedAt },
245
+ });
246
+ return;
247
+ }
248
+ const currentReadiness = hostState.readStatus(agent).runtimeReadiness;
249
+ const observedMs = Date.parse(String(currentReadiness?.observedAt || ""));
250
+ const daemonStartedMs = Date.parse(daemonStartedAt);
251
+ const currentEpoch = currentReadiness?.runtime === agent.runtime
252
+ && Number.isFinite(observedMs) && Number.isFinite(daemonStartedMs) && observedMs >= daemonStartedMs;
253
+ if (currentEpoch && (currentReadiness?.state === "missing" || currentReadiness?.state === "unauthenticated" || currentReadiness?.state === "incompatible"))
254
+ return;
255
+ if (currentEpoch && preserveCurrentUnavailableDiagnostic && currentReadiness?.state === "unavailable"
256
+ && typeof currentReadiness.reason === "string" && currentReadiness.reason.trim()) {
257
+ hostState.updateStatus(agent, {
258
+ runtimeReadiness: { ...currentReadiness, runtime: agent.runtime, state: "unavailable", observedAt },
259
+ });
260
+ return;
261
+ }
262
+ hostState.updateStatus(agent, {
263
+ runtimeReadiness: { runtime: agent.runtime, ...fallback, observedAt },
264
+ });
265
+ };
236
266
  const recordInboundDeliveryFailure = (agent, code) => {
237
267
  const at = new Date().toISOString();
238
268
  const reason = "Inbound Runtime delivery failed non-retryably; durable Inbox/ledger state is retained for recovery.";
239
269
  const nextAction = "Inspect the delivery/status error, correct the Runtime or canonical Inbox state, then restart to replay safely.";
240
- const currentReadiness = hostState.readStatus(agent).runtimeReadiness;
241
270
  hostState.updateStatus(agent, {
242
271
  inboundDeliveryHealth: { state: "error", code, at, reason, nextAction },
243
- ...(currentReadiness?.state === "unauthenticated" ? {} : {
244
- runtimeReadiness: { runtime: agent.runtime, state: "incompatible", reason, nextAction },
245
- }),
246
272
  });
273
+ projectFallbackRuntimeReadiness(agent, at, { state: "unavailable", reason, nextAction }, true);
247
274
  hostState.recordStatusError(agent, `${reason} ${nextAction} code=${code}`);
248
275
  };
249
276
  const agentStates = new Map();
@@ -1312,15 +1339,21 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1312
1339
  if (message.type === "agent-status") {
1313
1340
  log("agent:status", message.agentId, message.status);
1314
1341
  const observedAt = new Date().toISOString();
1315
- if (message.status === "error" || message.status === "inactive")
1316
- hostState.updateStatus(agent, {
1317
- runtimeReadiness: message.readiness?.state && message.readiness.state !== "ready" ? { ...message.readiness, observedAt } : {
1318
- runtime: agent.runtime,
1319
- state: message.status === "error" ? "incompatible" : "missing",
1320
- reason: message.status === "error" ? message.error || "Runtime entered an error state" : "Runtime is inactive",
1321
- observedAt,
1322
- },
1323
- });
1342
+ if (message.status === "error" || message.status === "inactive") {
1343
+ if (message.readiness?.state && message.readiness.state !== "ready") {
1344
+ hostState.updateStatus(agent, { runtimeReadiness: { ...message.readiness, observedAt } });
1345
+ }
1346
+ else {
1347
+ projectFallbackRuntimeReadiness(agent, observedAt, message.status === "error" ? {
1348
+ state: "unavailable",
1349
+ reason: safeRuntimeStatusDiagnostic(message.error),
1350
+ nextAction: "Inspect the Runtime status error, correct the Runtime availability issue, then retry.",
1351
+ } : {
1352
+ state: "missing",
1353
+ reason: "Runtime is inactive",
1354
+ });
1355
+ }
1356
+ }
1324
1357
  else if (message.readiness) {
1325
1358
  if (message.readiness.state === "ready" && unresolvedCurrentAuth(agent)) {
1326
1359
  projectReadyUnlessUnresolvedAuth(agent, observedAt);
@@ -1338,7 +1371,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1338
1371
  redeliveryTimer.unref?.();
1339
1372
  }
1340
1373
  if (message.status === "error" && message.error)
1341
- hostState.recordStatusError(agent, message.error);
1374
+ hostState.recordStatusError(agent, safeRuntimeStatusDiagnostic(message.error));
1342
1375
  if (message.status === "error" || message.status === "inactive") {
1343
1376
  processingEyes.clear(agent, `agent-status:${message.status}`);
1344
1377
  }
@@ -17,6 +17,7 @@ import { HostChannelBusiness } from "./host-channel-business.mjs";
17
17
  import { HostInteractionOrchestrator } from "./interaction-orchestrator.mjs";
18
18
  import { targetFor } from "./message-policy.mjs";
19
19
  import { authFailureAppliesTo, parsePersistedAuthFailure, providerAuthenticationFailureReadiness, readinessForPersistedAuthFailure, RuntimePrerequisiteError, } from "../runtime/runtime-readiness.mjs";
20
+ import { safeProviderDiagnostic } from "../runtime/provider-error-classifier.mjs";
20
21
  import { readDocumentCommentSubscription, verifyCallbackProbe } from "../platform/callback-capability.mjs";
21
22
  import { loadConfig, resolveMentionPolicy } from "../platform/config.mjs";
22
23
  import { processCommandToken } from "../app/internal-command.mjs";
@@ -79,6 +80,9 @@ export function memberNamesFromPayloads(payloads) {
79
80
  return names;
80
81
  }
81
82
  function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
83
+ function safeRuntimeStatusDiagnostic(error) {
84
+ return safeProviderDiagnostic(error, "Runtime entered an error state", 500);
85
+ }
82
86
  function agentConfigSignature(agent) {
83
87
  return JSON.stringify({
84
88
  agentId: agent.agentId, runtime: agent.runtime, model: agent.model, effort: agent.effort ?? null,
@@ -193,17 +197,40 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
193
197
  }
194
198
  hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt } });
195
199
  };
200
+ const projectFallbackRuntimeReadiness = (agent, observedAt, fallback, preserveCurrentUnavailableDiagnostic = false) => {
201
+ const failure = unresolvedCurrentAuth(agent);
202
+ if (failure) {
203
+ hostState.updateStatus(agent, {
204
+ runtimeReadiness: { ...readinessForPersistedAuthFailure(failure), observedAt },
205
+ });
206
+ return;
207
+ }
208
+ const currentReadiness = hostState.readStatus(agent).runtimeReadiness;
209
+ const observedMs = Date.parse(String(currentReadiness?.observedAt || ""));
210
+ const daemonStartedMs = Date.parse(daemonStartedAt);
211
+ const currentEpoch = currentReadiness?.runtime === agent.runtime
212
+ && Number.isFinite(observedMs) && Number.isFinite(daemonStartedMs) && observedMs >= daemonStartedMs;
213
+ if (currentEpoch && (currentReadiness?.state === "missing" || currentReadiness?.state === "unauthenticated" || currentReadiness?.state === "incompatible"))
214
+ return;
215
+ if (currentEpoch && preserveCurrentUnavailableDiagnostic && currentReadiness?.state === "unavailable"
216
+ && typeof currentReadiness.reason === "string" && currentReadiness.reason.trim()) {
217
+ hostState.updateStatus(agent, {
218
+ runtimeReadiness: { ...currentReadiness, runtime: agent.runtime, state: "unavailable", observedAt },
219
+ });
220
+ return;
221
+ }
222
+ hostState.updateStatus(agent, {
223
+ runtimeReadiness: { runtime: agent.runtime, ...fallback, observedAt },
224
+ });
225
+ };
196
226
  const recordInboundDeliveryFailure = (agent, code) => {
197
227
  const at = new Date().toISOString();
198
228
  const reason = "Inbound Runtime delivery failed non-retryably; durable Inbox/ledger state is retained for recovery.";
199
229
  const nextAction = "Inspect the delivery/status error, correct the Runtime or canonical Inbox state, then restart to replay safely.";
200
- const currentReadiness = hostState.readStatus(agent).runtimeReadiness;
201
230
  hostState.updateStatus(agent, {
202
231
  inboundDeliveryHealth: { state: "error", code, at, reason, nextAction },
203
- ...(currentReadiness?.state === "unauthenticated" ? {} : {
204
- runtimeReadiness: { runtime: agent.runtime, state: "incompatible", reason, nextAction },
205
- }),
206
232
  });
233
+ projectFallbackRuntimeReadiness(agent, at, { state: "unavailable", reason, nextAction }, true);
207
234
  hostState.recordStatusError(agent, `${reason} ${nextAction} code=${code}`);
208
235
  };
209
236
  const agentStates = new Map();
@@ -1272,15 +1299,21 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1272
1299
  if (message.type === "agent-status") {
1273
1300
  log("agent:status", message.agentId, message.status);
1274
1301
  const observedAt = new Date().toISOString();
1275
- if (message.status === "error" || message.status === "inactive")
1276
- hostState.updateStatus(agent, {
1277
- runtimeReadiness: message.readiness?.state && message.readiness.state !== "ready" ? { ...message.readiness, observedAt } : {
1278
- runtime: agent.runtime,
1279
- state: message.status === "error" ? "incompatible" : "missing",
1280
- reason: message.status === "error" ? message.error || "Runtime entered an error state" : "Runtime is inactive",
1281
- observedAt,
1282
- },
1283
- });
1302
+ if (message.status === "error" || message.status === "inactive") {
1303
+ if (message.readiness?.state && message.readiness.state !== "ready") {
1304
+ hostState.updateStatus(agent, { runtimeReadiness: { ...message.readiness, observedAt } });
1305
+ }
1306
+ else {
1307
+ projectFallbackRuntimeReadiness(agent, observedAt, message.status === "error" ? {
1308
+ state: "unavailable",
1309
+ reason: safeRuntimeStatusDiagnostic(message.error),
1310
+ nextAction: "Inspect the Runtime status error, correct the Runtime availability issue, then retry.",
1311
+ } : {
1312
+ state: "missing",
1313
+ reason: "Runtime is inactive",
1314
+ });
1315
+ }
1316
+ }
1284
1317
  else if (message.readiness) {
1285
1318
  if (message.readiness.state === "ready" && unresolvedCurrentAuth(agent)) {
1286
1319
  projectReadyUnlessUnresolvedAuth(agent, observedAt);
@@ -1298,7 +1331,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1298
1331
  redeliveryTimer.unref?.();
1299
1332
  }
1300
1333
  if (message.status === "error" && message.error)
1301
- hostState.recordStatusError(agent, message.error);
1334
+ hostState.recordStatusError(agent, safeRuntimeStatusDiagnostic(message.error));
1302
1335
  if (message.status === "error" || message.status === "inactive") {
1303
1336
  processingEyes.clear(agent, `agent-status:${message.status}`);
1304
1337
  }
@@ -1,11 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON = exports.CANONICAL_CONTEXT_WINDOW_MESSAGE = exports.CODEX_ERROR_PREFIX = void 0;
4
+ exports.safeProviderDiagnostic = safeProviderDiagnostic;
4
5
  exports.buildStrictProviderErrorInput = buildStrictProviderErrorInput;
5
6
  exports.classifyStrictProviderError = classifyStrictProviderError;
6
7
  exports.CODEX_ERROR_PREFIX = "Codex error: ";
7
8
  exports.CANONICAL_CONTEXT_WINDOW_MESSAGE = "Your input exceeds the context window of this model. Please adjust your input and try again.";
8
9
  exports.INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON = "provider rejected the input because the context window was exceeded";
10
+ /** Redact credentials and local filesystem locations from provider diagnostics kept in status. */
11
+ function safeProviderDiagnostic(value, fallback, max = 2_000) {
12
+ const source = typeof value === "string" ? value : fallback;
13
+ return source
14
+ .replace(/\b(?:authorization|proxy-authorization)\s*[:=]\s*(?:Bearer\s+)?[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
15
+ .replace(/\b(?:cookie|set-cookie)\s*[:=]\s*(?:[^;\s,]+(?:\s*;\s*[^;\s,]+)*)/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
16
+ .replace(/\b(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|token|secret|password)\s*[:=]\s*[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
17
+ .replace(/(["'](?:authorization|cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password)["']\s*:\s*)["'][^"']*["']/gi, "$1\"[redacted]\"")
18
+ .replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, "Bearer [redacted]")
19
+ .replace(/\b(?:sk|ghp|github_pat)-?[A-Za-z0-9_-]{8,}\b/g, "[redacted]")
20
+ .replace(/(^|[\s"'=])(?:\/(?:Users|home|private|var|tmp|etc)\/[^\s"']+|[A-Za-z]:[\\/][^\s"']+)/g, "$1[redacted-path]")
21
+ .replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, max) || fallback;
22
+ }
9
23
  /** Build the one structured provider-error shape shared by ingestion and recovery. */
10
24
  function buildStrictProviderErrorInput(source) {
11
25
  const upstream = source.upstream && typeof source.upstream === "object" && !Array.isArray(source.upstream)
@@ -1,6 +1,19 @@
1
1
  export const CODEX_ERROR_PREFIX = "Codex error: ";
2
2
  export const CANONICAL_CONTEXT_WINDOW_MESSAGE = "Your input exceeds the context window of this model. Please adjust your input and try again.";
3
3
  export const INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON = "provider rejected the input because the context window was exceeded";
4
+ /** Redact credentials and local filesystem locations from provider diagnostics kept in status. */
5
+ export function safeProviderDiagnostic(value, fallback, max = 2_000) {
6
+ const source = typeof value === "string" ? value : fallback;
7
+ return source
8
+ .replace(/\b(?:authorization|proxy-authorization)\s*[:=]\s*(?:Bearer\s+)?[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
9
+ .replace(/\b(?:cookie|set-cookie)\s*[:=]\s*(?:[^;\s,]+(?:\s*;\s*[^;\s,]+)*)/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
10
+ .replace(/\b(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|token|secret|password)\s*[:=]\s*[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
11
+ .replace(/(["'](?:authorization|cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password)["']\s*:\s*)["'][^"']*["']/gi, "$1\"[redacted]\"")
12
+ .replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, "Bearer [redacted]")
13
+ .replace(/\b(?:sk|ghp|github_pat)-?[A-Za-z0-9_-]{8,}\b/g, "[redacted]")
14
+ .replace(/(^|[\s"'=])(?:\/(?:Users|home|private|var|tmp|etc)\/[^\s"']+|[A-Za-z]:[\\/][^\s"']+)/g, "$1[redacted-path]")
15
+ .replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, max) || fallback;
16
+ }
4
17
  /** Build the one structured provider-error shape shared by ingestion and recovery. */
5
18
  export function buildStrictProviderErrorInput(source) {
6
19
  const upstream = source.upstream && typeof source.upstream === "object" && !Array.isArray(source.upstream)
@@ -12,7 +12,7 @@ import { resolvePiSubagentRecordWatchdogExtensionArg } from "./pi-subagent-recor
12
12
  import { effectivePiStateDir, extractBackgroundPiSubagentDispatch } from "./pi-subagent-ledger.mjs";
13
13
  import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
14
14
  import { classifyPiMissingCredentialRejection, classifyRuntimePrerequisite, missingProviderCredentialReadiness, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
15
- import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, } from "./provider-error-classifier.mjs";
15
+ import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, safeProviderDiagnostic, } from "./provider-error-classifier.mjs";
16
16
  import { assertEffectivePiCompactionSettings, MINIMUM_PI_VERSION, calculatePiCompactionSettings, parsePiExecutableVersion, projectPiSettingsFile, readOwnedPiSettings, verifyPiCapabilities, writeOwnedPiSettings, } from "./pi-compaction-recovery.mjs";
17
17
  /** External pi must see the user's own agent dir; Larkin never injects or honors PI_CODING_AGENT_DIR. */
18
18
  function stripPiCodingAgentDir(env) {
@@ -20,6 +20,31 @@ function stripPiCodingAgentDir(env) {
20
20
  delete next.PI_CODING_AGENT_DIR;
21
21
  return next;
22
22
  }
23
+ const PI_INITIALIZATION_CONCURRENCY = 2;
24
+ /** Limits short-lived Pi startup handshakes without serializing live sessions. */
25
+ function createInitializationLimiter(limit) {
26
+ let active = 0;
27
+ const waiters = [];
28
+ const release = () => {
29
+ const next = waiters.shift();
30
+ if (next)
31
+ next();
32
+ else
33
+ active -= 1;
34
+ };
35
+ return async (operation) => {
36
+ if (active >= limit)
37
+ await new Promise((resolve) => waiters.push(resolve));
38
+ else
39
+ active += 1;
40
+ try {
41
+ return await operation();
42
+ }
43
+ finally {
44
+ release();
45
+ }
46
+ };
47
+ }
23
48
  class EventSession {
24
49
  listeners = new Set();
25
50
  subscribe(listener) {
@@ -780,14 +805,7 @@ class PiSession extends EventSession {
780
805
  }
781
806
  }
782
807
  function safeProviderText(value, fallback) {
783
- const source = typeof value === "string" ? value : fallback;
784
- return source
785
- .replace(/\b(?:authorization|proxy-authorization)\s*[:=]\s*(?:Bearer\s+)?[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
786
- .replace(/\b(?:cookie|set-cookie|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|secret)\s*[:=]\s*[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
787
- .replace(/(["'](?:authorization|cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret)["']\s*:\s*)["'][^"']*["']/gi, "$1\"[redacted]\"")
788
- .replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, "Bearer [redacted]")
789
- .replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[redacted]")
790
- .replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 2_000) || fallback;
808
+ return safeProviderDiagnostic(value, fallback);
791
809
  }
792
810
  function piAssistantProviderError(message) {
793
811
  const diagnostic = [...(Array.isArray(message.diagnostics) ? message.diagnostics : [])]
@@ -1048,8 +1066,16 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
1048
1066
  commandArgs.push(...extensionArgs);
1049
1067
  let probedModel = null;
1050
1068
  if (productionSpawn) {
1051
- // 隔离探测用户自己的 Pi home(无 PI_CODING_AGENT_DIR),再把 15% reserve 写到工作区项目设置。
1052
- probedModel = await discoverEffectivePiContextWindow(input, command, commandPrefix, requestedModel, childEnv, spawn, dependencies.piRpcClientOptions);
1069
+ try {
1070
+ // Isolated probes must share the same prerequisite contract as the
1071
+ // session handshake, so a cold-start timeout can use bounded recovery.
1072
+ probedModel = await discoverEffectivePiContextWindow(input, command, commandPrefix, requestedModel, childEnv, spawn, dependencies.piRpcClientOptions);
1073
+ }
1074
+ catch (error) {
1075
+ if (error instanceof RuntimePrerequisiteError)
1076
+ throw error;
1077
+ throw new RuntimePrerequisiteError(classifyRuntimePrerequisite("pi", error, command));
1078
+ }
1053
1079
  writeOwnedPiSettings(input.workspaceDir, calculatePiCompactionSettings(probedModel.contextWindow));
1054
1080
  }
1055
1081
  const child = spawn(command, commandArgs, {
@@ -1119,6 +1145,7 @@ export function createNativeRuntimeAdapter(id, dependencies = {}) {
1119
1145
  ? dependencies.codexCommand ?? dependencies.env?.LARKIN_CODEX_COMMAND ?? "codex"
1120
1146
  : dependencies.claudeCommand ?? dependencies.env?.LARKIN_CLAUDE_COMMAND ?? "claude";
1121
1147
  const codexCommand = dependencies.codexCommand ?? dependencies.env?.LARKIN_CODEX_COMMAND ?? "codex";
1148
+ const runPiInitialization = createInitializationLimiter(PI_INITIALIZATION_CONCURRENCY);
1122
1149
  let codexUpdateAttempt = null;
1123
1150
  let codexUpdateAttempted = false;
1124
1151
  const runCodexUpdate = () => new Promise((resolve) => {
@@ -1217,9 +1244,9 @@ export function createNativeRuntimeAdapter(id, dependencies = {}) {
1217
1244
  throw new RuntimePrerequisiteError(readiness);
1218
1245
  }
1219
1246
  if (id === "pi") {
1220
- return new PiSession(await (dependencies.createPiSession
1247
+ return runPiInitialization(async () => new PiSession(await (dependencies.createPiSession
1221
1248
  ? dependencies.createPiSession(input)
1222
- : createPiRpcBackend(input, { ...dependencies, piCommand: resolvedExecutable }, spawn, productionSpawn)));
1249
+ : createPiRpcBackend(input, { ...dependencies, piCommand: resolvedExecutable }, spawn, productionSpawn))));
1223
1250
  }
1224
1251
  if (id === "codex") {
1225
1252
  const codexInput = dependencies.codexModelOverride?.trim()
@@ -1640,38 +1640,45 @@ function createRuntimeHost(options) {
1640
1640
  const ensureSession = async (agent) => {
1641
1641
  if (agent.disabledReason)
1642
1642
  throw new Error(agent.disabledReason);
1643
+ if (agent.stopped)
1644
+ throw new Error("runtime Agent is stopped");
1643
1645
  if (agent.session)
1644
1646
  return agent.session;
1645
1647
  if (agent.starting)
1646
1648
  return agent.starting;
1647
- const probeEnv = runtimeEnv(agent.config);
1648
- await assertOfficialCliReady(agent.config, probeEnv);
1649
- const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
1650
- workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1651
- env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
1652
- LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
1653
- : { runtime: agent.adapter.id, state: "ready" };
1654
- if (!agent.authFailureActive)
1655
- agent.readiness = readiness;
1656
- if (readiness.state !== "ready")
1657
- throw new runtime_readiness_js_1.RuntimePrerequisiteError(readiness);
1658
- const standingPrompt = options.promptBuilder.build({
1659
- agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1660
- description: agent.config.description || "", runtime: agent.adapter.id,
1661
- cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"), previousSession: agent.previousSession,
1662
- });
1663
- const generation = ++agent.generation;
1664
1649
  let completedSession = null;
1665
- const sessionEnv = runtimeEnv(agent.config, `${agent.launchId}:${generation}`);
1666
- agent.starting = agent.adapter.createSession({
1667
- agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
1668
- workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1669
- resumeSessionId: agent.config.sessionId || null, standingPrompt,
1670
- env: sessionEnv,
1671
- }).then((session) => {
1650
+ agent.starting = (async () => {
1651
+ const probeEnv = runtimeEnv(agent.config);
1652
+ await assertOfficialCliReady(agent.config, probeEnv);
1653
+ if (agent.stopped)
1654
+ throw new Error("runtime Agent is stopped");
1655
+ const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
1656
+ workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1657
+ env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
1658
+ LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
1659
+ : { runtime: agent.adapter.id, state: "ready" };
1660
+ if (!agent.authFailureActive)
1661
+ agent.readiness = readiness;
1662
+ if (readiness.state !== "ready")
1663
+ throw new runtime_readiness_js_1.RuntimePrerequisiteError(readiness);
1664
+ if (agent.stopped)
1665
+ throw new Error("runtime Agent is stopped");
1666
+ const standingPrompt = options.promptBuilder.build({
1667
+ agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1668
+ description: agent.config.description || "", runtime: agent.adapter.id,
1669
+ cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"), previousSession: agent.previousSession,
1670
+ });
1671
+ const generation = ++agent.generation;
1672
+ const sessionEnv = runtimeEnv(agent.config, `${agent.launchId}:${generation}`);
1673
+ const session = await agent.adapter.createSession({
1674
+ agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
1675
+ workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1676
+ resumeSessionId: agent.config.sessionId || null, standingPrompt,
1677
+ env: sessionEnv,
1678
+ });
1672
1679
  completedSession = session;
1673
1680
  if (agent.stopped || generation !== agent.generation) {
1674
- void session.close("stale creation");
1681
+ await session.close("stale creation");
1675
1682
  throw new Error("stale runtime session creation");
1676
1683
  }
1677
1684
  agent.session = session;
@@ -1691,7 +1698,7 @@ function createRuntimeHost(options) {
1691
1698
  agent.stabilityTimer = setTimeout(() => markSessionStable(agent, session), retryPolicy.stableWindowMs);
1692
1699
  agent.stabilityTimer.unref?.();
1693
1700
  return session;
1694
- }).finally(() => {
1701
+ })().finally(() => {
1695
1702
  agent.starting = null;
1696
1703
  if (completedSession && !agent.session && !agent.stopped) {
1697
1704
  scheduleRecreate(agent, agent.recreateReason || "runtime closed during session initialization");
@@ -2329,6 +2336,8 @@ function createRuntimeHost(options) {
2329
2336
  emitConsumed(agent, [...startupConsumed, ...persist(agent)]);
2330
2337
  try {
2331
2338
  await ensureSession(agent);
2339
+ if (agent.stopped)
2340
+ return;
2332
2341
  reconcileSubagentLedger(agent, { forceMissing: true, missingReason: "runtime restarted" });
2333
2342
  enqueueUndeliveredTerminalWakes(agent);
2334
2343
  await recoverStalePiCompaction(agent);
@@ -2336,10 +2345,14 @@ function createRuntimeHost(options) {
2336
2345
  const proactive = startupSession ? proactivelyCompactPiAtIdle(agent, startupSession) : null;
2337
2346
  if (proactive)
2338
2347
  await proactive;
2348
+ if (agent.stopped)
2349
+ return;
2339
2350
  activeCount += 1;
2340
2351
  await retryPending(agent);
2341
2352
  }
2342
2353
  catch (error) {
2354
+ if (agent.stopped)
2355
+ return;
2343
2356
  const reason = error instanceof Error ? error.message : String(error);
2344
2357
  const transient = error instanceof runtime_readiness_js_1.RuntimePrerequisiteError && error.readiness.state === "unavailable";
2345
2358
  agent.disabledReason = transient ? null : reason;
@@ -2353,6 +2366,8 @@ function createRuntimeHost(options) {
2353
2366
  emit({ type: "agent-status", agentId: config.agentId, status: "error", error: reason,
2354
2367
  ...(error instanceof runtime_readiness_js_1.RuntimePrerequisiteError ? { readiness: error.readiness } : {}) });
2355
2368
  }
2369
+ if (agent.stopped)
2370
+ return;
2356
2371
  const visibleQuarantines = startupQuarantined.filter(({ record }) => agent.records.get(record.deliveryId)?.status === "error");
2357
2372
  for (const { record, code } of visibleQuarantines)
2358
2373
  emit({ type: "delivery", agentId: config.agentId,
@@ -2447,6 +2462,7 @@ function createRuntimeHost(options) {
2447
2462
  if (!agent)
2448
2463
  return;
2449
2464
  agent.stopped = true;
2465
+ const starting = agent.starting;
2450
2466
  if (agent.poller)
2451
2467
  clearInterval(agent.poller);
2452
2468
  if (agent.retryTimer)
@@ -2460,6 +2476,10 @@ function createRuntimeHost(options) {
2460
2476
  if (agent.busy)
2461
2477
  await agent.session?.cancel(reason);
2462
2478
  await agent.session?.close(reason);
2479
+ // A queued Pi initializer cannot be cancelled through the public adapter
2480
+ // contract. Drain it before removing the Agent; ensureSession sees stopped
2481
+ // and closes any session created after this point.
2482
+ await starting?.catch(() => { });
2463
2483
  managed.delete(agentId);
2464
2484
  emit({ type: "agent-status", agentId, status: "inactive" });
2465
2485
  },
@@ -1631,38 +1631,45 @@ export function createRuntimeHost(options) {
1631
1631
  const ensureSession = async (agent) => {
1632
1632
  if (agent.disabledReason)
1633
1633
  throw new Error(agent.disabledReason);
1634
+ if (agent.stopped)
1635
+ throw new Error("runtime Agent is stopped");
1634
1636
  if (agent.session)
1635
1637
  return agent.session;
1636
1638
  if (agent.starting)
1637
1639
  return agent.starting;
1638
- const probeEnv = runtimeEnv(agent.config);
1639
- await assertOfficialCliReady(agent.config, probeEnv);
1640
- const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
1641
- workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1642
- env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
1643
- LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
1644
- : { runtime: agent.adapter.id, state: "ready" };
1645
- if (!agent.authFailureActive)
1646
- agent.readiness = readiness;
1647
- if (readiness.state !== "ready")
1648
- throw new RuntimePrerequisiteError(readiness);
1649
- const standingPrompt = options.promptBuilder.build({
1650
- agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1651
- description: agent.config.description || "", runtime: agent.adapter.id,
1652
- cli: agentCliPromptCapabilities("larkin"), previousSession: agent.previousSession,
1653
- });
1654
- const generation = ++agent.generation;
1655
1640
  let completedSession = null;
1656
- const sessionEnv = runtimeEnv(agent.config, `${agent.launchId}:${generation}`);
1657
- agent.starting = agent.adapter.createSession({
1658
- agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
1659
- workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1660
- resumeSessionId: agent.config.sessionId || null, standingPrompt,
1661
- env: sessionEnv,
1662
- }).then((session) => {
1641
+ agent.starting = (async () => {
1642
+ const probeEnv = runtimeEnv(agent.config);
1643
+ await assertOfficialCliReady(agent.config, probeEnv);
1644
+ if (agent.stopped)
1645
+ throw new Error("runtime Agent is stopped");
1646
+ const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
1647
+ workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1648
+ env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
1649
+ LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
1650
+ : { runtime: agent.adapter.id, state: "ready" };
1651
+ if (!agent.authFailureActive)
1652
+ agent.readiness = readiness;
1653
+ if (readiness.state !== "ready")
1654
+ throw new RuntimePrerequisiteError(readiness);
1655
+ if (agent.stopped)
1656
+ throw new Error("runtime Agent is stopped");
1657
+ const standingPrompt = options.promptBuilder.build({
1658
+ agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1659
+ description: agent.config.description || "", runtime: agent.adapter.id,
1660
+ cli: agentCliPromptCapabilities("larkin"), previousSession: agent.previousSession,
1661
+ });
1662
+ const generation = ++agent.generation;
1663
+ const sessionEnv = runtimeEnv(agent.config, `${agent.launchId}:${generation}`);
1664
+ const session = await agent.adapter.createSession({
1665
+ agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
1666
+ workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1667
+ resumeSessionId: agent.config.sessionId || null, standingPrompt,
1668
+ env: sessionEnv,
1669
+ });
1663
1670
  completedSession = session;
1664
1671
  if (agent.stopped || generation !== agent.generation) {
1665
- void session.close("stale creation");
1672
+ await session.close("stale creation");
1666
1673
  throw new Error("stale runtime session creation");
1667
1674
  }
1668
1675
  agent.session = session;
@@ -1682,7 +1689,7 @@ export function createRuntimeHost(options) {
1682
1689
  agent.stabilityTimer = setTimeout(() => markSessionStable(agent, session), retryPolicy.stableWindowMs);
1683
1690
  agent.stabilityTimer.unref?.();
1684
1691
  return session;
1685
- }).finally(() => {
1692
+ })().finally(() => {
1686
1693
  agent.starting = null;
1687
1694
  if (completedSession && !agent.session && !agent.stopped) {
1688
1695
  scheduleRecreate(agent, agent.recreateReason || "runtime closed during session initialization");
@@ -2320,6 +2327,8 @@ export function createRuntimeHost(options) {
2320
2327
  emitConsumed(agent, [...startupConsumed, ...persist(agent)]);
2321
2328
  try {
2322
2329
  await ensureSession(agent);
2330
+ if (agent.stopped)
2331
+ return;
2323
2332
  reconcileSubagentLedger(agent, { forceMissing: true, missingReason: "runtime restarted" });
2324
2333
  enqueueUndeliveredTerminalWakes(agent);
2325
2334
  await recoverStalePiCompaction(agent);
@@ -2327,10 +2336,14 @@ export function createRuntimeHost(options) {
2327
2336
  const proactive = startupSession ? proactivelyCompactPiAtIdle(agent, startupSession) : null;
2328
2337
  if (proactive)
2329
2338
  await proactive;
2339
+ if (agent.stopped)
2340
+ return;
2330
2341
  activeCount += 1;
2331
2342
  await retryPending(agent);
2332
2343
  }
2333
2344
  catch (error) {
2345
+ if (agent.stopped)
2346
+ return;
2334
2347
  const reason = error instanceof Error ? error.message : String(error);
2335
2348
  const transient = error instanceof RuntimePrerequisiteError && error.readiness.state === "unavailable";
2336
2349
  agent.disabledReason = transient ? null : reason;
@@ -2344,6 +2357,8 @@ export function createRuntimeHost(options) {
2344
2357
  emit({ type: "agent-status", agentId: config.agentId, status: "error", error: reason,
2345
2358
  ...(error instanceof RuntimePrerequisiteError ? { readiness: error.readiness } : {}) });
2346
2359
  }
2360
+ if (agent.stopped)
2361
+ return;
2347
2362
  const visibleQuarantines = startupQuarantined.filter(({ record }) => agent.records.get(record.deliveryId)?.status === "error");
2348
2363
  for (const { record, code } of visibleQuarantines)
2349
2364
  emit({ type: "delivery", agentId: config.agentId,
@@ -2438,6 +2453,7 @@ export function createRuntimeHost(options) {
2438
2453
  if (!agent)
2439
2454
  return;
2440
2455
  agent.stopped = true;
2456
+ const starting = agent.starting;
2441
2457
  if (agent.poller)
2442
2458
  clearInterval(agent.poller);
2443
2459
  if (agent.retryTimer)
@@ -2451,6 +2467,10 @@ export function createRuntimeHost(options) {
2451
2467
  if (agent.busy)
2452
2468
  await agent.session?.cancel(reason);
2453
2469
  await agent.session?.close(reason);
2470
+ // A queued Pi initializer cannot be cancelled through the public adapter
2471
+ // contract. Drain it before removing the Agent; ensureSession sees stopped
2472
+ // and closes any session created after this point.
2473
+ await starting?.catch(() => { });
2454
2474
  managed.delete(agentId);
2455
2475
  emit({ type: "agent-status", agentId, status: "inactive" });
2456
2476
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [