larkin 0.4.28 → 0.5.0

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 (55) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +5 -9
  3. package/artifacts/release/THIRD_PARTY_NOTICES.txt +751 -4425
  4. package/dist/app/agent-cli.mjs +14 -2
  5. package/dist/app/agent-config.mjs +26 -84
  6. package/dist/app/binary-entry.mjs +0 -20
  7. package/dist/app/cli.mjs +8 -14
  8. package/dist/app/internal-command.cjs +0 -2
  9. package/dist/app/internal-command.mjs +0 -2
  10. package/dist/app/local-control.mjs +16 -3
  11. package/dist/app/runtime-model-directory.mjs +2 -16
  12. package/dist/app/runtime-process.mjs +42 -6
  13. package/dist/app/setup.mjs +52 -57
  14. package/dist/dashboard/dashboard-config-controller.mjs +83 -24
  15. package/dist/dashboard/dashboard-view-model.mjs +4 -7
  16. package/dist/dashboard/web/assets/dashboard.js +1 -1
  17. package/dist/feishu/host-shell.cjs +68 -17
  18. package/dist/feishu/host-shell.mjs +69 -18
  19. package/dist/platform/config.cjs +201 -138
  20. package/dist/platform/config.mjs +199 -138
  21. package/dist/runtime/pi-artifact-provenance.mjs +3 -3
  22. package/dist/runtime/pi-bash-timeout-extension.mjs +4 -5
  23. package/dist/runtime/pi-compaction-recovery.cjs +9 -11
  24. package/dist/runtime/pi-compaction-recovery.mjs +8 -10
  25. package/dist/runtime/pi-extension-api.mjs +1 -0
  26. package/dist/runtime/pi-model-catalog.cjs +7 -8
  27. package/dist/runtime/pi-model-catalog.mjs +7 -8
  28. package/dist/runtime/pi-subagent-injection.mjs +17 -4
  29. package/dist/runtime/runtime-adapters.mjs +41 -38
  30. package/dist/runtime/runtime-host.cjs +197 -20
  31. package/dist/runtime/runtime-host.mjs +198 -21
  32. package/dist/runtime/runtime-model-catalog.cjs +1 -1
  33. package/dist/runtime/runtime-model-catalog.mjs +1 -1
  34. package/dist/runtime/runtime-readiness.cjs +112 -49
  35. package/dist/runtime/runtime-readiness.mjs +103 -49
  36. package/dist/runtime/user-runtime.cjs +7 -24
  37. package/dist/runtime/user-runtime.mjs +7 -23
  38. package/dist/setup/bot-register.mjs +15 -123
  39. package/dist/setup/setup-agent-choice.mjs +48 -74
  40. package/dist/setup/setup-bind.mjs +51 -151
  41. package/dist/setup/setup-binding.mjs +6 -9
  42. package/package.json +3 -2
  43. package/dist/app/pi-auth-cli.mjs +0 -67
  44. package/dist/platform/process-boundary-trace.cjs +0 -108
  45. package/dist/runtime/builtin-pi-assets.cjs +0 -117
  46. package/dist/runtime/builtin-pi-assets.mjs +0 -105
  47. package/dist/runtime/pi-artifact-provenance.cjs +0 -147
  48. package/dist/runtime/pi-distribution-label.cjs +0 -6
  49. package/dist/runtime/pi-distribution-label.mjs +0 -3
  50. package/dist/runtime/pi-inline-extensions.mjs +0 -78
  51. package/dist/runtime/pi-official-auth.mjs +0 -491
  52. package/dist/runtime/pi-profile-migration.cjs +0 -764
  53. package/dist/runtime/pi-profile-migration.mjs +0 -719
  54. package/dist/runtime/pi-provider-config.cjs +0 -331
  55. package/dist/runtime/pi-provider-config.mjs +0 -314
@@ -41,7 +41,6 @@ exports.findExactPiModel = findExactPiModel;
41
41
  exports.discoverPiModelCatalog = discoverPiModelCatalog;
42
42
  const node_child_process_1 = require("node:child_process");
43
43
  const path = __importStar(require("node:path"));
44
- const builtin_pi_assets_js_1 = require("./builtin-pi-assets.cjs");
45
44
  const pi_rpc_client_js_1 = require("./pi-rpc-client.cjs");
46
45
  const runtime_readiness_js_1 = require("./runtime-readiness.cjs");
47
46
  exports.PI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
@@ -73,16 +72,16 @@ function findExactPiModel(reference, models) {
73
72
  const discoveryCache = new Map();
74
73
  /** Discover only through Pi's structured RPC protocol; no table parsing or static fallback. */
75
74
  function piCatalogChildEnv(options) {
76
- const mergedEnv = {
75
+ const env = {
77
76
  ...process.env,
78
77
  ...options.env,
79
- ...(options.agentDir ? { PI_CODING_AGENT_DIR: path.resolve(options.agentDir) } : {}),
80
78
  NO_COLOR: "1",
81
79
  };
82
- return (0, builtin_pi_assets_js_1.applyPiPackageDirForChild)(mergedEnv, {
83
- distribution: (0, builtin_pi_assets_js_1.catalogPiChildDistribution)(options.commandArgs),
84
- explicitPackageDir: options.packageDir,
85
- });
80
+ if (options.agentDir)
81
+ env.PI_CODING_AGENT_DIR = path.resolve(options.agentDir);
82
+ else
83
+ delete env.PI_CODING_AGENT_DIR;
84
+ return env;
86
85
  }
87
86
  async function discoverPiModelCatalog(options) {
88
87
  if (!options.spawn) {
@@ -118,7 +117,7 @@ async function discoverPiModelCatalogUncached(options) {
118
117
  ]);
119
118
  const available = [...(availableResponse?.models ?? [])].sort((left, right) => piModelId(left).localeCompare(piModelId(right)));
120
119
  if (!available.length)
121
- throw new Error("Pi has no authenticated available models. Run the official `pi` login flow or configure provider credentials; Larkin will not create a fallback session.");
120
+ throw new Error("Pi has no authenticated available models. Run the official `pi` login flow; Larkin will not create a fallback session.");
122
121
  const effectiveModel = state.model ? piModelId(state.model) : null;
123
122
  if (!effectiveModel || !available.some((model) => piModelId(model) === effectiveModel)) {
124
123
  throw new Error(`Pi official default resolution returned an unavailable model (${effectiveModel || "none"}); refusing implicit fallback`);
@@ -1,6 +1,5 @@
1
1
  import { spawn as nodeSpawn } from "node:child_process";
2
2
  import * as path from "node:path";
3
- import { applyPiPackageDirForChild, catalogPiChildDistribution } from "./builtin-pi-assets.mjs";
4
3
  import { PiRpcClient } from "./pi-rpc-client.mjs";
5
4
  import { classifyRuntimePrerequisite, RuntimePrerequisiteError } from "./runtime-readiness.mjs";
6
5
  export const PI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
@@ -32,16 +31,16 @@ export function findExactPiModel(reference, models) {
32
31
  const discoveryCache = new Map();
33
32
  /** Discover only through Pi's structured RPC protocol; no table parsing or static fallback. */
34
33
  function piCatalogChildEnv(options) {
35
- const mergedEnv = {
34
+ const env = {
36
35
  ...process.env,
37
36
  ...options.env,
38
- ...(options.agentDir ? { PI_CODING_AGENT_DIR: path.resolve(options.agentDir) } : {}),
39
37
  NO_COLOR: "1",
40
38
  };
41
- return applyPiPackageDirForChild(mergedEnv, {
42
- distribution: catalogPiChildDistribution(options.commandArgs),
43
- explicitPackageDir: options.packageDir,
44
- });
39
+ if (options.agentDir)
40
+ env.PI_CODING_AGENT_DIR = path.resolve(options.agentDir);
41
+ else
42
+ delete env.PI_CODING_AGENT_DIR;
43
+ return env;
45
44
  }
46
45
  export async function discoverPiModelCatalog(options) {
47
46
  if (!options.spawn) {
@@ -77,7 +76,7 @@ async function discoverPiModelCatalogUncached(options) {
77
76
  ]);
78
77
  const available = [...(availableResponse?.models ?? [])].sort((left, right) => piModelId(left).localeCompare(piModelId(right)));
79
78
  if (!available.length)
80
- throw new Error("Pi has no authenticated available models. Run the official `pi` login flow or configure provider credentials; Larkin will not create a fallback session.");
79
+ throw new Error("Pi has no authenticated available models. Run the official `pi` login flow; Larkin will not create a fallback session.");
81
80
  const effectiveModel = state.model ? piModelId(state.model) : null;
82
81
  if (!effectiveModel || !available.some((model) => piModelId(model) === effectiveModel)) {
83
82
  throw new Error(`Pi official default resolution returned an unavailable model (${effectiveModel || "none"}); refusing implicit fallback`);
@@ -160,12 +160,25 @@ export function piVersionSupportsSubagents(version) {
160
160
  return false;
161
161
  return version.major > 0 || (version.major === 0 && version.minor >= 80);
162
162
  }
163
- /** 解析 external `pi --version`;无法确认版本时视为不支持,不注入。 */
163
+ const externalPiVersionProbeCache = new Map();
164
+ function externalPiVersionProbeKey(piCommand, env) {
165
+ return JSON.stringify([piCommand, env.PATH ?? ""]);
166
+ }
167
+ /** 测试用:清空进程内 `pi --version` 探测缓存。 */
168
+ export function resetExternalPiVersionProbeCache() {
169
+ externalPiVersionProbeCache.clear();
170
+ }
171
+ /** 解析 external `pi --version`;无法确认版本时视为不支持,不注入。按 piCommand+PATH 缓存。 */
164
172
  export function probeExternalPiVersion(piCommand, env) {
173
+ const key = externalPiVersionProbeKey(piCommand, env);
174
+ if (externalPiVersionProbeCache.has(key))
175
+ return externalPiVersionProbeCache.get(key) ?? null;
165
176
  const result = spawnSync(piCommand, ["--version"], { env, encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024 });
166
- if (result.error || result.status !== 0)
167
- return null;
168
- return parsePiVersion(String(result.stdout || result.stderr || ""));
177
+ const version = result.error || result.status !== 0
178
+ ? null
179
+ : parsePiVersion(String(result.stdout || result.stderr || ""));
180
+ externalPiVersionProbeCache.set(key, version);
181
+ return version;
169
182
  }
170
183
  /**
171
184
  * 用户 pi 是否已自行安装 pi-subagents(settings.json packages 或包目录)。
@@ -3,11 +3,8 @@ import crypto from "node:crypto";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { createInterface } from "node:readline";
6
- import { applyPiPackageDirForChild, piChildDistributionFromOverrides } from "./builtin-pi-assets.mjs";
7
6
  import { isPiThinkingLevel } from "./pi-model-catalog.mjs";
8
7
  import { PiRpcClient } from "./pi-rpc-client.mjs";
9
- import { internalCommandSpec } from "../app/internal-command.mjs";
10
- import { BUNDLED_PI_VERSION, piAgentDirectory } from "./pi-provider-config.mjs";
11
8
  import { recordPiRuntimeArtifactProvenance } from "./pi-artifact-provenance.mjs";
12
9
  import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
13
10
  import { resolvePiSubagentExtensionArg } from "./pi-subagent-injection.mjs";
@@ -15,9 +12,12 @@ import { extractCanonicalPiSubagentNotification, ledgerStatusFromPiNotificationS
15
12
  import { resolvePiSubagentRecordWatchdogExtensionArg } from "./pi-subagent-record-watchdog-injection.mjs";
16
13
  import { effectivePiStateDir, extractBackgroundPiSubagentDispatch } from "./pi-subagent-ledger.mjs";
17
14
  import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
18
- import { classifyRuntimePrerequisite, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
15
+ import { classifyPiMissingCredentialRejection, classifyRuntimePrerequisite, missingProviderCredentialReadiness, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
19
16
  import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, } from "./provider-error-classifier.mjs";
20
- import { assertEffectivePiCompactionSettings, assertNoProjectPiCompactionOverride, calculatePiCompactionSettings, parsePiExecutableVersion, prepareOwnedPiDirectory, readOwnedPiSettings, verifyPiCapabilities, writeOwnedPiSettings, } from "./pi-compaction-recovery.mjs";
17
+ import { assertEffectivePiCompactionSettings, assertNoProjectPiCompactionOverride, PINNED_PI_VERSION, calculatePiCompactionSettings, parsePiExecutableVersion, prepareOwnedPiDirectory, readOwnedPiSettings, verifyPiCapabilities, writeOwnedPiSettings, } from "./pi-compaction-recovery.mjs";
18
+ function piAgentDirectory(configDir, agentId) {
19
+ return path.join(path.resolve(configDir), "providers", "pi", agentId);
20
+ }
21
21
  class EventSession {
22
22
  listeners = new Set();
23
23
  subscribe(listener) {
@@ -462,7 +462,7 @@ class PiSession extends EventSession {
462
462
  firstOutputObserved = false;
463
463
  toolCallOpen = false;
464
464
  piPolicyManaged;
465
- constructor(sdk, distribution) {
465
+ constructor(sdk, distribution = "external") {
466
466
  super();
467
467
  this.sdk = sdk;
468
468
  this.distribution = distribution;
@@ -537,7 +537,19 @@ class PiSession extends EventSession {
537
537
  this.awaitingAcknowledgement.delete(input.inputId);
538
538
  this.ownedInputIds.delete(input.inputId);
539
539
  this.inputEpochs.delete(input.inputId);
540
- return { status: "rejected", inputId: input.inputId, retryable: true, reason: error.message };
540
+ const rawReason = error instanceof Error ? error.message : String(error);
541
+ const missing = classifyPiMissingCredentialRejection(rawReason);
542
+ if (missing) {
543
+ const classified = classifyPiProviderError({ provider: missing.provider, message: missing.diagnostic });
544
+ this.emit({
545
+ type: "input-error", inputId: input.inputId, retryable: false, willRetry: false,
546
+ message: classified.reason, errorCategory: classified.category, nextAction: classified.nextAction,
547
+ upstream: { provider: missing.provider, message: missing.diagnostic },
548
+ });
549
+ // 拒绝原因保留 Pi 原文,方便 RuntimeHost 独立分类,不依赖 input-error 事件顺序。
550
+ return { status: "rejected", inputId: input.inputId, retryable: false, reason: rawReason };
551
+ }
552
+ return { status: "rejected", inputId: input.inputId, retryable: true, reason: rawReason };
541
553
  }
542
554
  }
543
555
  onEvent(raw) {
@@ -664,7 +676,7 @@ class PiSession extends EventSession {
664
676
  this.emitObservation("settled");
665
677
  const owned = [...this.ownedInputIds].filter((inputId) => this.inputEpochs.get(inputId) === epoch);
666
678
  if (error) {
667
- const classified = classifyPiProviderError(error);
679
+ const classified = classifyPiProviderError(error, { distribution: this.distribution });
668
680
  const retryable = classified.category === "rate_limit" || isRetryablePiProviderError(error.upstream.message);
669
681
  for (const inputId of owned)
670
682
  this.emit({
@@ -794,7 +806,7 @@ function piAssistantProviderError(message) {
794
806
  ...(typeof bodyError.type === "string" ? { type: bodyError.type } : {}),
795
807
  };
796
808
  }
797
- export function classifyPiProviderError(error) {
809
+ export function classifyPiProviderError(error, scope) {
798
810
  const upstream = "upstream" in error ? error.upstream : error;
799
811
  const errorType = "type" in error ? error.type : undefined;
800
812
  const reason = safeProviderText(upstream.message, "Pi provider request failed");
@@ -818,6 +830,11 @@ export function classifyPiProviderError(error) {
818
830
  return {
819
831
  category: "rate_limit", reason, nextAction: "Wait for the provider rate-limit window, then retry.",
820
832
  };
833
+ const missing = classifyPiMissingCredentialRejection(reason);
834
+ if (missing) {
835
+ const readiness = missingProviderCredentialReadiness("pi", missing.provider);
836
+ return { category: "auth", reason: readiness.reason, nextAction: readiness.nextAction };
837
+ }
821
838
  if (upstream.status === 401
822
839
  || [...signal].some((value) => ["authentication_error", "invalid_api_key", "invalid_token", "token_expired", "unauthorized", "provider_auth_error"].includes(value))
823
840
  || /\bAPI key auth failed\b|\bFailed to resolve API key\b/i.test(reason)) {
@@ -983,10 +1000,6 @@ class PiRpcBackend {
983
1000
  }
984
1001
  }
985
1002
  export function resolvePiProcessExtensionArgs(input, resolvers = {}) {
986
- // Builtin factories are passed directly to Pi main on every platform. The platform
987
- // field makes that invariant explicit and testable without changing process.platform.
988
- if (input.distribution === "builtin")
989
- return [];
990
1003
  const resolverInput = { distribution: "external", piCommand: input.piCommand, env: input.env };
991
1004
  const args = [];
992
1005
  // Watchdog must load before the subagent extension so session_shutdown still
@@ -1024,31 +1037,23 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
1024
1037
  ...(session.sessionFile ? ["--session", session.sessionFile] : []),
1025
1038
  ...(requestedModel ? ["--model", requestedModel] : []),
1026
1039
  ...(requestedEffort ? ["--thinking", requestedEffort] : [])];
1027
- const builtin = piChildDistributionFromOverrides(dependencies.env, input.env) === "builtin";
1028
- if (!builtin && mergedEnv.LARKIN_PI_DISTRIBUTION === "builtin")
1040
+ if (mergedEnv.LARKIN_PI_DISTRIBUTION === "builtin")
1029
1041
  delete mergedEnv.LARKIN_PI_DISTRIBUTION;
1030
- const builtinSpec = builtin ? internalCommandSpec("pi-rpc", [], mergedEnv) : null;
1031
- const command = builtinSpec?.command ?? dependencies.piCommand ?? dependencies.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
1032
- const commandPrefix = builtinSpec?.args ?? dependencies.piCommandArgs ?? [];
1042
+ const command = dependencies.piCommand ?? dependencies.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
1043
+ const commandPrefix = dependencies.piCommandArgs ?? [];
1033
1044
  const commandArgs = [...commandPrefix, ...args];
1034
1045
  mergedEnv.PI_CODING_AGENT_DIR = ownedPiDirectory;
1035
- if (builtin)
1036
- mergedEnv.PI_TELEMETRY = "0";
1037
- const childEnv = applyPiPackageDirForChild(mergedEnv, {
1038
- distribution: builtin ? "builtin" : "external",
1039
- explicitPackageDir: input.env?.PI_PACKAGE_DIR ?? dependencies.env?.PI_PACKAGE_DIR,
1040
- });
1041
- const reportedVersion = productionSpawn && !builtin
1046
+ const childEnv = { ...mergedEnv };
1047
+ const reportedVersion = productionSpawn
1042
1048
  ? parsePiExecutableVersion(String(spawnSync(command, [...commandPrefix, "--version"], {
1043
1049
  cwd: input.workspaceDir, env: childEnv, encoding: "utf8", timeout: 5_000,
1044
1050
  }).stdout || ""))
1045
- : BUNDLED_PI_VERSION;
1046
- const extensionArgs = (dependencies.resolvePiProcessExtensionArgs ?? resolvePiProcessExtensionArgs)({
1047
- distribution: builtin ? "builtin" : "external",
1048
- piCommand: command,
1049
- env: childEnv,
1050
- platform: process.platform,
1051
- });
1051
+ : PINNED_PI_VERSION;
1052
+ const extensionInput = { piCommand: command, env: childEnv, platform: process.platform };
1053
+ // 假 spawn 不需要真实 extension 路径;默认 resolver 会 spawnSync(`pi --version`)。
1054
+ const extensionArgs = dependencies.resolvePiProcessExtensionArgs
1055
+ ? dependencies.resolvePiProcessExtensionArgs(extensionInput)
1056
+ : productionSpawn ? resolvePiProcessExtensionArgs(extensionInput) : [];
1052
1057
  commandArgs.push(...extensionArgs);
1053
1058
  let probedModel = null;
1054
1059
  if (productionSpawn) {
@@ -1074,7 +1079,7 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
1074
1079
  client.request("get_available_models"),
1075
1080
  ]);
1076
1081
  if (!available?.models?.length)
1077
- throw new Error("Pi has no authenticated available models. Run the official `pi` login flow or configure provider credentials; Larkin will not create a fallback session.");
1082
+ throw new Error("Pi has no authenticated available models. Run the official `pi` login flow; Larkin will not create a fallback session.");
1078
1083
  const effectiveModel = state.model?.provider && state.model.id ? `${state.model.provider}/${state.model.id}` : null;
1079
1084
  if (productionSpawn) {
1080
1085
  if (!effectiveModel || !probedModel || effectiveModel !== probedModel.model
@@ -1088,11 +1093,10 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
1088
1093
  keepRecentTokens: ownedSettings.compaction?.keepRecentTokens,
1089
1094
  } });
1090
1095
  const handshake = state.compactionCapabilities;
1091
- verifyPiCapabilities({ distribution: builtin ? "builtin" : "external", version: reportedVersion,
1096
+ verifyPiCapabilities({ distribution: "external", version: reportedVersion,
1092
1097
  contextWindow: state.model?.contextWindow, autoCompactionEnabled: state.autoCompactionEnabled,
1093
1098
  reserveTokens: handshake?.reserveTokens, keepRecentTokens: handshake?.keepRecentTokens,
1094
- compactRpc: true, events: handshake?.events,
1095
- trustedProtocol: builtin });
1099
+ compactRpc: true, events: handshake?.events });
1096
1100
  }
1097
1101
  // 严格对账(Owner 决策):模型必须来自 pi 的权威可用列表,不做后缀猜测——
1098
1102
  // 配置的模型与 pi 可用列表不符即报错,并在错误里列出可用模型。
@@ -1227,10 +1231,9 @@ export function createNativeRuntimeAdapter(id, dependencies = {}) {
1227
1231
  throw new RuntimePrerequisiteError(readiness);
1228
1232
  }
1229
1233
  if (id === "pi") {
1230
- const distribution = piChildDistributionFromOverrides(dependencies.env, input.env);
1231
1234
  return new PiSession(await (dependencies.createPiSession
1232
1235
  ? dependencies.createPiSession(input)
1233
- : createPiRpcBackend(input, { ...dependencies, piCommand: resolvedExecutable }, spawn, productionSpawn)), distribution);
1236
+ : createPiRpcBackend(input, { ...dependencies, piCommand: resolvedExecutable }, spawn, productionSpawn)));
1234
1237
  }
1235
1238
  if (id === "codex") {
1236
1239
  const codexInput = dependencies.codexModelOverride?.trim()
@@ -102,6 +102,107 @@ function persistPreviousSession(agent, previous) {
102
102
  // In-memory ref still holds for this process; restart will miss it only if this write failed.
103
103
  }
104
104
  }
105
+ function authFailureScopeOf(agent) {
106
+ return {
107
+ runtime: agent.config.runtime,
108
+ model: agent.config.model,
109
+ adapterId: agent.adapter.id,
110
+ };
111
+ }
112
+ function currentAuthFailure(agent) {
113
+ if (!agent.authFailureActive)
114
+ return null;
115
+ const runtime = agent.adapter.id === "codex" || agent.adapter.id === "claude" || agent.adapter.id === "pi"
116
+ ? agent.adapter.id : null;
117
+ if (!runtime)
118
+ return null;
119
+ return {
120
+ kind: agent.authFailureKind === "missing-provider" ? "missing-provider" : "generic",
121
+ runtime,
122
+ provider: agent.authFailureProvider,
123
+ };
124
+ }
125
+ function genericAuthProvider(agent, upstream) {
126
+ return (0, runtime_readiness_js_1.safeProviderId)(upstream?.provider) ?? (0, runtime_readiness_js_1.configuredProviderId)(agent.config.model);
127
+ }
128
+ function persistAuthFailure(agent) {
129
+ const store = agent.stateStore;
130
+ if (!store)
131
+ return;
132
+ try {
133
+ const write = () => {
134
+ const current = store.readJson("agentState", { sessions: {} });
135
+ if (Array.isArray(current.records) && !("sessions" in current))
136
+ return;
137
+ const { authFailureProvider: _legacy, authFailure: _scoped, ...rest } = current;
138
+ const failure = currentAuthFailure(agent);
139
+ if (!failure) {
140
+ if (!("authFailureProvider" in current) && !("authFailure" in current))
141
+ return;
142
+ store.writeJson("agentState", rest);
143
+ return;
144
+ }
145
+ store.writeJson("agentState", {
146
+ ...rest,
147
+ authFailure: failure,
148
+ ...(failure.kind === "missing-provider" && failure.provider
149
+ ? { authFailureProvider: failure.provider } : {}),
150
+ });
151
+ };
152
+ if (store.withInboxTransaction)
153
+ store.withInboxTransaction(write);
154
+ else
155
+ write();
156
+ }
157
+ catch {
158
+ // In-memory flags and the delivery ledger still hold for this process.
159
+ }
160
+ }
161
+ function loadPersistedAuthFailure(store) {
162
+ if (!store)
163
+ return null;
164
+ try {
165
+ return (0, runtime_readiness_js_1.parsePersistedAuthFailure)(store.readJson("agentState", {}));
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ }
171
+ function applyAuthFailure(agent, failure) {
172
+ agent.authFailureActive = true;
173
+ agent.authFailureKind = failure.kind;
174
+ agent.authFailureProvider = failure.provider ?? null;
175
+ agent.readiness = (0, runtime_readiness_js_1.readinessForPersistedAuthFailure)(failure);
176
+ }
177
+ function clearAuthFailure(agent, persist = true) {
178
+ agent.authFailureActive = false;
179
+ agent.authFailureKind = null;
180
+ agent.authFailureProvider = null;
181
+ if (persist)
182
+ persistAuthFailure(agent);
183
+ }
184
+ function restoreAuthFailure(agent) {
185
+ const persisted = loadPersistedAuthFailure(agent.stateStore);
186
+ if (!persisted)
187
+ return;
188
+ // 只恢复仍绑定当前 candidate 的当前作用域 auth;不从历史 delivery 回灌。
189
+ if (!(0, runtime_readiness_js_1.authFailureAppliesTo)(authFailureScopeOf(agent), persisted)) {
190
+ clearAuthFailure(agent);
191
+ return;
192
+ }
193
+ applyAuthFailure(agent, persisted);
194
+ }
195
+ function clearLedgerAuthProviders(agent) {
196
+ let changed = false;
197
+ for (const record of agent.records.values()) {
198
+ if (!record.authProvider)
199
+ continue;
200
+ delete record.authProvider;
201
+ changed = true;
202
+ }
203
+ if (changed)
204
+ persist(agent);
205
+ }
105
206
  const REPLAY_FAILURE_CODES = new Set([
106
207
  "canonical_inbox_row_missing", "canonical_inbox_malformed", "duplicate_message_id",
107
208
  "inbox_state_conflict", "delivery_target_conflict", "structured_target_invalid",
@@ -228,7 +329,6 @@ function createRuntimeHost(options) {
228
329
  } : {}),
229
330
  LARKIN_CONFIG_DIR: process.env.LARKIN_CONFIG_DIR,
230
331
  LARKIN_HOME: process.env.LARKIN_HOME,
231
- ...(config.piDistribution ? { LARKIN_PI_DISTRIBUTION: config.piDistribution } : {}),
232
332
  LARKIN_STATE_DIR: (0, pi_subagent_ledger_js_1.effectivePiStateDir)(config),
233
333
  ...(process.env.HOME ? { HOME: process.env.HOME } : {}),
234
334
  ...(process.env.SHELL ? { SHELL: process.env.SHELL } : {}),
@@ -490,6 +590,25 @@ function createRuntimeHost(options) {
490
590
  messageId: record.messageId, status: "consumed" });
491
591
  }
492
592
  };
593
+ const projectAuthFailure = (agent, readiness, failure) => {
594
+ agent.authFailureActive = true;
595
+ agent.authFailureKind = failure.kind;
596
+ agent.authFailureProvider = failure.provider;
597
+ agent.readiness = {
598
+ ...readiness,
599
+ ...(agent.readiness?.executable ? { executable: agent.readiness.executable } : {}),
600
+ ...(agent.readiness?.version ? { version: agent.readiness.version } : {}),
601
+ };
602
+ persistAuthFailure(agent);
603
+ emit({ type: "agent-status", agentId: agent.config.agentId, status: "error",
604
+ error: `${agent.readiness.reason} ${agent.readiness.nextAction}`, readiness: agent.readiness });
605
+ };
606
+ const namedAuthProviderPresent = (agent) => {
607
+ if (agent.authFailureKind !== "missing-provider" || !agent.authFailureProvider)
608
+ return true;
609
+ const failure = currentAuthFailure(agent);
610
+ return Boolean(failure && (0, runtime_readiness_js_1.authFailureAppliesTo)(authFailureScopeOf(agent), failure));
611
+ };
493
612
  const resultFor = (agent, record, result) => {
494
613
  if (result.status === "accepted") {
495
614
  if (record.errorCategory !== "context_window") {
@@ -503,6 +622,28 @@ function createRuntimeHost(options) {
503
622
  deliveryId: record.deliveryId, messageId: record.messageId, status: "accepted" });
504
623
  return { status: "accepted", deliveryId: record.deliveryId };
505
624
  }
625
+ const missing = result.status === "rejected" && agent.adapter.id === "pi"
626
+ ? (0, runtime_readiness_js_1.classifyPiMissingCredentialRejection)(result.reason)
627
+ : null;
628
+ if (missing) {
629
+ const readiness = (0, runtime_readiness_js_1.missingProviderCredentialReadiness)(agent.adapter.id, missing.provider);
630
+ const reason = readiness.reason ?? result.reason;
631
+ if (record.status !== "error") {
632
+ record.reason = reason;
633
+ record.retryable = false;
634
+ record.errorCategory = "auth";
635
+ record.authProvider = missing.provider;
636
+ const finalRecord = setRecord(agent, record, "error");
637
+ if (finalRecord.status !== "consumed")
638
+ emit({ type: "delivery", agentId: agent.config.agentId,
639
+ deliveryId: record.deliveryId, messageId: record.messageId, status: "error", reason });
640
+ projectAuthFailure(agent, readiness, { kind: "missing-provider", provider: missing.provider });
641
+ }
642
+ return { status: "error", deliveryId: record.deliveryId, reason: record.reason ?? reason, retryable: false };
643
+ }
644
+ if (record.status === "error") {
645
+ return { status: "error", deliveryId: record.deliveryId, reason: record.reason ?? result.reason, retryable: false };
646
+ }
506
647
  if (record.errorCategory !== "context_window")
507
648
  delete record.errorCategory;
508
649
  const retryable = result.status === "deferred" || result.retryable;
@@ -1347,7 +1488,7 @@ function createRuntimeHost(options) {
1347
1488
  agent.compactionMachines.delete(inputId);
1348
1489
  }
1349
1490
  const recoveredAuthentication = agent.authFailureActive && agent.turnInProgress
1350
- && agent.turnHadAuthenticatedOutput && !agent.turnHadFailure;
1491
+ && agent.turnHadAuthenticatedOutput && !agent.turnHadFailure && namedAuthProviderPresent(agent);
1351
1492
  agent.turnInProgress = false;
1352
1493
  agent.busy = false;
1353
1494
  emit({ type: "activity", agentId: agent.config.agentId, activity: "idle", activityKind: "idle", detailKind: "turn_ended" });
@@ -1369,7 +1510,8 @@ function createRuntimeHost(options) {
1369
1510
  reconcileSubagentLedger(agent);
1370
1511
  telemetry?.runtimeEvent(agent.config.agentId, event);
1371
1512
  if (recoveredAuthentication) {
1372
- agent.authFailureActive = false;
1513
+ clearAuthFailure(agent);
1514
+ clearLedgerAuthProviders(agent);
1373
1515
  const prior = agent.readiness;
1374
1516
  agent.readiness = {
1375
1517
  runtime: agent.adapter.id,
@@ -1462,23 +1604,30 @@ function createRuntimeHost(options) {
1462
1604
  record.errorCategory = classifiedCategory;
1463
1605
  else
1464
1606
  delete record.errorCategory;
1465
- const finalRecord = setRecord(agent, record, event.retryable ? "pending" : "error");
1607
+ const missing = agent.adapter.id === "pi"
1608
+ ? (0, runtime_readiness_js_1.classifyPiMissingCredentialRejection)(event.upstream?.message)
1609
+ ?? (0, runtime_readiness_js_1.classifyPiMissingCredentialRejection)(event.message)
1610
+ : null;
1611
+ if (missing) {
1612
+ record.retryable = false;
1613
+ record.errorCategory = "auth";
1614
+ record.authProvider = missing.provider;
1615
+ }
1616
+ const terminal = Boolean(missing) || !event.retryable;
1617
+ const finalRecord = setRecord(agent, record, terminal ? "error" : "pending");
1466
1618
  if (finalRecord.status !== "consumed")
1467
1619
  emit({ type: "delivery", agentId: agent.config.agentId,
1468
1620
  deliveryId: record.deliveryId, messageId: record.messageId,
1469
- status: event.retryable ? "deferred" : "error", reason: event.message });
1470
- if (event.errorCategory === "auth" && !event.retryable) {
1621
+ status: terminal ? "error" : "deferred", reason: event.message });
1622
+ if (missing) {
1623
+ const readiness = (0, runtime_readiness_js_1.missingProviderCredentialReadiness)(agent.adapter.id, missing.provider);
1624
+ projectAuthFailure(agent, readiness, { kind: "missing-provider", provider: missing.provider });
1625
+ }
1626
+ else if (event.errorCategory === "auth" && !event.retryable) {
1471
1627
  const readiness = (0, runtime_readiness_js_1.providerAuthenticationFailureReadiness)(agent.adapter.id, event.upstream?.provider);
1472
- agent.authFailureActive = true;
1473
- agent.readiness = {
1474
- ...readiness,
1475
- ...(agent.readiness?.executable ? { executable: agent.readiness.executable } : {}),
1476
- ...(agent.readiness?.version ? { version: agent.readiness.version } : {}),
1477
- };
1478
- emit({ type: "agent-status", agentId: agent.config.agentId, status: "error",
1479
- error: `${agent.readiness.reason} ${agent.readiness.nextAction}`, readiness: agent.readiness });
1628
+ projectAuthFailure(agent, readiness, { kind: "generic", provider: genericAuthProvider(agent, event.upstream) });
1480
1629
  }
1481
- if (event.retryable && !agent.busy && !agent.turnInProgress)
1630
+ if (!terminal && event.retryable && !agent.busy && !agent.turnInProgress)
1482
1631
  void retryPending(agent);
1483
1632
  }
1484
1633
  else if (event.type === "configuration-error") {
@@ -1748,14 +1897,26 @@ function createRuntimeHost(options) {
1748
1897
  clearTimeout(previous.backgroundCompletionRetryTimer);
1749
1898
  if (previous.subagentReconcileTimer)
1750
1899
  clearInterval(previous.subagentReconcileTimer);
1900
+ const previousFailure = currentAuthFailure(previous);
1901
+ const scoped = Boolean(previousFailure
1902
+ && (0, runtime_readiness_js_1.authFailureAppliesTo)({
1903
+ runtime: config.runtime,
1904
+ model: config.model,
1905
+ adapterId: adapter.id,
1906
+ }, previousFailure));
1751
1907
  const candidate = {
1752
1908
  ...previous, config, adapter, session, launchId: node_crypto_1.default.randomUUID(), busy: false, submitting: false,
1753
1909
  starting: null, retryAfterSubmit: false, generation: 0, poller: null, retryTimer: null,
1754
1910
  recreateAttempts: 0, stabilityTimer: null, recreateReason: null, stopped: false,
1755
1911
  backgroundCompletionRetryTimer: null, subagentReconcileTimer: null,
1756
1912
  disabledReason: null, configurationRecovery: null,
1757
- readiness: previous.authFailureActive ? previous.readiness : readiness,
1913
+ authFailureActive: scoped && previous.authFailureActive,
1914
+ authFailureKind: scoped ? previous.authFailureKind : null,
1915
+ authFailureProvider: scoped ? previous.authFailureProvider : null,
1916
+ readiness: scoped && previous.authFailureActive ? previous.readiness : readiness,
1758
1917
  };
1918
+ if (previous.authFailureActive && !candidate.authFailureActive)
1919
+ clearAuthFailure(candidate);
1759
1920
  managed.set(config.agentId, candidate);
1760
1921
  session.subscribe((event) => observe(candidate, session, event));
1761
1922
  if (session.sessionId) {
@@ -1850,7 +2011,8 @@ function createRuntimeHost(options) {
1850
2011
  agent.config.sessionId = null;
1851
2012
  agent.session = fresh;
1852
2013
  persistPreviousSession(agent, previousRef);
1853
- agent.readiness = readiness;
2014
+ if (!agent.authFailureActive)
2015
+ agent.readiness = readiness;
1854
2016
  agent.disabledReason = null;
1855
2017
  };
1856
2018
  try {
@@ -1871,7 +2033,13 @@ function createRuntimeHost(options) {
1871
2033
  ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}),
1872
2034
  ...(agent.previousSession ? { previousSession: agent.previousSession } : {}) });
1873
2035
  }
1874
- emit({ type: "agent-status", agentId, status: "active", readiness });
2036
+ emit({
2037
+ type: "agent-status",
2038
+ agentId,
2039
+ status: agent.authFailureActive ? "error" : "active",
2040
+ ...(agent.authFailureActive ? { error: `${agent.readiness?.reason} ${agent.readiness?.nextAction}` } : {}),
2041
+ readiness: agent.authFailureActive ? agent.readiness ?? readiness : readiness,
2042
+ });
1875
2043
  agent.piSessionOwner = probeEnv[pi_subagent_ledger_js_1.PI_SUBAGENT_SESSION_OWNER_ENV];
1876
2044
  await oldSession.close("fresh session reset committed")
1877
2045
  .catch((error) => log("previous runtime close after fresh reset failed", String(error)));
@@ -2009,7 +2177,8 @@ function createRuntimeHost(options) {
2009
2177
  agent.config.sessionId = null;
2010
2178
  agent.session = fresh;
2011
2179
  persistPreviousSession(agent, previousRef);
2012
- agent.readiness = readiness;
2180
+ if (!agent.authFailureActive)
2181
+ agent.readiness = readiness;
2013
2182
  agent.disabledReason = null;
2014
2183
  for (const record of agent.records.values()) {
2015
2184
  if (!messageIds.includes(record.messageId))
@@ -2027,7 +2196,13 @@ function createRuntimeHost(options) {
2027
2196
  ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}),
2028
2197
  ...(agent.previousSession ? { previousSession: agent.previousSession } : {}) });
2029
2198
  }
2030
- emit({ type: "agent-status", agentId, status: "active", readiness });
2199
+ emit({
2200
+ type: "agent-status",
2201
+ agentId,
2202
+ status: agent.authFailureActive ? "error" : "active",
2203
+ ...(agent.authFailureActive ? { error: `${agent.readiness?.reason} ${agent.readiness?.nextAction}` } : {}),
2204
+ readiness: agent.authFailureActive ? agent.readiness ?? readiness : readiness,
2205
+ });
2031
2206
  durableRollback = null;
2032
2207
  agent.piSessionOwner = probeEnv[pi_subagent_ledger_js_1.PI_SUBAGENT_SESSION_OWNER_ENV];
2033
2208
  await oldSession.close("context-window recovery committed")
@@ -2117,11 +2292,13 @@ function createRuntimeHost(options) {
2117
2292
  piProactiveCompactionFailedGeneration: null,
2118
2293
  previousSession: loadPersistedPreviousSession(config, stateStore), readiness: null,
2119
2294
  turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false,
2295
+ authFailureKind: null, authFailureProvider: null,
2120
2296
  promotedInboxUpdateIds: new Set(),
2121
2297
  backgroundCompletionQueue: [], backgroundCompletionKeys: new Set(),
2122
2298
  backgroundCompletionInFlight: null, backgroundCompletionWakeInputId: null,
2123
2299
  backgroundCompletionRejectStreak: 0, backgroundCompletionRetryTimer: null,
2124
2300
  subagentLedger: loadSubagentLedger(config), subagentReconcileTimer: null };
2301
+ restoreAuthFailure(agent);
2125
2302
  const startupConsumed = [];
2126
2303
  const startupQuarantined = [];
2127
2304
  for (const record of agent.records.values()) {