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.
- package/README.md +76 -33
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +218 -228
- package/assets/readme/larkin-github-cover.png +0 -0
- package/assets/readme/larkin-hero.svg +111 -0
- package/dist/agent/agent-state-store.cjs +193 -1
- package/dist/agent/agent-state-store.mjs +191 -0
- package/dist/app/agent-config.mjs +73 -6
- package/dist/app/agent-readiness.cjs +22 -2
- package/dist/app/agent-readiness.mjs +20 -2
- package/dist/app/binary-entry.mjs +6 -0
- package/dist/app/cli.mjs +7 -1
- package/dist/app/local-control.mjs +108 -2
- package/dist/app/run.mjs +8 -0
- package/dist/app/runtime-process.mjs +17 -1
- package/dist/app/session-cli.mjs +55 -20
- package/dist/dashboard/dashboard-view-model.mjs +12 -3
- package/dist/feishu/host-shell.cjs +104 -9
- package/dist/feishu/host-shell.mjs +105 -10
- package/dist/platform/config.cjs +406 -33
- package/dist/platform/config.mjs +405 -33
- package/dist/platform/process-boundary-trace.cjs +108 -0
- package/dist/platform/process-boundary-trace.mjs +102 -0
- package/dist/runtime/pi-artifact-provenance.cjs +147 -0
- package/dist/runtime/pi-artifact-provenance.mjs +139 -0
- package/dist/runtime/pi-compaction-recovery.cjs +703 -0
- package/dist/runtime/pi-compaction-recovery.mjs +679 -0
- package/dist/runtime/pi-model-catalog.cjs +2 -2
- package/dist/runtime/pi-model-catalog.mjs +2 -2
- package/dist/runtime/pi-profile-migration.cjs +556 -0
- package/dist/runtime/pi-profile-migration.mjs +515 -0
- package/dist/runtime/pi-provider-config.cjs +3 -3
- package/dist/runtime/pi-provider-config.mjs +3 -3
- package/dist/runtime/pi-rpc-client.cjs +39 -22
- package/dist/runtime/pi-rpc-client.mjs +39 -22
- package/dist/runtime/provider-error-classifier.cjs +39 -0
- package/dist/runtime/provider-error-classifier.mjs +34 -0
- package/dist/runtime/runtime-adapters.mjs +176 -27
- package/dist/runtime/runtime-host.cjs +630 -19
- package/dist/runtime/runtime-host.mjs +628 -18
- package/dist/runtime/runtime-readiness.cjs +6 -4
- package/dist/runtime/runtime-readiness.mjs +6 -4
- 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
|
-
|
|
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
|
|
1376
|
-
|
|
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:
|
|
1404
|
-
lastSeenAt: null, lastTurnAt: null, turns: 0 },
|
|
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
|
-
|
|
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
|
|
1336
|
-
|
|
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:
|
|
1364
|
-
lastSeenAt: null, lastTurnAt: null, turns: 0 },
|
|
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),
|