larkin 0.4.1 → 0.4.2

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.
@@ -1244,7 +1244,10 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1244
1244
  if (message.type === "session") {
1245
1245
  const record = agentStates.get(agent.agentId);
1246
1246
  if (record && record.state.sessions[message.runtime] !== message.sessionId) {
1247
- record.state.sessions[message.runtime] = message.sessionId;
1247
+ if (message.sessionId)
1248
+ record.state.sessions[message.runtime] = message.sessionId;
1249
+ else
1250
+ delete record.state.sessions[message.runtime];
1248
1251
  saveAgentState(record);
1249
1252
  log(`持久化 agent=${agent.name} runtime=${message.runtime} sessionId=${message.sessionId}`);
1250
1253
  }
@@ -1392,6 +1395,74 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1392
1395
  : unavailable ? "reset committed but Runtime/channel readiness is unavailable"
1393
1396
  : "reset committed but readiness did not converge before timeout" };
1394
1397
  },
1398
+ async recoverSession(agentId, reason, waitReadyMs = 30_000) {
1399
+ const agent = agents.find((candidate) => candidate.agentId === agentId);
1400
+ if (!agent)
1401
+ throw Object.assign(new Error(`未知 Agent: ${agentId}`), { code: "unknown_agent" });
1402
+ if (reason !== "context-overflow")
1403
+ throw Object.assign(new Error("unsupported recovery reason"), { code: "recovery_refused" });
1404
+ if (!runtimeHost.recoverSession)
1405
+ throw Object.assign(new Error("Runtime session recovery 尚未就绪"), { code: "recovery_unavailable" });
1406
+ const connectionState = (status = hostState.readStatus(agent)) => {
1407
+ const connectedAt = Date.parse(String(status.connectedAt || ""));
1408
+ const channelConnected = Number.isFinite(connectedAt) && connectedAt >= Date.parse(daemonStartedAt) - 1000;
1409
+ return { channelConnected, reconnecting: isChannelReconnecting(status) };
1410
+ };
1411
+ const readinessProjection = () => {
1412
+ const status = hostState.readStatus(agent);
1413
+ const turns = status.session && typeof status.session === "object"
1414
+ ? Math.max(0, Number(status.session.turns) || 0) : 0;
1415
+ const runtimeReady = Boolean(status.runtimeReadiness && typeof status.runtimeReadiness === "object"
1416
+ && status.runtimeReadiness.state === "ready");
1417
+ const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
1418
+ return { turns, runtimeReady, ...connectionState(status), pendingCount };
1419
+ };
1420
+ const initialProjection = readinessProjection();
1421
+ if (!initialProjection.channelConnected)
1422
+ throw Object.assign(new Error(`Agent ${agentId} channel is not connected`), { code: "channel_unavailable", ...initialProjection });
1423
+ if (initialProjection.reconnecting)
1424
+ throw Object.assign(new Error(`Agent ${agentId} channel is reconnecting`), { code: "channel_reconnecting", ...initialProjection });
1425
+ let recovery;
1426
+ try {
1427
+ recovery = await runtimeHost.recoverSession(agentId, reason);
1428
+ }
1429
+ catch (error) {
1430
+ throw Object.assign(error instanceof Error ? error : new Error(String(error)), readinessProjection());
1431
+ }
1432
+ const record = agentStates.get(agentId);
1433
+ try {
1434
+ if (record) {
1435
+ if (recovery.sessionId)
1436
+ record.state.sessions[agent.runtime] = recovery.sessionId;
1437
+ else
1438
+ delete record.state.sessions[agent.runtime];
1439
+ record.store.writeJson("agentState", record.state);
1440
+ }
1441
+ }
1442
+ catch (error) {
1443
+ const projection = readinessProjection();
1444
+ return { recoveryCommitted: true, generationChanged: recovery.generationChanged, sessionChanged: recovery.sessionChanged,
1445
+ ...projection, rearmedCount: recovery.rearmedCount, replayStatus: "pending", remainingPendingCount: projection.pendingCount,
1446
+ readyForFreshScenario: false, inboundObserved: false, code: "state_persistence_failed",
1447
+ error: "recovery committed but Agent session state persistence failed" };
1448
+ }
1449
+ const deadline = Date.now() + Math.max(0, waitReadyMs);
1450
+ let projection = readinessProjection();
1451
+ do {
1452
+ projection = readinessProjection();
1453
+ if (projection.turns === 0 && projection.runtimeReady && projection.channelConnected && !projection.reconnecting)
1454
+ break;
1455
+ if (Date.now() >= deadline)
1456
+ break;
1457
+ await new Promise((resolve) => setTimeout(resolve, Math.min(25, Math.max(1, deadline - Date.now()))));
1458
+ } while (true);
1459
+ const replayStatus = projection.pendingCount === 0 ? "consumed" : "pending";
1460
+ const ready = projection.turns === 0 && projection.runtimeReady && projection.channelConnected && !projection.reconnecting;
1461
+ return { recoveryCommitted: true, generationChanged: recovery.generationChanged, sessionChanged: recovery.sessionChanged,
1462
+ ...projection, rearmedCount: recovery.rearmedCount, replayStatus, remainingPendingCount: projection.pendingCount,
1463
+ readyForFreshScenario: ready, inboundObserved: false,
1464
+ ...(!ready ? { code: "recovery_timeout", error: "recovery committed but Runtime/channel readiness did not converge before timeout" } : {}) };
1465
+ },
1395
1466
  async start() {
1396
1467
  try {
1397
1468
  fs.mkdirSync(larkinHome, { recursive: true });