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.
@@ -208,26 +208,72 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
208
208
  runtimeReadiness: { runtime: agent.runtime, state: "unavailable", reason, nextAction: "Wait for the current daemon epoch to publish Runtime readiness.", observedAt: new Date().toISOString() },
209
209
  });
210
210
  };
211
+ const unresolvedCurrentAuth = (agent) => {
212
+ try {
213
+ const persisted = (0, runtime_readiness_js_1.parsePersistedAuthFailure)(stateStore(agent).readJson("agentState", {}));
214
+ if (!persisted)
215
+ return null;
216
+ // 只使用当前 agentState 作用域标记,不扫描历史 delivery 的 authProvider。
217
+ return (0, runtime_readiness_js_1.authFailureAppliesTo)({
218
+ runtime: agent.runtime,
219
+ model: agent.model,
220
+ piDistribution: agent.piDistribution,
221
+ }, persisted) ? persisted : null;
222
+ }
223
+ catch {
224
+ return null;
225
+ }
226
+ };
227
+ const projectReadyUnlessUnresolvedAuth = (agent, observedAt) => {
228
+ const failure = unresolvedCurrentAuth(agent);
229
+ if (failure) {
230
+ hostState.updateStatus(agent, {
231
+ runtimeReadiness: { ...(0, runtime_readiness_js_1.readinessForPersistedAuthFailure)(failure), observedAt },
232
+ });
233
+ return;
234
+ }
235
+ hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt } });
236
+ };
211
237
  const recordInboundDeliveryFailure = (agent, code) => {
212
238
  const at = new Date().toISOString();
213
239
  const reason = "Inbound Runtime delivery failed non-retryably; durable Inbox/ledger state is retained for recovery.";
214
240
  const nextAction = "Inspect the delivery/status error, correct the Runtime or canonical Inbox state, then restart to replay safely.";
241
+ const currentReadiness = hostState.readStatus(agent).runtimeReadiness;
215
242
  hostState.updateStatus(agent, {
216
243
  inboundDeliveryHealth: { state: "error", code, at, reason, nextAction },
217
- runtimeReadiness: { runtime: agent.runtime, state: "incompatible", reason, nextAction },
244
+ ...(currentReadiness?.state === "unauthenticated" ? {} : {
245
+ runtimeReadiness: { runtime: agent.runtime, state: "incompatible", reason, nextAction },
246
+ }),
218
247
  });
219
248
  hostState.recordStatusError(agent, `${reason} ${nextAction} code=${code}`);
220
249
  };
221
250
  const agentStates = new Map();
251
+ const mergePersistedAgentState = (record) => {
252
+ const latest = record.store.readJson("agentState", {});
253
+ if (isRecord(latest.previousSessions)) {
254
+ record.state.previousSessions = {
255
+ ...latest.previousSessions,
256
+ ...(record.state.previousSessions ?? {}),
257
+ };
258
+ }
259
+ const persisted = (0, runtime_readiness_js_1.parsePersistedAuthFailure)(latest);
260
+ if (persisted) {
261
+ record.state.authFailure = persisted;
262
+ if (persisted.kind === "missing-provider" && persisted.provider) {
263
+ record.state.authFailureProvider = persisted.provider;
264
+ }
265
+ else {
266
+ delete record.state.authFailureProvider;
267
+ }
268
+ }
269
+ else {
270
+ delete record.state.authFailure;
271
+ delete record.state.authFailureProvider;
272
+ }
273
+ };
222
274
  const saveAgentState = (record) => {
223
275
  try {
224
- const latest = record.store.readJson("agentState", {});
225
- if (isRecord(latest.previousSessions)) {
226
- record.state.previousSessions = {
227
- ...latest.previousSessions,
228
- ...(record.state.previousSessions ?? {}),
229
- };
230
- }
276
+ mergePersistedAgentState(record);
231
277
  record.store.writeJson("agentState", record.state);
232
278
  }
233
279
  catch (error) {
@@ -1276,12 +1322,16 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1276
1322
  observedAt,
1277
1323
  },
1278
1324
  });
1279
- else if (message.readiness)
1280
- hostState.updateStatus(agent, { runtimeReadiness: { ...message.readiness, observedAt } });
1325
+ else if (message.readiness) {
1326
+ if (message.readiness.state === "ready" && unresolvedCurrentAuth(agent)) {
1327
+ projectReadyUnlessUnresolvedAuth(agent, observedAt);
1328
+ }
1329
+ else {
1330
+ hostState.updateStatus(agent, { runtimeReadiness: { ...message.readiness, observedAt } });
1331
+ }
1332
+ }
1281
1333
  else if (message.status === "active")
1282
- hostState.updateStatus(agent, { runtimeReadiness: {
1283
- runtime: agent.runtime, state: "ready", observedAt,
1284
- } });
1334
+ projectReadyUnlessUnresolvedAuth(agent, observedAt);
1285
1335
  if (message.status === "active") {
1286
1336
  const redeliveryTimer = setTimeout(() => {
1287
1337
  void reminder.redeliverUnread(agent).catch((error) => log("启动补投失败", errorMessage(error)));
@@ -1448,6 +1498,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1448
1498
  record.state.sessions[agent.runtime] = reset.sessionId;
1449
1499
  else
1450
1500
  delete record.state.sessions[agent.runtime];
1501
+ mergePersistedAgentState(record);
1451
1502
  record.store.writeJson("agentState", record.state);
1452
1503
  }
1453
1504
  if (!reset.sessionId) {
@@ -1455,11 +1506,11 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1455
1506
  hostState.updateStatus(agent, {
1456
1507
  session: { runtime: agent.runtime, id: null, launchId: null, startedAt: observedAt,
1457
1508
  lastSeenAt: null, lastTurnAt: null, turns: 0 },
1458
- runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt },
1459
1509
  });
1510
+ projectReadyUnlessUnresolvedAuth(agent, observedAt);
1460
1511
  }
1461
1512
  else if (reset.runtimeReady) {
1462
- hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt: new Date().toISOString() } });
1513
+ projectReadyUnlessUnresolvedAuth(agent, new Date().toISOString());
1463
1514
  }
1464
1515
  }
1465
1516
  catch (error) {
@@ -1527,7 +1578,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1527
1578
  throw Object.assign(error instanceof Error ? error : new Error(String(error)), readinessProjection());
1528
1579
  }
1529
1580
  if (recovery.runtimeReady) {
1530
- hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt: new Date().toISOString() } });
1581
+ projectReadyUnlessUnresolvedAuth(agent, new Date().toISOString());
1531
1582
  }
1532
1583
  const record = agentStates.get(agentId);
1533
1584
  try {
@@ -1536,6 +1587,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1536
1587
  record.state.sessions[agent.runtime] = recovery.sessionId;
1537
1588
  else
1538
1589
  delete record.state.sessions[agent.runtime];
1590
+ mergePersistedAgentState(record);
1539
1591
  record.store.writeJson("agentState", record.state);
1540
1592
  }
1541
1593
  }
@@ -16,7 +16,7 @@ import { inboxAuditRegistryFile, observeInboxAuditTarget } from "../agent/missed
16
16
  import { HostChannelBusiness } from "./host-channel-business.mjs";
17
17
  import { HostInteractionOrchestrator } from "./interaction-orchestrator.mjs";
18
18
  import { targetFor } from "./message-policy.mjs";
19
- import { providerAuthenticationFailureReadiness, RuntimePrerequisiteError } from "../runtime/runtime-readiness.mjs";
19
+ import { authFailureAppliesTo, parsePersistedAuthFailure, providerAuthenticationFailureReadiness, readinessForPersistedAuthFailure, RuntimePrerequisiteError, } from "../runtime/runtime-readiness.mjs";
20
20
  import { readDocumentCommentSubscription, verifyCallbackProbe } from "../platform/callback-capability.mjs";
21
21
  import { loadConfig, resolveMentionPolicy } from "../platform/config.mjs";
22
22
  import { processCommandToken } from "../app/internal-command.mjs";
@@ -168,26 +168,72 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
168
168
  runtimeReadiness: { runtime: agent.runtime, state: "unavailable", reason, nextAction: "Wait for the current daemon epoch to publish Runtime readiness.", observedAt: new Date().toISOString() },
169
169
  });
170
170
  };
171
+ const unresolvedCurrentAuth = (agent) => {
172
+ try {
173
+ const persisted = parsePersistedAuthFailure(stateStore(agent).readJson("agentState", {}));
174
+ if (!persisted)
175
+ return null;
176
+ // 只使用当前 agentState 作用域标记,不扫描历史 delivery 的 authProvider。
177
+ return authFailureAppliesTo({
178
+ runtime: agent.runtime,
179
+ model: agent.model,
180
+ piDistribution: agent.piDistribution,
181
+ }, persisted) ? persisted : null;
182
+ }
183
+ catch {
184
+ return null;
185
+ }
186
+ };
187
+ const projectReadyUnlessUnresolvedAuth = (agent, observedAt) => {
188
+ const failure = unresolvedCurrentAuth(agent);
189
+ if (failure) {
190
+ hostState.updateStatus(agent, {
191
+ runtimeReadiness: { ...readinessForPersistedAuthFailure(failure), observedAt },
192
+ });
193
+ return;
194
+ }
195
+ hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt } });
196
+ };
171
197
  const recordInboundDeliveryFailure = (agent, code) => {
172
198
  const at = new Date().toISOString();
173
199
  const reason = "Inbound Runtime delivery failed non-retryably; durable Inbox/ledger state is retained for recovery.";
174
200
  const nextAction = "Inspect the delivery/status error, correct the Runtime or canonical Inbox state, then restart to replay safely.";
201
+ const currentReadiness = hostState.readStatus(agent).runtimeReadiness;
175
202
  hostState.updateStatus(agent, {
176
203
  inboundDeliveryHealth: { state: "error", code, at, reason, nextAction },
177
- runtimeReadiness: { runtime: agent.runtime, state: "incompatible", reason, nextAction },
204
+ ...(currentReadiness?.state === "unauthenticated" ? {} : {
205
+ runtimeReadiness: { runtime: agent.runtime, state: "incompatible", reason, nextAction },
206
+ }),
178
207
  });
179
208
  hostState.recordStatusError(agent, `${reason} ${nextAction} code=${code}`);
180
209
  };
181
210
  const agentStates = new Map();
211
+ const mergePersistedAgentState = (record) => {
212
+ const latest = record.store.readJson("agentState", {});
213
+ if (isRecord(latest.previousSessions)) {
214
+ record.state.previousSessions = {
215
+ ...latest.previousSessions,
216
+ ...(record.state.previousSessions ?? {}),
217
+ };
218
+ }
219
+ const persisted = parsePersistedAuthFailure(latest);
220
+ if (persisted) {
221
+ record.state.authFailure = persisted;
222
+ if (persisted.kind === "missing-provider" && persisted.provider) {
223
+ record.state.authFailureProvider = persisted.provider;
224
+ }
225
+ else {
226
+ delete record.state.authFailureProvider;
227
+ }
228
+ }
229
+ else {
230
+ delete record.state.authFailure;
231
+ delete record.state.authFailureProvider;
232
+ }
233
+ };
182
234
  const saveAgentState = (record) => {
183
235
  try {
184
- const latest = record.store.readJson("agentState", {});
185
- if (isRecord(latest.previousSessions)) {
186
- record.state.previousSessions = {
187
- ...latest.previousSessions,
188
- ...(record.state.previousSessions ?? {}),
189
- };
190
- }
236
+ mergePersistedAgentState(record);
191
237
  record.store.writeJson("agentState", record.state);
192
238
  }
193
239
  catch (error) {
@@ -1236,12 +1282,16 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1236
1282
  observedAt,
1237
1283
  },
1238
1284
  });
1239
- else if (message.readiness)
1240
- hostState.updateStatus(agent, { runtimeReadiness: { ...message.readiness, observedAt } });
1285
+ else if (message.readiness) {
1286
+ if (message.readiness.state === "ready" && unresolvedCurrentAuth(agent)) {
1287
+ projectReadyUnlessUnresolvedAuth(agent, observedAt);
1288
+ }
1289
+ else {
1290
+ hostState.updateStatus(agent, { runtimeReadiness: { ...message.readiness, observedAt } });
1291
+ }
1292
+ }
1241
1293
  else if (message.status === "active")
1242
- hostState.updateStatus(agent, { runtimeReadiness: {
1243
- runtime: agent.runtime, state: "ready", observedAt,
1244
- } });
1294
+ projectReadyUnlessUnresolvedAuth(agent, observedAt);
1245
1295
  if (message.status === "active") {
1246
1296
  const redeliveryTimer = setTimeout(() => {
1247
1297
  void reminder.redeliverUnread(agent).catch((error) => log("启动补投失败", errorMessage(error)));
@@ -1408,6 +1458,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1408
1458
  record.state.sessions[agent.runtime] = reset.sessionId;
1409
1459
  else
1410
1460
  delete record.state.sessions[agent.runtime];
1461
+ mergePersistedAgentState(record);
1411
1462
  record.store.writeJson("agentState", record.state);
1412
1463
  }
1413
1464
  if (!reset.sessionId) {
@@ -1415,11 +1466,11 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1415
1466
  hostState.updateStatus(agent, {
1416
1467
  session: { runtime: agent.runtime, id: null, launchId: null, startedAt: observedAt,
1417
1468
  lastSeenAt: null, lastTurnAt: null, turns: 0 },
1418
- runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt },
1419
1469
  });
1470
+ projectReadyUnlessUnresolvedAuth(agent, observedAt);
1420
1471
  }
1421
1472
  else if (reset.runtimeReady) {
1422
- hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt: new Date().toISOString() } });
1473
+ projectReadyUnlessUnresolvedAuth(agent, new Date().toISOString());
1423
1474
  }
1424
1475
  }
1425
1476
  catch (error) {
@@ -1487,7 +1538,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1487
1538
  throw Object.assign(error instanceof Error ? error : new Error(String(error)), readinessProjection());
1488
1539
  }
1489
1540
  if (recovery.runtimeReady) {
1490
- hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt: new Date().toISOString() } });
1541
+ projectReadyUnlessUnresolvedAuth(agent, new Date().toISOString());
1491
1542
  }
1492
1543
  const record = agentStates.get(agentId);
1493
1544
  try {
@@ -1496,6 +1547,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1496
1547
  record.state.sessions[agent.runtime] = recovery.sessionId;
1497
1548
  else
1498
1549
  delete record.state.sessions[agent.runtime];
1550
+ mergePersistedAgentState(record);
1499
1551
  record.store.writeJson("agentState", record.state);
1500
1552
  }
1501
1553
  }
@@ -53,6 +53,8 @@ exports.resolveAgentGlobalMentionPolicy = resolveAgentGlobalMentionPolicy;
53
53
  exports.runtimeConfigSignature = runtimeConfigSignature;
54
54
  exports.configApplyState = configApplyState;
55
55
  exports.markConfigApplied = markConfigApplied;
56
+ exports.withConfigMutationLock = withConfigMutationLock;
57
+ exports.withConfigMutationLockAsync = withConfigMutationLockAsync;
56
58
  exports.mutateConfig = mutateConfig;
57
59
  exports.rollbackConfig = rollbackConfig;
58
60
  exports.commitSetupConfig = commitSetupConfig;
@@ -652,7 +654,7 @@ function applyMutation(config, mutation) {
652
654
  (0, pi_provider_config_js_1.assertBuiltinPiAgentDirectory)((0, pi_provider_config_js_1.piAgentDirectory)(config.configDir, mutation.agentId));
653
655
  }
654
656
  catch (error) {
655
- throw new Error(`无法切换到 builtin-pi:${error instanceof Error ? error.message : String(error)}。请先运行 larkin setup 并为该 Agent 选择 builtin-pi 完成 provider 配置`);
657
+ throw new Error(`无法切换到 builtin-pi:${error instanceof Error ? error.message : String(error)}。请使用 Dashboard Provider Credentials 或 larkin pi-auth login。`);
656
658
  }
657
659
  }
658
660
  assertModel(stored.runtime, mutation.model || "default");
@@ -665,6 +667,9 @@ function applyMutation(config, mutation) {
665
667
  delete agent.effort;
666
668
  }
667
669
  else if (mutation.kind === "set-agent-model") {
670
+ if (mutation.requireBuiltinPi && (agent.runtime !== "pi" || agent.piDistribution !== "builtin")) {
671
+ throw new Error(`Agent ${mutation.agentId} 不是内置 Pi`);
672
+ }
668
673
  if (!mutation.model.trim())
669
674
  throw new Error("model 不能为空");
670
675
  assertModel(agent.runtime, mutation.model);
@@ -815,7 +820,7 @@ function assertConfigRoot(root) {
815
820
  if (typeof process.getuid === "function" && stat.uid !== process.getuid())
816
821
  throw new Error("config root owner is unsafe");
817
822
  }
818
- function withConfigLock(layout, action) {
823
+ function acquireConfigLock(layout) {
819
824
  assertNoSymlinkAncestors(layout.root);
820
825
  fs.mkdirSync(layout.root, { recursive: true, mode: 0o700 });
821
826
  const rootStat = fs.lstatSync(layout.root);
@@ -854,20 +859,43 @@ function withConfigLock(layout, action) {
854
859
  }
855
860
  if (!acquired)
856
861
  throw new Error("配置正被其他进程修改,请稍后重试");
862
+ return {
863
+ release() {
864
+ const current = readLockRecord(lock);
865
+ if (current?.record?.nonce === owner.nonce && current.record.pid === owner.pid) {
866
+ try {
867
+ fs.unlinkSync(lock);
868
+ (0, secure_metadata_js_1.fsyncDirectoryOf)(lock);
869
+ }
870
+ catch { /* best effort */ }
871
+ }
872
+ },
873
+ };
874
+ }
875
+ function withConfigLock(layout, action) {
876
+ const held = acquireConfigLock(layout);
857
877
  try {
858
878
  recoverMigrationJournal(layout.root);
859
879
  recoverRollbackJournal(layout.root);
860
880
  return action();
861
881
  }
862
882
  finally {
863
- const current = readLockRecord(lock);
864
- if (current?.record?.nonce === owner.nonce && current.record.pid === owner.pid) {
865
- try {
866
- fs.unlinkSync(lock);
867
- (0, secure_metadata_js_1.fsyncDirectoryOf)(lock);
868
- }
869
- catch { /* best effort */ }
870
- }
883
+ held.release();
884
+ }
885
+ }
886
+ function withConfigMutationLock(env, action) {
887
+ return withConfigLock(root_layout_js_1.TargetRootLayout.fromConfigDir(resolveConfigDir(env)), action);
888
+ }
889
+ async function withConfigMutationLockAsync(env, action) {
890
+ const layout = root_layout_js_1.TargetRootLayout.fromConfigDir(resolveConfigDir(env));
891
+ const held = acquireConfigLock(layout);
892
+ try {
893
+ recoverMigrationJournal(layout.root);
894
+ recoverRollbackJournal(layout.root);
895
+ return await action();
896
+ }
897
+ finally {
898
+ held.release();
871
899
  }
872
900
  }
873
901
  const CONFIG_ROLLBACK_JOURNAL_FILE = ".config-rollback-journal.json";
@@ -586,7 +586,7 @@ function applyMutation(config, mutation) {
586
586
  assertBuiltinPiAgentDirectory(piAgentDirectory(config.configDir, mutation.agentId));
587
587
  }
588
588
  catch (error) {
589
- throw new Error(`无法切换到 builtin-pi:${error instanceof Error ? error.message : String(error)}。请先运行 larkin setup 并为该 Agent 选择 builtin-pi 完成 provider 配置`);
589
+ throw new Error(`无法切换到 builtin-pi:${error instanceof Error ? error.message : String(error)}。请使用 Dashboard Provider Credentials 或 larkin pi-auth login。`);
590
590
  }
591
591
  }
592
592
  assertModel(stored.runtime, mutation.model || "default");
@@ -599,6 +599,9 @@ function applyMutation(config, mutation) {
599
599
  delete agent.effort;
600
600
  }
601
601
  else if (mutation.kind === "set-agent-model") {
602
+ if (mutation.requireBuiltinPi && (agent.runtime !== "pi" || agent.piDistribution !== "builtin")) {
603
+ throw new Error(`Agent ${mutation.agentId} 不是内置 Pi`);
604
+ }
602
605
  if (!mutation.model.trim())
603
606
  throw new Error("model 不能为空");
604
607
  assertModel(agent.runtime, mutation.model);
@@ -749,7 +752,7 @@ function assertConfigRoot(root) {
749
752
  if (typeof process.getuid === "function" && stat.uid !== process.getuid())
750
753
  throw new Error("config root owner is unsafe");
751
754
  }
752
- function withConfigLock(layout, action) {
755
+ function acquireConfigLock(layout) {
753
756
  assertNoSymlinkAncestors(layout.root);
754
757
  fs.mkdirSync(layout.root, { recursive: true, mode: 0o700 });
755
758
  const rootStat = fs.lstatSync(layout.root);
@@ -788,20 +791,43 @@ function withConfigLock(layout, action) {
788
791
  }
789
792
  if (!acquired)
790
793
  throw new Error("配置正被其他进程修改,请稍后重试");
794
+ return {
795
+ release() {
796
+ const current = readLockRecord(lock);
797
+ if (current?.record?.nonce === owner.nonce && current.record.pid === owner.pid) {
798
+ try {
799
+ fs.unlinkSync(lock);
800
+ fsyncDirectoryOf(lock);
801
+ }
802
+ catch { /* best effort */ }
803
+ }
804
+ },
805
+ };
806
+ }
807
+ function withConfigLock(layout, action) {
808
+ const held = acquireConfigLock(layout);
791
809
  try {
792
810
  recoverMigrationJournal(layout.root);
793
811
  recoverRollbackJournal(layout.root);
794
812
  return action();
795
813
  }
796
814
  finally {
797
- const current = readLockRecord(lock);
798
- if (current?.record?.nonce === owner.nonce && current.record.pid === owner.pid) {
799
- try {
800
- fs.unlinkSync(lock);
801
- fsyncDirectoryOf(lock);
802
- }
803
- catch { /* best effort */ }
804
- }
815
+ held.release();
816
+ }
817
+ }
818
+ export function withConfigMutationLock(env, action) {
819
+ return withConfigLock(TargetRootLayout.fromConfigDir(resolveConfigDir(env)), action);
820
+ }
821
+ export async function withConfigMutationLockAsync(env, action) {
822
+ const layout = TargetRootLayout.fromConfigDir(resolveConfigDir(env));
823
+ const held = acquireConfigLock(layout);
824
+ try {
825
+ recoverMigrationJournal(layout.root);
826
+ recoverRollbackJournal(layout.root);
827
+ return await action();
828
+ }
829
+ finally {
830
+ held.release();
805
831
  }
806
832
  }
807
833
  const CONFIG_ROLLBACK_JOURNAL_FILE = ".config-rollback-journal.json";