larkin 0.5.2 → 0.5.4

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.
@@ -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.4",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [