larkin 0.4.1 → 0.4.5

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 (42) hide show
  1. package/README.md +76 -33
  2. package/artifacts/release/THIRD_PARTY_NOTICES.txt +218 -228
  3. package/assets/readme/larkin-github-cover.png +0 -0
  4. package/assets/readme/larkin-hero.svg +111 -0
  5. package/dist/agent/agent-state-store.cjs +193 -1
  6. package/dist/agent/agent-state-store.mjs +191 -0
  7. package/dist/app/agent-config.mjs +73 -6
  8. package/dist/app/agent-readiness.cjs +22 -2
  9. package/dist/app/agent-readiness.mjs +20 -2
  10. package/dist/app/binary-entry.mjs +6 -0
  11. package/dist/app/cli.mjs +7 -1
  12. package/dist/app/local-control.mjs +108 -2
  13. package/dist/app/run.mjs +8 -0
  14. package/dist/app/runtime-process.mjs +17 -1
  15. package/dist/app/session-cli.mjs +55 -20
  16. package/dist/dashboard/dashboard-view-model.mjs +12 -3
  17. package/dist/feishu/host-shell.cjs +104 -9
  18. package/dist/feishu/host-shell.mjs +105 -10
  19. package/dist/platform/config.cjs +406 -33
  20. package/dist/platform/config.mjs +405 -33
  21. package/dist/platform/process-boundary-trace.cjs +108 -0
  22. package/dist/platform/process-boundary-trace.mjs +102 -0
  23. package/dist/runtime/pi-artifact-provenance.cjs +147 -0
  24. package/dist/runtime/pi-artifact-provenance.mjs +139 -0
  25. package/dist/runtime/pi-compaction-recovery.cjs +703 -0
  26. package/dist/runtime/pi-compaction-recovery.mjs +679 -0
  27. package/dist/runtime/pi-model-catalog.cjs +2 -2
  28. package/dist/runtime/pi-model-catalog.mjs +2 -2
  29. package/dist/runtime/pi-profile-migration.cjs +556 -0
  30. package/dist/runtime/pi-profile-migration.mjs +515 -0
  31. package/dist/runtime/pi-provider-config.cjs +3 -3
  32. package/dist/runtime/pi-provider-config.mjs +3 -3
  33. package/dist/runtime/pi-rpc-client.cjs +39 -22
  34. package/dist/runtime/pi-rpc-client.mjs +39 -22
  35. package/dist/runtime/provider-error-classifier.cjs +39 -0
  36. package/dist/runtime/provider-error-classifier.mjs +34 -0
  37. package/dist/runtime/runtime-adapters.mjs +176 -27
  38. package/dist/runtime/runtime-host.cjs +630 -19
  39. package/dist/runtime/runtime-host.mjs +628 -18
  40. package/dist/runtime/runtime-readiness.cjs +6 -4
  41. package/dist/runtime/runtime-readiness.mjs +6 -4
  42. package/package.json +3 -3
@@ -200,6 +200,11 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
200
200
  return store;
201
201
  };
202
202
  const hostState = new host_business_state_js_1.HostStateProjection(stateStore, log);
203
+ const markRuntimeTransition = (agent, reason) => {
204
+ hostState.updateStatus(agent, {
205
+ runtimeReadiness: { runtime: agent.runtime, state: "unavailable", reason, nextAction: "Wait for the current daemon epoch to publish Runtime readiness.", observedAt: new Date().toISOString() },
206
+ });
207
+ };
203
208
  const recordInboundDeliveryFailure = (agent, code) => {
204
209
  const at = new Date().toISOString();
205
210
  const reason = "Inbound Runtime delivery failed non-retryably; durable Inbox/ledger state is retained for recovery.";
@@ -1231,19 +1236,21 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1231
1236
  return;
1232
1237
  if (message.type === "agent-status") {
1233
1238
  log("agent:status", message.agentId, message.status);
1239
+ const observedAt = new Date().toISOString();
1234
1240
  if (message.status === "error" || message.status === "inactive")
1235
1241
  hostState.updateStatus(agent, {
1236
- runtimeReadiness: message.readiness?.state && message.readiness.state !== "ready" ? message.readiness : {
1242
+ runtimeReadiness: message.readiness?.state && message.readiness.state !== "ready" ? { ...message.readiness, observedAt } : {
1237
1243
  runtime: agent.runtime,
1238
1244
  state: message.status === "error" ? "incompatible" : "missing",
1239
1245
  reason: message.status === "error" ? message.error || "Runtime entered an error state" : "Runtime is inactive",
1246
+ observedAt,
1240
1247
  },
1241
1248
  });
1242
1249
  else if (message.readiness)
1243
- hostState.updateStatus(agent, { runtimeReadiness: message.readiness });
1250
+ hostState.updateStatus(agent, { runtimeReadiness: { ...message.readiness, observedAt } });
1244
1251
  else if (message.status === "active")
1245
1252
  hostState.updateStatus(agent, { runtimeReadiness: {
1246
- runtime: agent.runtime, state: "ready",
1253
+ runtime: agent.runtime, state: "ready", observedAt,
1247
1254
  } });
1248
1255
  if (message.status === "active") {
1249
1256
  const redeliveryTimer = setTimeout(() => {
@@ -1284,7 +1291,10 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1284
1291
  if (message.type === "session") {
1285
1292
  const record = agentStates.get(agent.agentId);
1286
1293
  if (record && record.state.sessions[message.runtime] !== message.sessionId) {
1287
- record.state.sessions[message.runtime] = message.sessionId;
1294
+ if (message.sessionId)
1295
+ record.state.sessions[message.runtime] = message.sessionId;
1296
+ else
1297
+ delete record.state.sessions[message.runtime];
1288
1298
  saveAgentState(record);
1289
1299
  log(`持久化 agent=${agent.name} runtime=${message.runtime} sessionId=${message.sessionId}`);
1290
1300
  }
@@ -1372,8 +1382,10 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1372
1382
  const status = hostState.readStatus(agent);
1373
1383
  const turns = status.session && typeof status.session === "object"
1374
1384
  ? Math.max(0, Number(status.session.turns) || 0) : 0;
1375
- const runtimeReady = Boolean(status.runtimeReadiness && typeof status.runtimeReadiness === "object"
1376
- && status.runtimeReadiness.state === "ready");
1385
+ const sessionStartedAt = status.session && typeof status.session === "object"
1386
+ ? Date.parse(String(status.session.startedAt || "")) : Number.NaN;
1387
+ const sessionCurrent = Number.isFinite(sessionStartedAt) && sessionStartedAt >= Date.parse(daemonStartedAt);
1388
+ const runtimeReady = sessionCurrent && (0, agent_readiness_js_1.isRuntimeReadinessCurrent)(status.runtimeReadiness, daemonStartedAt);
1377
1389
  const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
1378
1390
  return { turns, runtimeReady, ...connectionState(status), pendingCount };
1379
1391
  };
@@ -1382,6 +1394,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1382
1394
  throw Object.assign(new Error(`Agent ${agentId} channel is not connected`), { code: "channel_unavailable", ...initialProjection });
1383
1395
  if (initialProjection.reconnecting)
1384
1396
  throw Object.assign(new Error(`Agent ${agentId} channel is reconnecting`), { code: "channel_reconnecting", ...initialProjection });
1397
+ markRuntimeTransition(agent, "Runtime session reset in progress");
1385
1398
  let reset;
1386
1399
  try {
1387
1400
  reset = await runtimeHost.resetSession(agentId);
@@ -1398,11 +1411,17 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1398
1411
  delete record.state.sessions[agent.runtime];
1399
1412
  record.store.writeJson("agentState", record.state);
1400
1413
  }
1401
- if (!reset.sessionId)
1414
+ if (!reset.sessionId) {
1415
+ const observedAt = new Date().toISOString();
1402
1416
  hostState.updateStatus(agent, {
1403
- session: { runtime: agent.runtime, id: null, launchId: null, startedAt: new Date().toISOString(),
1404
- lastSeenAt: null, lastTurnAt: null, turns: 0 }, runtimeReadiness: { runtime: agent.runtime, state: "ready" },
1417
+ session: { runtime: agent.runtime, id: null, launchId: null, startedAt: observedAt,
1418
+ lastSeenAt: null, lastTurnAt: null, turns: 0 },
1419
+ runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt },
1405
1420
  });
1421
+ }
1422
+ else if (reset.runtimeReady) {
1423
+ hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt: new Date().toISOString() } });
1424
+ }
1406
1425
  }
1407
1426
  catch (error) {
1408
1427
  const projection = readinessProjection();
@@ -1432,9 +1451,85 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1432
1451
  : unavailable ? "reset committed but Runtime/channel readiness is unavailable"
1433
1452
  : "reset committed but readiness did not converge before timeout" };
1434
1453
  },
1454
+ async recoverSession(agentId, reason, waitReadyMs = 30_000) {
1455
+ const agent = agents.find((candidate) => candidate.agentId === agentId);
1456
+ if (!agent)
1457
+ throw Object.assign(new Error(`未知 Agent: ${agentId}`), { code: "unknown_agent" });
1458
+ if (reason !== "context-overflow")
1459
+ throw Object.assign(new Error("unsupported recovery reason"), { code: "recovery_refused" });
1460
+ if (!runtimeHost.recoverSession)
1461
+ throw Object.assign(new Error("Runtime session recovery 尚未就绪"), { code: "recovery_unavailable" });
1462
+ const connectionState = (status = hostState.readStatus(agent)) => {
1463
+ const connectedAt = Date.parse(String(status.connectedAt || ""));
1464
+ const channelConnected = Number.isFinite(connectedAt) && connectedAt >= Date.parse(daemonStartedAt) - 1000;
1465
+ return { channelConnected, reconnecting: (0, agent_readiness_js_1.isChannelReconnecting)(status) };
1466
+ };
1467
+ const readinessProjection = () => {
1468
+ const status = hostState.readStatus(agent);
1469
+ const turns = status.session && typeof status.session === "object"
1470
+ ? Math.max(0, Number(status.session.turns) || 0) : 0;
1471
+ const sessionStartedAt = status.session && typeof status.session === "object"
1472
+ ? Date.parse(String(status.session.startedAt || "")) : Number.NaN;
1473
+ const sessionCurrent = Number.isFinite(sessionStartedAt) && sessionStartedAt >= Date.parse(daemonStartedAt);
1474
+ const runtimeReady = sessionCurrent && (0, agent_readiness_js_1.isRuntimeReadinessCurrent)(status.runtimeReadiness, daemonStartedAt);
1475
+ const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
1476
+ return { turns, runtimeReady, ...connectionState(status), pendingCount };
1477
+ };
1478
+ const initialProjection = readinessProjection();
1479
+ if (!initialProjection.channelConnected)
1480
+ throw Object.assign(new Error(`Agent ${agentId} channel is not connected`), { code: "channel_unavailable", ...initialProjection });
1481
+ if (initialProjection.reconnecting)
1482
+ throw Object.assign(new Error(`Agent ${agentId} channel is reconnecting`), { code: "channel_reconnecting", ...initialProjection });
1483
+ markRuntimeTransition(agent, "Runtime session recovery in progress");
1484
+ let recovery;
1485
+ try {
1486
+ recovery = await runtimeHost.recoverSession(agentId, reason);
1487
+ }
1488
+ catch (error) {
1489
+ throw Object.assign(error instanceof Error ? error : new Error(String(error)), readinessProjection());
1490
+ }
1491
+ if (recovery.runtimeReady) {
1492
+ hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt: new Date().toISOString() } });
1493
+ }
1494
+ const record = agentStates.get(agentId);
1495
+ try {
1496
+ if (record) {
1497
+ if (recovery.sessionId)
1498
+ record.state.sessions[agent.runtime] = recovery.sessionId;
1499
+ else
1500
+ delete record.state.sessions[agent.runtime];
1501
+ record.store.writeJson("agentState", record.state);
1502
+ }
1503
+ }
1504
+ catch (error) {
1505
+ const projection = readinessProjection();
1506
+ return { recoveryCommitted: true, generationChanged: recovery.generationChanged, sessionChanged: recovery.sessionChanged,
1507
+ ...projection, rearmedCount: recovery.rearmedCount, replayStatus: "pending", remainingPendingCount: projection.pendingCount,
1508
+ readyForFreshScenario: false, inboundObserved: false, code: "state_persistence_failed",
1509
+ error: "recovery committed but Agent session state persistence failed" };
1510
+ }
1511
+ const deadline = Date.now() + Math.max(0, waitReadyMs);
1512
+ let projection = readinessProjection();
1513
+ do {
1514
+ projection = readinessProjection();
1515
+ if (projection.turns === 0 && projection.runtimeReady && projection.channelConnected && !projection.reconnecting)
1516
+ break;
1517
+ if (Date.now() >= deadline)
1518
+ break;
1519
+ await new Promise((resolve) => setTimeout(resolve, Math.min(25, Math.max(1, deadline - Date.now()))));
1520
+ } while (true);
1521
+ const replayStatus = projection.pendingCount === 0 ? "consumed" : "pending";
1522
+ const ready = projection.turns === 0 && projection.runtimeReady && projection.channelConnected && !projection.reconnecting;
1523
+ return { recoveryCommitted: true, generationChanged: recovery.generationChanged, sessionChanged: recovery.sessionChanged,
1524
+ ...projection, rearmedCount: recovery.rearmedCount, replayStatus, remainingPendingCount: projection.pendingCount,
1525
+ readyForFreshScenario: ready, inboundObserved: false,
1526
+ ...(!ready ? { code: "recovery_timeout", error: "recovery committed but Runtime/channel readiness did not converge before timeout" } : {}) };
1527
+ },
1435
1528
  async start() {
1436
1529
  try {
1437
1530
  node_fs_1.default.mkdirSync(larkinHome, { recursive: true });
1531
+ for (const agent of agents)
1532
+ markRuntimeTransition(agent, "Current daemon epoch starting");
1438
1533
  node_fs_1.default.writeFileSync(node_path_1.default.join(larkinHome, "daemon-status.json"), JSON.stringify({
1439
1534
  ...(0, process_inspect_cjs_1.currentProcessMetadata)((0, internal_command_js_1.processCommandToken)("daemon", "app/runtime-process.mjs")), pid: process.pid, startedAt: daemonStartedAt,
1440
1535
  agents: agents.map((agent) => agent.agentId),
@@ -19,7 +19,7 @@ import { readDocumentCommentSubscription, verifyCallbackProbe } from "../platfor
19
19
  import { loadConfig, resolveMentionPolicy } from "../platform/config.mjs";
20
20
  import { processCommandToken } from "../app/internal-command.mjs";
21
21
  import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
22
- import { isChannelReconnecting } from "../app/agent-readiness.mjs";
22
+ import { isChannelReconnecting, isRuntimeReadinessCurrent } from "../app/agent-readiness.mjs";
23
23
  import { documentCommentMessageId, documentCommentNoticeType, projectDocumentCommentEnvelope, resolveDocumentCommentContext, } from "./document-comment.mjs";
24
24
  const PENDING_DOCUMENT_COMMENT_LIMIT = 256;
25
25
  function isRecord(value) {
@@ -160,6 +160,11 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
160
160
  return store;
161
161
  };
162
162
  const hostState = new HostStateProjection(stateStore, log);
163
+ const markRuntimeTransition = (agent, reason) => {
164
+ hostState.updateStatus(agent, {
165
+ runtimeReadiness: { runtime: agent.runtime, state: "unavailable", reason, nextAction: "Wait for the current daemon epoch to publish Runtime readiness.", observedAt: new Date().toISOString() },
166
+ });
167
+ };
163
168
  const recordInboundDeliveryFailure = (agent, code) => {
164
169
  const at = new Date().toISOString();
165
170
  const reason = "Inbound Runtime delivery failed non-retryably; durable Inbox/ledger state is retained for recovery.";
@@ -1191,19 +1196,21 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1191
1196
  return;
1192
1197
  if (message.type === "agent-status") {
1193
1198
  log("agent:status", message.agentId, message.status);
1199
+ const observedAt = new Date().toISOString();
1194
1200
  if (message.status === "error" || message.status === "inactive")
1195
1201
  hostState.updateStatus(agent, {
1196
- runtimeReadiness: message.readiness?.state && message.readiness.state !== "ready" ? message.readiness : {
1202
+ runtimeReadiness: message.readiness?.state && message.readiness.state !== "ready" ? { ...message.readiness, observedAt } : {
1197
1203
  runtime: agent.runtime,
1198
1204
  state: message.status === "error" ? "incompatible" : "missing",
1199
1205
  reason: message.status === "error" ? message.error || "Runtime entered an error state" : "Runtime is inactive",
1206
+ observedAt,
1200
1207
  },
1201
1208
  });
1202
1209
  else if (message.readiness)
1203
- hostState.updateStatus(agent, { runtimeReadiness: message.readiness });
1210
+ hostState.updateStatus(agent, { runtimeReadiness: { ...message.readiness, observedAt } });
1204
1211
  else if (message.status === "active")
1205
1212
  hostState.updateStatus(agent, { runtimeReadiness: {
1206
- runtime: agent.runtime, state: "ready",
1213
+ runtime: agent.runtime, state: "ready", observedAt,
1207
1214
  } });
1208
1215
  if (message.status === "active") {
1209
1216
  const redeliveryTimer = setTimeout(() => {
@@ -1244,7 +1251,10 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1244
1251
  if (message.type === "session") {
1245
1252
  const record = agentStates.get(agent.agentId);
1246
1253
  if (record && record.state.sessions[message.runtime] !== message.sessionId) {
1247
- record.state.sessions[message.runtime] = message.sessionId;
1254
+ if (message.sessionId)
1255
+ record.state.sessions[message.runtime] = message.sessionId;
1256
+ else
1257
+ delete record.state.sessions[message.runtime];
1248
1258
  saveAgentState(record);
1249
1259
  log(`持久化 agent=${agent.name} runtime=${message.runtime} sessionId=${message.sessionId}`);
1250
1260
  }
@@ -1332,8 +1342,10 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1332
1342
  const status = hostState.readStatus(agent);
1333
1343
  const turns = status.session && typeof status.session === "object"
1334
1344
  ? Math.max(0, Number(status.session.turns) || 0) : 0;
1335
- const runtimeReady = Boolean(status.runtimeReadiness && typeof status.runtimeReadiness === "object"
1336
- && status.runtimeReadiness.state === "ready");
1345
+ const sessionStartedAt = status.session && typeof status.session === "object"
1346
+ ? Date.parse(String(status.session.startedAt || "")) : Number.NaN;
1347
+ const sessionCurrent = Number.isFinite(sessionStartedAt) && sessionStartedAt >= Date.parse(daemonStartedAt);
1348
+ const runtimeReady = sessionCurrent && isRuntimeReadinessCurrent(status.runtimeReadiness, daemonStartedAt);
1337
1349
  const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
1338
1350
  return { turns, runtimeReady, ...connectionState(status), pendingCount };
1339
1351
  };
@@ -1342,6 +1354,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1342
1354
  throw Object.assign(new Error(`Agent ${agentId} channel is not connected`), { code: "channel_unavailable", ...initialProjection });
1343
1355
  if (initialProjection.reconnecting)
1344
1356
  throw Object.assign(new Error(`Agent ${agentId} channel is reconnecting`), { code: "channel_reconnecting", ...initialProjection });
1357
+ markRuntimeTransition(agent, "Runtime session reset in progress");
1345
1358
  let reset;
1346
1359
  try {
1347
1360
  reset = await runtimeHost.resetSession(agentId);
@@ -1358,11 +1371,17 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1358
1371
  delete record.state.sessions[agent.runtime];
1359
1372
  record.store.writeJson("agentState", record.state);
1360
1373
  }
1361
- if (!reset.sessionId)
1374
+ if (!reset.sessionId) {
1375
+ const observedAt = new Date().toISOString();
1362
1376
  hostState.updateStatus(agent, {
1363
- session: { runtime: agent.runtime, id: null, launchId: null, startedAt: new Date().toISOString(),
1364
- lastSeenAt: null, lastTurnAt: null, turns: 0 }, runtimeReadiness: { runtime: agent.runtime, state: "ready" },
1377
+ session: { runtime: agent.runtime, id: null, launchId: null, startedAt: observedAt,
1378
+ lastSeenAt: null, lastTurnAt: null, turns: 0 },
1379
+ runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt },
1365
1380
  });
1381
+ }
1382
+ else if (reset.runtimeReady) {
1383
+ hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt: new Date().toISOString() } });
1384
+ }
1366
1385
  }
1367
1386
  catch (error) {
1368
1387
  const projection = readinessProjection();
@@ -1392,9 +1411,85 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1392
1411
  : unavailable ? "reset committed but Runtime/channel readiness is unavailable"
1393
1412
  : "reset committed but readiness did not converge before timeout" };
1394
1413
  },
1414
+ async recoverSession(agentId, reason, waitReadyMs = 30_000) {
1415
+ const agent = agents.find((candidate) => candidate.agentId === agentId);
1416
+ if (!agent)
1417
+ throw Object.assign(new Error(`未知 Agent: ${agentId}`), { code: "unknown_agent" });
1418
+ if (reason !== "context-overflow")
1419
+ throw Object.assign(new Error("unsupported recovery reason"), { code: "recovery_refused" });
1420
+ if (!runtimeHost.recoverSession)
1421
+ throw Object.assign(new Error("Runtime session recovery 尚未就绪"), { code: "recovery_unavailable" });
1422
+ const connectionState = (status = hostState.readStatus(agent)) => {
1423
+ const connectedAt = Date.parse(String(status.connectedAt || ""));
1424
+ const channelConnected = Number.isFinite(connectedAt) && connectedAt >= Date.parse(daemonStartedAt) - 1000;
1425
+ return { channelConnected, reconnecting: isChannelReconnecting(status) };
1426
+ };
1427
+ const readinessProjection = () => {
1428
+ const status = hostState.readStatus(agent);
1429
+ const turns = status.session && typeof status.session === "object"
1430
+ ? Math.max(0, Number(status.session.turns) || 0) : 0;
1431
+ const sessionStartedAt = status.session && typeof status.session === "object"
1432
+ ? Date.parse(String(status.session.startedAt || "")) : Number.NaN;
1433
+ const sessionCurrent = Number.isFinite(sessionStartedAt) && sessionStartedAt >= Date.parse(daemonStartedAt);
1434
+ const runtimeReady = sessionCurrent && isRuntimeReadinessCurrent(status.runtimeReadiness, daemonStartedAt);
1435
+ const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
1436
+ return { turns, runtimeReady, ...connectionState(status), pendingCount };
1437
+ };
1438
+ const initialProjection = readinessProjection();
1439
+ if (!initialProjection.channelConnected)
1440
+ throw Object.assign(new Error(`Agent ${agentId} channel is not connected`), { code: "channel_unavailable", ...initialProjection });
1441
+ if (initialProjection.reconnecting)
1442
+ throw Object.assign(new Error(`Agent ${agentId} channel is reconnecting`), { code: "channel_reconnecting", ...initialProjection });
1443
+ markRuntimeTransition(agent, "Runtime session recovery in progress");
1444
+ let recovery;
1445
+ try {
1446
+ recovery = await runtimeHost.recoverSession(agentId, reason);
1447
+ }
1448
+ catch (error) {
1449
+ throw Object.assign(error instanceof Error ? error : new Error(String(error)), readinessProjection());
1450
+ }
1451
+ if (recovery.runtimeReady) {
1452
+ hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt: new Date().toISOString() } });
1453
+ }
1454
+ const record = agentStates.get(agentId);
1455
+ try {
1456
+ if (record) {
1457
+ if (recovery.sessionId)
1458
+ record.state.sessions[agent.runtime] = recovery.sessionId;
1459
+ else
1460
+ delete record.state.sessions[agent.runtime];
1461
+ record.store.writeJson("agentState", record.state);
1462
+ }
1463
+ }
1464
+ catch (error) {
1465
+ const projection = readinessProjection();
1466
+ return { recoveryCommitted: true, generationChanged: recovery.generationChanged, sessionChanged: recovery.sessionChanged,
1467
+ ...projection, rearmedCount: recovery.rearmedCount, replayStatus: "pending", remainingPendingCount: projection.pendingCount,
1468
+ readyForFreshScenario: false, inboundObserved: false, code: "state_persistence_failed",
1469
+ error: "recovery committed but Agent session state persistence failed" };
1470
+ }
1471
+ const deadline = Date.now() + Math.max(0, waitReadyMs);
1472
+ let projection = readinessProjection();
1473
+ do {
1474
+ projection = readinessProjection();
1475
+ if (projection.turns === 0 && projection.runtimeReady && projection.channelConnected && !projection.reconnecting)
1476
+ break;
1477
+ if (Date.now() >= deadline)
1478
+ break;
1479
+ await new Promise((resolve) => setTimeout(resolve, Math.min(25, Math.max(1, deadline - Date.now()))));
1480
+ } while (true);
1481
+ const replayStatus = projection.pendingCount === 0 ? "consumed" : "pending";
1482
+ const ready = projection.turns === 0 && projection.runtimeReady && projection.channelConnected && !projection.reconnecting;
1483
+ return { recoveryCommitted: true, generationChanged: recovery.generationChanged, sessionChanged: recovery.sessionChanged,
1484
+ ...projection, rearmedCount: recovery.rearmedCount, replayStatus, remainingPendingCount: projection.pendingCount,
1485
+ readyForFreshScenario: ready, inboundObserved: false,
1486
+ ...(!ready ? { code: "recovery_timeout", error: "recovery committed but Runtime/channel readiness did not converge before timeout" } : {}) };
1487
+ },
1395
1488
  async start() {
1396
1489
  try {
1397
1490
  fs.mkdirSync(larkinHome, { recursive: true });
1491
+ for (const agent of agents)
1492
+ markRuntimeTransition(agent, "Current daemon epoch starting");
1398
1493
  fs.writeFileSync(path.join(larkinHome, "daemon-status.json"), JSON.stringify({
1399
1494
  ...currentProcessMetadata(processCommandToken("daemon", "app/runtime-process.mjs")), pid: process.pid, startedAt: daemonStartedAt,
1400
1495
  agents: agents.map((agent) => agent.agentId),