larkin 0.4.28 → 0.4.29

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.
@@ -6,7 +6,8 @@ import { auditReminderDelivery } from "../agent/reminder-delivery-audit.mjs";
6
6
  import { isCanonicalInboxTarget, targetKeyOfInboxEnvelope } from "../agent/inbox-projection.mjs";
7
7
  import { SpanKind } from "@opentelemetry/api";
8
8
  import { buildStrictProviderErrorInput, classifyStrictProviderError } from "./provider-error-classifier.mjs";
9
- import { classifyRuntimePrerequisite, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
9
+ import { authFailureAppliesTo, classifyPiMissingCredentialRejection, classifyRuntimePrerequisite, configuredProviderId, isBuiltinPiDistribution, missingProviderCredentialReadiness, parsePersistedAuthFailure, providerAuthenticationFailureReadiness, readinessForPersistedAuthFailure, safeProviderId, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
10
+ import { officialPiHasStoredProvider } from "./pi-official-auth.mjs";
10
11
  import { resolveOfficialLarkCli } from "../app/official-lark-cli.mjs";
11
12
  import { assertAgentWorkspaceBound, managedLarkCliEnv } from "../app/agent-lark-cli-workspace.mjs";
12
13
  import { isPiNativeCompactionRequired, PiCompactionBreaker, PiCompactionRecoveryMachine, } from "./pi-compaction-recovery.mjs";
@@ -93,6 +94,121 @@ function persistPreviousSession(agent, previous) {
93
94
  // In-memory ref still holds for this process; restart will miss it only if this write failed.
94
95
  }
95
96
  }
97
+ function isBuiltinPiAgent(agent) {
98
+ return agent.adapter.id === "pi"
99
+ && (isBuiltinPiDistribution(agent.config.piDistribution) || agent.config.runtime === "builtin-pi");
100
+ }
101
+ function authFailureScopeOf(agent) {
102
+ return {
103
+ runtime: agent.config.runtime,
104
+ model: agent.config.model,
105
+ piDistribution: agent.config.piDistribution,
106
+ adapterId: agent.adapter.id,
107
+ };
108
+ }
109
+ function currentAuthFailure(agent) {
110
+ if (!agent.authFailureActive)
111
+ return null;
112
+ const kind = agent.authFailureKind
113
+ ?? (agent.authFailureProvider ? "missing-provider" : "generic");
114
+ if (kind === "missing-provider") {
115
+ if (!agent.authFailureProvider)
116
+ return null;
117
+ return { kind: "missing-provider", runtime: "pi", piDistribution: "builtin", provider: agent.authFailureProvider };
118
+ }
119
+ const runtime = agent.adapter.id === "codex" || agent.adapter.id === "claude" || agent.adapter.id === "pi"
120
+ ? agent.adapter.id : null;
121
+ if (!runtime)
122
+ return null;
123
+ return {
124
+ kind: "generic",
125
+ runtime,
126
+ ...(runtime === "pi" && (agent.config.piDistribution === "builtin" || agent.config.piDistribution === "external")
127
+ ? { piDistribution: agent.config.piDistribution } : {}),
128
+ provider: agent.authFailureProvider,
129
+ };
130
+ }
131
+ function genericAuthProvider(agent, upstream) {
132
+ return safeProviderId(upstream?.provider) ?? configuredProviderId(agent.config.model);
133
+ }
134
+ function persistAuthFailure(agent) {
135
+ const store = agent.stateStore;
136
+ if (!store)
137
+ return;
138
+ try {
139
+ const write = () => {
140
+ const current = store.readJson("agentState", { sessions: {} });
141
+ if (Array.isArray(current.records) && !("sessions" in current))
142
+ return;
143
+ const { authFailureProvider: _legacy, authFailure: _scoped, ...rest } = current;
144
+ const failure = currentAuthFailure(agent);
145
+ if (!failure) {
146
+ if (!("authFailureProvider" in current) && !("authFailure" in current))
147
+ return;
148
+ store.writeJson("agentState", rest);
149
+ return;
150
+ }
151
+ store.writeJson("agentState", {
152
+ ...rest,
153
+ authFailure: failure,
154
+ ...(failure.kind === "missing-provider" && failure.provider
155
+ ? { authFailureProvider: failure.provider } : {}),
156
+ });
157
+ };
158
+ if (store.withInboxTransaction)
159
+ store.withInboxTransaction(write);
160
+ else
161
+ write();
162
+ }
163
+ catch {
164
+ // In-memory flags and the delivery ledger still hold for this process.
165
+ }
166
+ }
167
+ function loadPersistedAuthFailure(store) {
168
+ if (!store)
169
+ return null;
170
+ try {
171
+ return parsePersistedAuthFailure(store.readJson("agentState", {}));
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ }
177
+ function applyAuthFailure(agent, failure) {
178
+ agent.authFailureActive = true;
179
+ agent.authFailureKind = failure.kind;
180
+ agent.authFailureProvider = failure.provider ?? null;
181
+ agent.readiness = readinessForPersistedAuthFailure(failure);
182
+ }
183
+ function clearAuthFailure(agent, persist = true) {
184
+ agent.authFailureActive = false;
185
+ agent.authFailureKind = null;
186
+ agent.authFailureProvider = null;
187
+ if (persist)
188
+ persistAuthFailure(agent);
189
+ }
190
+ function restoreAuthFailure(agent) {
191
+ const persisted = loadPersistedAuthFailure(agent.stateStore);
192
+ if (!persisted)
193
+ return;
194
+ // 只恢复仍绑定当前 candidate 的当前作用域 auth;不从历史 delivery 回灌。
195
+ if (!authFailureAppliesTo(authFailureScopeOf(agent), persisted)) {
196
+ clearAuthFailure(agent);
197
+ return;
198
+ }
199
+ applyAuthFailure(agent, persisted);
200
+ }
201
+ function clearLedgerAuthProviders(agent) {
202
+ let changed = false;
203
+ for (const record of agent.records.values()) {
204
+ if (!record.authProvider)
205
+ continue;
206
+ delete record.authProvider;
207
+ changed = true;
208
+ }
209
+ if (changed)
210
+ persist(agent);
211
+ }
96
212
  const REPLAY_FAILURE_CODES = new Set([
97
213
  "canonical_inbox_row_missing", "canonical_inbox_malformed", "duplicate_message_id",
98
214
  "inbox_state_conflict", "delivery_target_conflict", "structured_target_invalid",
@@ -481,6 +597,34 @@ export function createRuntimeHost(options) {
481
597
  messageId: record.messageId, status: "consumed" });
482
598
  }
483
599
  };
600
+ const ownedConfigDir = () => {
601
+ const value = process.env.LARKIN_CONFIG_DIR?.trim();
602
+ return value || null;
603
+ };
604
+ const projectAuthFailure = (agent, readiness, failure) => {
605
+ agent.authFailureActive = true;
606
+ agent.authFailureKind = failure.kind;
607
+ agent.authFailureProvider = failure.provider;
608
+ agent.readiness = {
609
+ ...readiness,
610
+ ...(agent.readiness?.executable ? { executable: agent.readiness.executable } : {}),
611
+ ...(agent.readiness?.version ? { version: agent.readiness.version } : {}),
612
+ };
613
+ persistAuthFailure(agent);
614
+ emit({ type: "agent-status", agentId: agent.config.agentId, status: "error",
615
+ error: `${agent.readiness.reason} ${agent.readiness.nextAction}`, readiness: agent.readiness });
616
+ };
617
+ const namedAuthProviderPresent = (agent) => {
618
+ if (agent.authFailureKind !== "missing-provider" || !agent.authFailureProvider)
619
+ return true;
620
+ const failure = currentAuthFailure(agent);
621
+ if (!failure || !authFailureAppliesTo(authFailureScopeOf(agent), failure))
622
+ return false;
623
+ const configDir = ownedConfigDir();
624
+ if (!configDir)
625
+ return false;
626
+ return officialPiHasStoredProvider(configDir, agent.config.agentId, agent.authFailureProvider);
627
+ };
484
628
  const resultFor = (agent, record, result) => {
485
629
  if (result.status === "accepted") {
486
630
  if (record.errorCategory !== "context_window") {
@@ -494,6 +638,28 @@ export function createRuntimeHost(options) {
494
638
  deliveryId: record.deliveryId, messageId: record.messageId, status: "accepted" });
495
639
  return { status: "accepted", deliveryId: record.deliveryId };
496
640
  }
641
+ const missing = result.status === "rejected" && isBuiltinPiAgent(agent)
642
+ ? classifyPiMissingCredentialRejection(result.reason)
643
+ : null;
644
+ if (missing) {
645
+ const readiness = missingProviderCredentialReadiness(agent.adapter.id, missing.provider);
646
+ const reason = readiness.reason ?? `Provider ${missing.provider} is missing from this Agent's official credential store.`;
647
+ if (record.status !== "error") {
648
+ record.reason = reason;
649
+ record.retryable = false;
650
+ record.errorCategory = "auth";
651
+ record.authProvider = missing.provider;
652
+ const finalRecord = setRecord(agent, record, "error");
653
+ if (finalRecord.status !== "consumed")
654
+ emit({ type: "delivery", agentId: agent.config.agentId,
655
+ deliveryId: record.deliveryId, messageId: record.messageId, status: "error", reason });
656
+ projectAuthFailure(agent, readiness, { kind: "missing-provider", provider: missing.provider });
657
+ }
658
+ return { status: "error", deliveryId: record.deliveryId, reason: record.reason ?? reason, retryable: false };
659
+ }
660
+ if (record.status === "error") {
661
+ return { status: "error", deliveryId: record.deliveryId, reason: record.reason ?? result.reason, retryable: false };
662
+ }
497
663
  if (record.errorCategory !== "context_window")
498
664
  delete record.errorCategory;
499
665
  const retryable = result.status === "deferred" || result.retryable;
@@ -1338,7 +1504,7 @@ export function createRuntimeHost(options) {
1338
1504
  agent.compactionMachines.delete(inputId);
1339
1505
  }
1340
1506
  const recoveredAuthentication = agent.authFailureActive && agent.turnInProgress
1341
- && agent.turnHadAuthenticatedOutput && !agent.turnHadFailure;
1507
+ && agent.turnHadAuthenticatedOutput && !agent.turnHadFailure && namedAuthProviderPresent(agent);
1342
1508
  agent.turnInProgress = false;
1343
1509
  agent.busy = false;
1344
1510
  emit({ type: "activity", agentId: agent.config.agentId, activity: "idle", activityKind: "idle", detailKind: "turn_ended" });
@@ -1360,7 +1526,8 @@ export function createRuntimeHost(options) {
1360
1526
  reconcileSubagentLedger(agent);
1361
1527
  telemetry?.runtimeEvent(agent.config.agentId, event);
1362
1528
  if (recoveredAuthentication) {
1363
- agent.authFailureActive = false;
1529
+ clearAuthFailure(agent);
1530
+ clearLedgerAuthProviders(agent);
1364
1531
  const prior = agent.readiness;
1365
1532
  agent.readiness = {
1366
1533
  runtime: agent.adapter.id,
@@ -1453,21 +1620,24 @@ export function createRuntimeHost(options) {
1453
1620
  record.errorCategory = classifiedCategory;
1454
1621
  else
1455
1622
  delete record.errorCategory;
1623
+ const missing = event.errorCategory === "auth" && !event.retryable && isBuiltinPiAgent(agent)
1624
+ ? classifyPiMissingCredentialRejection(event.upstream?.message)
1625
+ ?? classifyPiMissingCredentialRejection(event.message)
1626
+ : null;
1627
+ if (missing)
1628
+ record.authProvider = missing.provider;
1456
1629
  const finalRecord = setRecord(agent, record, event.retryable ? "pending" : "error");
1457
1630
  if (finalRecord.status !== "consumed")
1458
1631
  emit({ type: "delivery", agentId: agent.config.agentId,
1459
1632
  deliveryId: record.deliveryId, messageId: record.messageId,
1460
1633
  status: event.retryable ? "deferred" : "error", reason: event.message });
1461
1634
  if (event.errorCategory === "auth" && !event.retryable) {
1462
- const readiness = providerAuthenticationFailureReadiness(agent.adapter.id, event.upstream?.provider);
1463
- agent.authFailureActive = true;
1464
- agent.readiness = {
1465
- ...readiness,
1466
- ...(agent.readiness?.executable ? { executable: agent.readiness.executable } : {}),
1467
- ...(agent.readiness?.version ? { version: agent.readiness.version } : {}),
1468
- };
1469
- emit({ type: "agent-status", agentId: agent.config.agentId, status: "error",
1470
- error: `${agent.readiness.reason} ${agent.readiness.nextAction}`, readiness: agent.readiness });
1635
+ const readiness = missing
1636
+ ? missingProviderCredentialReadiness(agent.adapter.id, missing.provider)
1637
+ : providerAuthenticationFailureReadiness(agent.adapter.id, event.upstream?.provider);
1638
+ projectAuthFailure(agent, readiness, missing
1639
+ ? { kind: "missing-provider", provider: missing.provider }
1640
+ : { kind: "generic", provider: genericAuthProvider(agent, event.upstream) });
1471
1641
  }
1472
1642
  if (event.retryable && !agent.busy && !agent.turnInProgress)
1473
1643
  void retryPending(agent);
@@ -1739,14 +1909,27 @@ export function createRuntimeHost(options) {
1739
1909
  clearTimeout(previous.backgroundCompletionRetryTimer);
1740
1910
  if (previous.subagentReconcileTimer)
1741
1911
  clearInterval(previous.subagentReconcileTimer);
1912
+ const previousFailure = currentAuthFailure(previous);
1913
+ const scoped = Boolean(previousFailure
1914
+ && authFailureAppliesTo({
1915
+ runtime: config.runtime,
1916
+ model: config.model,
1917
+ piDistribution: config.piDistribution,
1918
+ adapterId: adapter.id,
1919
+ }, previousFailure));
1742
1920
  const candidate = {
1743
1921
  ...previous, config, adapter, session, launchId: crypto.randomUUID(), busy: false, submitting: false,
1744
1922
  starting: null, retryAfterSubmit: false, generation: 0, poller: null, retryTimer: null,
1745
1923
  recreateAttempts: 0, stabilityTimer: null, recreateReason: null, stopped: false,
1746
1924
  backgroundCompletionRetryTimer: null, subagentReconcileTimer: null,
1747
1925
  disabledReason: null, configurationRecovery: null,
1748
- readiness: previous.authFailureActive ? previous.readiness : readiness,
1926
+ authFailureActive: scoped && previous.authFailureActive,
1927
+ authFailureKind: scoped ? previous.authFailureKind : null,
1928
+ authFailureProvider: scoped ? previous.authFailureProvider : null,
1929
+ readiness: scoped && previous.authFailureActive ? previous.readiness : readiness,
1749
1930
  };
1931
+ if (previous.authFailureActive && !candidate.authFailureActive)
1932
+ clearAuthFailure(candidate);
1750
1933
  managed.set(config.agentId, candidate);
1751
1934
  session.subscribe((event) => observe(candidate, session, event));
1752
1935
  if (session.sessionId) {
@@ -1841,7 +2024,8 @@ export function createRuntimeHost(options) {
1841
2024
  agent.config.sessionId = null;
1842
2025
  agent.session = fresh;
1843
2026
  persistPreviousSession(agent, previousRef);
1844
- agent.readiness = readiness;
2027
+ if (!agent.authFailureActive)
2028
+ agent.readiness = readiness;
1845
2029
  agent.disabledReason = null;
1846
2030
  };
1847
2031
  try {
@@ -1862,7 +2046,13 @@ export function createRuntimeHost(options) {
1862
2046
  ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}),
1863
2047
  ...(agent.previousSession ? { previousSession: agent.previousSession } : {}) });
1864
2048
  }
1865
- emit({ type: "agent-status", agentId, status: "active", readiness });
2049
+ emit({
2050
+ type: "agent-status",
2051
+ agentId,
2052
+ status: agent.authFailureActive ? "error" : "active",
2053
+ ...(agent.authFailureActive ? { error: `${agent.readiness?.reason} ${agent.readiness?.nextAction}` } : {}),
2054
+ readiness: agent.authFailureActive ? agent.readiness ?? readiness : readiness,
2055
+ });
1866
2056
  agent.piSessionOwner = probeEnv[PI_SUBAGENT_SESSION_OWNER_ENV];
1867
2057
  await oldSession.close("fresh session reset committed")
1868
2058
  .catch((error) => log("previous runtime close after fresh reset failed", String(error)));
@@ -2000,7 +2190,8 @@ export function createRuntimeHost(options) {
2000
2190
  agent.config.sessionId = null;
2001
2191
  agent.session = fresh;
2002
2192
  persistPreviousSession(agent, previousRef);
2003
- agent.readiness = readiness;
2193
+ if (!agent.authFailureActive)
2194
+ agent.readiness = readiness;
2004
2195
  agent.disabledReason = null;
2005
2196
  for (const record of agent.records.values()) {
2006
2197
  if (!messageIds.includes(record.messageId))
@@ -2018,7 +2209,13 @@ export function createRuntimeHost(options) {
2018
2209
  ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}),
2019
2210
  ...(agent.previousSession ? { previousSession: agent.previousSession } : {}) });
2020
2211
  }
2021
- emit({ type: "agent-status", agentId, status: "active", readiness });
2212
+ emit({
2213
+ type: "agent-status",
2214
+ agentId,
2215
+ status: agent.authFailureActive ? "error" : "active",
2216
+ ...(agent.authFailureActive ? { error: `${agent.readiness?.reason} ${agent.readiness?.nextAction}` } : {}),
2217
+ readiness: agent.authFailureActive ? agent.readiness ?? readiness : readiness,
2218
+ });
2022
2219
  durableRollback = null;
2023
2220
  agent.piSessionOwner = probeEnv[PI_SUBAGENT_SESSION_OWNER_ENV];
2024
2221
  await oldSession.close("context-window recovery committed")
@@ -2108,11 +2305,13 @@ export function createRuntimeHost(options) {
2108
2305
  piProactiveCompactionFailedGeneration: null,
2109
2306
  previousSession: loadPersistedPreviousSession(config, stateStore), readiness: null,
2110
2307
  turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false,
2308
+ authFailureKind: null, authFailureProvider: null,
2111
2309
  promotedInboxUpdateIds: new Set(),
2112
2310
  backgroundCompletionQueue: [], backgroundCompletionKeys: new Set(),
2113
2311
  backgroundCompletionInFlight: null, backgroundCompletionWakeInputId: null,
2114
2312
  backgroundCompletionRejectStreak: 0, backgroundCompletionRetryTimer: null,
2115
2313
  subagentLedger: loadSubagentLedger(config), subagentReconcileTimer: null };
2314
+ restoreAuthFailure(agent);
2116
2315
  const startupConsumed = [];
2117
2316
  const startupQuarantined = [];
2118
2317
  for (const record of agent.records.values()) {
@@ -37,7 +37,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.RuntimePrerequisiteError = void 0;
40
+ exports.safeProviderId = safeProviderId;
41
+ exports.isBuiltinPiDistribution = isBuiltinPiDistribution;
42
+ exports.configuredProviderId = configuredProviderId;
43
+ exports.parsePersistedAuthFailure = parsePersistedAuthFailure;
44
+ exports.authFailureAppliesTo = authFailureAppliesTo;
45
+ exports.readinessForPersistedAuthFailure = readinessForPersistedAuthFailure;
46
+ exports.classifyPiMissingCredentialRejection = classifyPiMissingCredentialRejection;
40
47
  exports.providerAuthenticationFailureReadiness = providerAuthenticationFailureReadiness;
48
+ exports.missingProviderCredentialReadiness = missingProviderCredentialReadiness;
41
49
  exports.resolveRuntimeExecutable = resolveRuntimeExecutable;
42
50
  exports.classifyRuntimePrerequisite = classifyRuntimePrerequisite;
43
51
  exports.probeNativeRuntimeReadiness = probeNativeRuntimeReadiness;
@@ -49,10 +57,103 @@ const root_layout_js_1 = require("../platform/root-layout.cjs");
49
57
  const builtin_pi_assets_js_1 = require("./builtin-pi-assets.cjs");
50
58
  const pi_provider_config_js_1 = require("./pi-provider-config.cjs");
51
59
  const process_boundary_trace_js_1 = require("../platform/process-boundary-trace.cjs");
52
- function safeProviderLabel(value) {
60
+ function safeProviderId(value) {
53
61
  const provider = typeof value === "string" ? value.trim() : "";
54
62
  return /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(provider) ? provider : null;
55
63
  }
64
+ function safeProviderLabel(value) {
65
+ return safeProviderId(value);
66
+ }
67
+ function isBuiltinPiDistribution(value) {
68
+ return value === "builtin";
69
+ }
70
+ /** Model ids are `provider/model`; only the provider prefix is used for scope. */
71
+ function configuredProviderId(model) {
72
+ const value = typeof model === "string" ? model.trim() : "";
73
+ const slash = value.indexOf("/");
74
+ return slash > 0 ? safeProviderId(value.slice(0, slash)) : null;
75
+ }
76
+ function persistedRuntime(value) {
77
+ return value === "pi" || value === "codex" || value === "claude" ? value : null;
78
+ }
79
+ function parsePersistedAuthFailure(state) {
80
+ if (!state || typeof state !== "object" || Array.isArray(state))
81
+ return null;
82
+ const record = state;
83
+ if (record.authFailure && typeof record.authFailure === "object" && !Array.isArray(record.authFailure)) {
84
+ const failure = record.authFailure;
85
+ const kind = failure.kind === "missing-provider" || failure.kind === "generic" ? failure.kind : null;
86
+ const runtime = persistedRuntime(failure.runtime);
87
+ if (!kind || !runtime)
88
+ return null;
89
+ const provider = safeProviderId(failure.provider);
90
+ if (kind === "missing-provider" && !provider)
91
+ return null;
92
+ const piDistribution = failure.piDistribution === "builtin" || failure.piDistribution === "external"
93
+ ? failure.piDistribution : undefined;
94
+ return {
95
+ kind,
96
+ runtime,
97
+ ...(piDistribution ? { piDistribution } : {}),
98
+ provider,
99
+ };
100
+ }
101
+ const legacy = safeProviderId(record.authFailureProvider);
102
+ return legacy
103
+ ? { kind: "missing-provider", runtime: "pi", piDistribution: "builtin", provider: legacy }
104
+ : null;
105
+ }
106
+ function currentAdapterId(current) {
107
+ return current.adapterId ?? (current.runtime === "builtin-pi" ? "pi" : current.runtime);
108
+ }
109
+ function currentIsBuiltinPi(current) {
110
+ return currentAdapterId(current) === "pi"
111
+ && (isBuiltinPiDistribution(current.piDistribution) || current.runtime === "builtin-pi");
112
+ }
113
+ /**
114
+ * Missing-provider and known-provider generic markers apply only to that provider.
115
+ * Generic without a provider is conservative fallback: it still binds runtime /
116
+ * builtin distribution, but cannot reject an A→B model switch because the
117
+ * upstream provider was genuinely unavailable.
118
+ */
119
+ function authFailureAppliesTo(current, failure) {
120
+ if (failure.kind === "missing-provider") {
121
+ return currentIsBuiltinPi(current)
122
+ && failure.runtime === "pi"
123
+ && failure.piDistribution === "builtin"
124
+ && Boolean(failure.provider)
125
+ && configuredProviderId(current.model) === failure.provider;
126
+ }
127
+ if (failure.runtime !== currentAdapterId(current))
128
+ return false;
129
+ if (failure.runtime === "pi") {
130
+ const persistedBuiltin = failure.piDistribution === "builtin";
131
+ if (currentIsBuiltinPi(current) !== persistedBuiltin)
132
+ return false;
133
+ }
134
+ if (!failure.provider)
135
+ return true;
136
+ const currentProvider = configuredProviderId(current.model);
137
+ if (currentIsBuiltinPi(current))
138
+ return currentProvider === failure.provider;
139
+ return currentProvider ? currentProvider === failure.provider : true;
140
+ }
141
+ function readinessForPersistedAuthFailure(failure) {
142
+ if (failure.kind === "missing-provider") {
143
+ return missingProviderCredentialReadiness(failure.runtime, failure.provider);
144
+ }
145
+ return providerAuthenticationFailureReadiness(failure.runtime, failure.provider);
146
+ }
147
+ const MISSING_CREDENTIAL_REJECTION = /^(?:Pi RPC (?:prompt|steer) failed: )?(No API key found for|No login found for) ([A-Za-z0-9][A-Za-z0-9._-]{0,79})$/;
148
+ /** Narrow match for an explicit Pi absent-key / absent-login diagnostic. */
149
+ function classifyPiMissingCredentialRejection(message) {
150
+ const text = typeof message === "string"
151
+ ? message.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim()
152
+ : "";
153
+ const match = MISSING_CREDENTIAL_REJECTION.exec(text);
154
+ const provider = match ? safeProviderLabel(match[2]) : null;
155
+ return provider && match ? { provider, diagnostic: `${match[1]} ${provider}` } : null;
156
+ }
56
157
  function providerAuthenticationFailureReadiness(runtime, provider) {
57
158
  const label = safeProviderLabel(provider);
58
159
  return {
@@ -64,6 +165,22 @@ function providerAuthenticationFailureReadiness(runtime, provider) {
64
165
  nextAction: "Check the provider login or API-key resolver command, then retry the Agent turn.",
65
166
  };
66
167
  }
168
+ /** P0 分类;Pi nextAction 共用 UX preset 恢复文案,不写官方 provider ID。 */
169
+ function missingProviderCredentialReadiness(runtime, provider) {
170
+ const label = safeProviderLabel(provider);
171
+ return {
172
+ runtime,
173
+ state: "unauthenticated",
174
+ reason: label
175
+ ? `Provider ${label} is missing from this Agent's official credential store.`
176
+ : "The configured provider is missing from this Agent's official credential store.",
177
+ nextAction: runtime === "pi"
178
+ ? (0, pi_provider_config_js_1.builtinPiProviderRecoveryNextAction)({ providerId: label ?? undefined })
179
+ : label
180
+ ? `Add the missing ${label} credential to this Agent's official store, then retry the Agent turn.`
181
+ : "Add the missing provider credential to this Agent's official store, then retry the Agent turn.",
182
+ };
183
+ }
67
184
  class RuntimePrerequisiteError extends Error {
68
185
  readiness;
69
186
  constructor(readiness) {
@@ -100,7 +217,10 @@ function classifyRuntimePrerequisite(runtime, error, executable) {
100
217
  if (/no authenticated available models|login|credential|unauthenticated|unauthorized|oauth/i.test(reason))
101
218
  return {
102
219
  runtime, state: "unauthenticated", ...(executable ? { executable } : {}), reason,
103
- nextAction: runtime === "pi" ? "Run the official `pi` login flow, then retry." : `Authenticate ${runtime}, then retry.`,
220
+ // 此分类没有 distribution:外部 Pi 不能指向 Dashboard / pi-auth login。
221
+ nextAction: runtime === "pi"
222
+ ? "Run the official `pi` login flow, then retry."
223
+ : `Authenticate ${runtime}, then retry.`,
104
224
  };
105
225
  if (/protocol (?:version )?(?:mismatch|unsupported|incompatible)|unsupported (?:rpc|protocol|schema)|schema (?:mismatch|incompatible)|requires (?:a )?newer version/i.test(reason))
106
226
  return {
@@ -151,7 +271,7 @@ async function probeNativeRuntimeReadiness(options) {
151
271
  catch (error) {
152
272
  (0, process_boundary_trace_js_1.traceProcessBoundary)(env, "readiness:builtin-pi-failure", { configDir: env.LARKIN_CONFIG_DIR, agentId: options.agentId, targetDir: options.agentId && env.LARKIN_CONFIG_DIR ? (0, pi_provider_config_js_1.piAgentDirectory)(env.LARKIN_CONFIG_DIR, options.agentId) : undefined, error });
153
273
  return { runtime: "pi", state: "unauthenticated", reason: error instanceof Error ? error.message : String(error),
154
- nextAction: "重新运行 larkin setup,选择内置 Pi 并配置有效 API Key。" };
274
+ nextAction: (0, pi_provider_config_js_1.builtinPiProviderRecoveryNextAction)({ agentId: options.agentId }) };
155
275
  }
156
276
  }
157
277
  const command = selectedCommand(options);
@@ -4,12 +4,105 @@ import path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { resolveConfigDir as resolveRootConfigDir } from "../platform/root-layout.mjs";
6
6
  import { applyPiPackageDirForChild, piChildDistributionFromOverrides } from "./builtin-pi-assets.mjs";
7
- import { assertBuiltinPiAgentDirectory, BUNDLED_PI_VERSION, ownedPiCatalogAgentDir, piAgentDirectory } from "./pi-provider-config.mjs";
7
+ import { assertBuiltinPiAgentDirectory, BUNDLED_PI_VERSION, builtinPiProviderRecoveryNextAction, ownedPiCatalogAgentDir, piAgentDirectory } from "./pi-provider-config.mjs";
8
8
  import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
9
- function safeProviderLabel(value) {
9
+ export function safeProviderId(value) {
10
10
  const provider = typeof value === "string" ? value.trim() : "";
11
11
  return /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(provider) ? provider : null;
12
12
  }
13
+ function safeProviderLabel(value) {
14
+ return safeProviderId(value);
15
+ }
16
+ export function isBuiltinPiDistribution(value) {
17
+ return value === "builtin";
18
+ }
19
+ /** Model ids are `provider/model`; only the provider prefix is used for scope. */
20
+ export function configuredProviderId(model) {
21
+ const value = typeof model === "string" ? model.trim() : "";
22
+ const slash = value.indexOf("/");
23
+ return slash > 0 ? safeProviderId(value.slice(0, slash)) : null;
24
+ }
25
+ function persistedRuntime(value) {
26
+ return value === "pi" || value === "codex" || value === "claude" ? value : null;
27
+ }
28
+ export function parsePersistedAuthFailure(state) {
29
+ if (!state || typeof state !== "object" || Array.isArray(state))
30
+ return null;
31
+ const record = state;
32
+ if (record.authFailure && typeof record.authFailure === "object" && !Array.isArray(record.authFailure)) {
33
+ const failure = record.authFailure;
34
+ const kind = failure.kind === "missing-provider" || failure.kind === "generic" ? failure.kind : null;
35
+ const runtime = persistedRuntime(failure.runtime);
36
+ if (!kind || !runtime)
37
+ return null;
38
+ const provider = safeProviderId(failure.provider);
39
+ if (kind === "missing-provider" && !provider)
40
+ return null;
41
+ const piDistribution = failure.piDistribution === "builtin" || failure.piDistribution === "external"
42
+ ? failure.piDistribution : undefined;
43
+ return {
44
+ kind,
45
+ runtime,
46
+ ...(piDistribution ? { piDistribution } : {}),
47
+ provider,
48
+ };
49
+ }
50
+ const legacy = safeProviderId(record.authFailureProvider);
51
+ return legacy
52
+ ? { kind: "missing-provider", runtime: "pi", piDistribution: "builtin", provider: legacy }
53
+ : null;
54
+ }
55
+ function currentAdapterId(current) {
56
+ return current.adapterId ?? (current.runtime === "builtin-pi" ? "pi" : current.runtime);
57
+ }
58
+ function currentIsBuiltinPi(current) {
59
+ return currentAdapterId(current) === "pi"
60
+ && (isBuiltinPiDistribution(current.piDistribution) || current.runtime === "builtin-pi");
61
+ }
62
+ /**
63
+ * Missing-provider and known-provider generic markers apply only to that provider.
64
+ * Generic without a provider is conservative fallback: it still binds runtime /
65
+ * builtin distribution, but cannot reject an A→B model switch because the
66
+ * upstream provider was genuinely unavailable.
67
+ */
68
+ export function authFailureAppliesTo(current, failure) {
69
+ if (failure.kind === "missing-provider") {
70
+ return currentIsBuiltinPi(current)
71
+ && failure.runtime === "pi"
72
+ && failure.piDistribution === "builtin"
73
+ && Boolean(failure.provider)
74
+ && configuredProviderId(current.model) === failure.provider;
75
+ }
76
+ if (failure.runtime !== currentAdapterId(current))
77
+ return false;
78
+ if (failure.runtime === "pi") {
79
+ const persistedBuiltin = failure.piDistribution === "builtin";
80
+ if (currentIsBuiltinPi(current) !== persistedBuiltin)
81
+ return false;
82
+ }
83
+ if (!failure.provider)
84
+ return true;
85
+ const currentProvider = configuredProviderId(current.model);
86
+ if (currentIsBuiltinPi(current))
87
+ return currentProvider === failure.provider;
88
+ return currentProvider ? currentProvider === failure.provider : true;
89
+ }
90
+ export function readinessForPersistedAuthFailure(failure) {
91
+ if (failure.kind === "missing-provider") {
92
+ return missingProviderCredentialReadiness(failure.runtime, failure.provider);
93
+ }
94
+ return providerAuthenticationFailureReadiness(failure.runtime, failure.provider);
95
+ }
96
+ const MISSING_CREDENTIAL_REJECTION = /^(?:Pi RPC (?:prompt|steer) failed: )?(No API key found for|No login found for) ([A-Za-z0-9][A-Za-z0-9._-]{0,79})$/;
97
+ /** Narrow match for an explicit Pi absent-key / absent-login diagnostic. */
98
+ export function classifyPiMissingCredentialRejection(message) {
99
+ const text = typeof message === "string"
100
+ ? message.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim()
101
+ : "";
102
+ const match = MISSING_CREDENTIAL_REJECTION.exec(text);
103
+ const provider = match ? safeProviderLabel(match[2]) : null;
104
+ return provider && match ? { provider, diagnostic: `${match[1]} ${provider}` } : null;
105
+ }
13
106
  export function providerAuthenticationFailureReadiness(runtime, provider) {
14
107
  const label = safeProviderLabel(provider);
15
108
  return {
@@ -21,6 +114,22 @@ export function providerAuthenticationFailureReadiness(runtime, provider) {
21
114
  nextAction: "Check the provider login or API-key resolver command, then retry the Agent turn.",
22
115
  };
23
116
  }
117
+ /** P0 分类;Pi nextAction 共用 UX preset 恢复文案,不写官方 provider ID。 */
118
+ export function missingProviderCredentialReadiness(runtime, provider) {
119
+ const label = safeProviderLabel(provider);
120
+ return {
121
+ runtime,
122
+ state: "unauthenticated",
123
+ reason: label
124
+ ? `Provider ${label} is missing from this Agent's official credential store.`
125
+ : "The configured provider is missing from this Agent's official credential store.",
126
+ nextAction: runtime === "pi"
127
+ ? builtinPiProviderRecoveryNextAction({ providerId: label ?? undefined })
128
+ : label
129
+ ? `Add the missing ${label} credential to this Agent's official store, then retry the Agent turn.`
130
+ : "Add the missing provider credential to this Agent's official store, then retry the Agent turn.",
131
+ };
132
+ }
24
133
  export class RuntimePrerequisiteError extends Error {
25
134
  readiness;
26
135
  constructor(readiness) {
@@ -56,7 +165,10 @@ export function classifyRuntimePrerequisite(runtime, error, executable) {
56
165
  if (/no authenticated available models|login|credential|unauthenticated|unauthorized|oauth/i.test(reason))
57
166
  return {
58
167
  runtime, state: "unauthenticated", ...(executable ? { executable } : {}), reason,
59
- nextAction: runtime === "pi" ? "Run the official `pi` login flow, then retry." : `Authenticate ${runtime}, then retry.`,
168
+ // 此分类没有 distribution:外部 Pi 不能指向 Dashboard / pi-auth login。
169
+ nextAction: runtime === "pi"
170
+ ? "Run the official `pi` login flow, then retry."
171
+ : `Authenticate ${runtime}, then retry.`,
60
172
  };
61
173
  if (/protocol (?:version )?(?:mismatch|unsupported|incompatible)|unsupported (?:rpc|protocol|schema)|schema (?:mismatch|incompatible)|requires (?:a )?newer version/i.test(reason))
62
174
  return {
@@ -107,7 +219,7 @@ export async function probeNativeRuntimeReadiness(options) {
107
219
  catch (error) {
108
220
  traceProcessBoundary(env, "readiness:builtin-pi-failure", { configDir: env.LARKIN_CONFIG_DIR, agentId: options.agentId, targetDir: options.agentId && env.LARKIN_CONFIG_DIR ? piAgentDirectory(env.LARKIN_CONFIG_DIR, options.agentId) : undefined, error });
109
221
  return { runtime: "pi", state: "unauthenticated", reason: error instanceof Error ? error.message : String(error),
110
- nextAction: "重新运行 larkin setup,选择内置 Pi 并配置有效 API Key。" };
222
+ nextAction: builtinPiProviderRecoveryNextAction({ agentId: options.agentId }) };
111
223
  }
112
224
  }
113
225
  const command = selectedCommand(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.4.28",
3
+ "version": "0.4.29",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [