larkin 0.3.3 → 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.
- package/README.md +72 -33
- package/assets/readme/larkin-hero.svg +111 -0
- package/dist/agent/agent-state-store.cjs +321 -22
- package/dist/agent/agent-state-store.mjs +320 -22
- package/dist/agent/agent-transport.cjs +6 -4
- package/dist/agent/agent-transport.mjs +6 -4
- package/dist/agent/host-reminder-orchestrator.cjs +9 -3
- package/dist/agent/host-reminder-orchestrator.mjs +9 -3
- package/dist/agent/inbox-projection.cjs +114 -23
- package/dist/agent/inbox-projection.mjs +112 -23
- package/dist/app/cli.mjs +3 -1
- package/dist/app/local-control.mjs +108 -2
- package/dist/app/runtime-process.mjs +4 -0
- package/dist/app/session-cli.mjs +55 -20
- package/dist/feishu/document-comment.cjs +2 -1
- package/dist/feishu/document-comment.mjs +2 -1
- package/dist/feishu/host-business-state.cjs +7 -2
- package/dist/feishu/host-business-state.mjs +7 -2
- package/dist/feishu/host-shell.cjs +138 -32
- package/dist/feishu/host-shell.mjs +138 -32
- package/dist/runtime/pi-compaction-recovery.cjs +623 -0
- package/dist/runtime/pi-compaction-recovery.mjs +602 -0
- package/dist/runtime/pi-provider-config.cjs +2 -2
- package/dist/runtime/pi-provider-config.mjs +2 -2
- 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 +82 -19
- package/dist/runtime/runtime-host.cjs +663 -12
- package/dist/runtime/runtime-host.mjs +661 -11
- package/package.json +1 -1
package/dist/app/session-cli.mjs
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { loadConfig } from "../platform/config.mjs";
|
|
2
|
-
import { requestSessionReset } from "./local-control.mjs";
|
|
3
|
-
function
|
|
4
|
-
if (
|
|
2
|
+
import { requestSessionRecovery, requestSessionReset } from "./local-control.mjs";
|
|
3
|
+
function sanitizeRecoveryReadiness(value) {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5
|
+
return undefined;
|
|
6
|
+
const candidate = value;
|
|
7
|
+
if (!["codex", "claude", "pi"].includes(String(candidate.runtime))
|
|
8
|
+
|| !["missing", "unauthenticated", "unavailable", "incompatible", "ready"].includes(String(candidate.state)))
|
|
9
|
+
return undefined;
|
|
10
|
+
return { runtime: candidate.runtime, state: candidate.state,
|
|
11
|
+
...(candidate.state === "ready" ? {} : { reason: `Runtime readiness is ${String(candidate.state)}.`, nextAction: "Inspect Runtime/provider configuration, then retry." }) };
|
|
12
|
+
}
|
|
13
|
+
function parseSessionArguments(args) {
|
|
14
|
+
if (args[0] !== "reset" && args[0] !== "recover")
|
|
5
15
|
throw new Error("unsupported session subcommand");
|
|
16
|
+
const operation = args[0];
|
|
6
17
|
const values = new Map();
|
|
7
18
|
let json = false;
|
|
8
19
|
for (let index = 1; index < args.length; index += 1) {
|
|
@@ -13,7 +24,7 @@ function parseResetArguments(args) {
|
|
|
13
24
|
json = true;
|
|
14
25
|
continue;
|
|
15
26
|
}
|
|
16
|
-
if (!["--agent", "--wait-ready"].includes(token)) {
|
|
27
|
+
if (!["--agent", "--wait-ready", "--reason"].includes(token)) {
|
|
17
28
|
throw new Error(token.startsWith("-") ? `unknown flag: ${token}` : `unexpected positional: ${token}`);
|
|
18
29
|
}
|
|
19
30
|
if (values.has(token))
|
|
@@ -32,7 +43,12 @@ function parseResetArguments(args) {
|
|
|
32
43
|
const waitSeconds = Number(values.get("--wait-ready") ?? "30");
|
|
33
44
|
if (!Number.isFinite(waitSeconds) || waitSeconds < 0 || waitSeconds > 300)
|
|
34
45
|
throw new Error("--wait-ready must be 0..300 seconds");
|
|
35
|
-
|
|
46
|
+
const reason = values.get("--reason");
|
|
47
|
+
if (operation === "recover" && reason !== "context-overflow")
|
|
48
|
+
throw new Error("--reason context-overflow is required for session recover");
|
|
49
|
+
if (operation === "reset" && reason !== undefined)
|
|
50
|
+
throw new Error("--reason is only valid for session recover");
|
|
51
|
+
return { operation, agentId, waitReadyMs: Math.round(waitSeconds * 1000), ...(reason ? { reason: reason } : {}) };
|
|
36
52
|
}
|
|
37
53
|
export async function runSessionCli(args, env = process.env, dependencies = {}) {
|
|
38
54
|
const io = dependencies.io ?? { stdout: (value) => process.stdout.write(value), stderr: (value) => process.stderr.write(value) };
|
|
@@ -42,29 +58,46 @@ export async function runSessionCli(args, env = process.env, dependencies = {})
|
|
|
42
58
|
};
|
|
43
59
|
let parsed;
|
|
44
60
|
try {
|
|
45
|
-
parsed =
|
|
61
|
+
parsed = parseSessionArguments(args);
|
|
46
62
|
}
|
|
47
63
|
catch (error) {
|
|
48
64
|
return fail("invalid_arguments", error instanceof Error ? error.message : String(error));
|
|
49
65
|
}
|
|
50
66
|
const agentId = parsed.agentId;
|
|
51
67
|
if (typeof env.LARKIN_AGENT_ID === "string" && env.LARKIN_AGENT_ID.trim()) {
|
|
52
|
-
return fail("user_authority_required",
|
|
68
|
+
return fail("user_authority_required", parsed.operation === "reset"
|
|
69
|
+
? "session reset is available only from a user terminal"
|
|
70
|
+
: "session recovery is available only from a user terminal", agentId);
|
|
53
71
|
}
|
|
54
72
|
try {
|
|
55
73
|
const loaded = loadConfig(env);
|
|
56
|
-
const result =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
const result = parsed.operation === "recover"
|
|
75
|
+
? await (dependencies.requestRecovery ?? requestSessionRecovery)({
|
|
76
|
+
larkinHome: loaded.config.larkinHome, agentId, reason: parsed.reason, waitReadyMs: parsed.waitReadyMs,
|
|
77
|
+
})
|
|
78
|
+
: await (dependencies.request ?? requestSessionReset)({
|
|
79
|
+
larkinHome: loaded.config.larkinHome, agentId, waitReadyMs: parsed.waitReadyMs,
|
|
80
|
+
});
|
|
81
|
+
const output = "recoveryCommitted" in result
|
|
82
|
+
? {
|
|
83
|
+
ok: result.ok, agent_id: result.agentId, recovery_committed: result.recoveryCommitted,
|
|
84
|
+
generation_changed: result.generationChanged, session_changed: result.sessionChanged, turns: result.turns,
|
|
85
|
+
runtime_ready: result.runtimeReady, channel_connected: result.channelConnected, reconnecting: result.reconnecting,
|
|
86
|
+
pending_count: result.pendingCount, rearmed_count: result.rearmedCount, replay_status: result.replayStatus,
|
|
87
|
+
remaining_pending_count: result.remainingPendingCount, ready_for_fresh_scenario: result.readyForFreshScenario,
|
|
88
|
+
inbound_observed: result.inboundObserved, ...(result.code ? { code: result.code } : {}),
|
|
89
|
+
...(result.error ? { error: "context-overflow recovery failed" } : {}),
|
|
90
|
+
...(sanitizeRecoveryReadiness(result.readiness) ? { readiness: sanitizeRecoveryReadiness(result.readiness) } : {}),
|
|
91
|
+
}
|
|
92
|
+
: {
|
|
93
|
+
ok: result.ok, agent_id: result.agentId,
|
|
94
|
+
reset_committed: result.resetCommitted, generation_changed: result.generationChanged,
|
|
95
|
+
session_changed: result.sessionChanged, turns: result.turns, runtime_ready: result.runtimeReady,
|
|
96
|
+
channel_connected: result.channelConnected, reconnecting: result.reconnecting,
|
|
97
|
+
pending_count: result.pendingCount, ready_for_fresh_scenario: result.readyForFreshScenario,
|
|
98
|
+
inbound_observed: result.inboundObserved, ...(result.code ? { code: result.code } : {}),
|
|
99
|
+
...(result.error ? { error: result.error } : {}), ...(result.readiness ? { readiness: result.readiness } : {}),
|
|
100
|
+
};
|
|
68
101
|
io.stdout(`${JSON.stringify(output, null, 2)}\n`);
|
|
69
102
|
return result.ok ? 0 : 1;
|
|
70
103
|
}
|
|
@@ -72,7 +105,9 @@ export async function runSessionCli(args, env = process.env, dependencies = {})
|
|
|
72
105
|
const message = error instanceof Error ? error.message : String(error);
|
|
73
106
|
const output = {
|
|
74
107
|
ok: false, agent_id: agentId,
|
|
75
|
-
code: /timeout/i.test(message) ? "control_timeout" : "control_unavailable",
|
|
108
|
+
code: /timeout/i.test(message) ? "control_timeout" : "control_unavailable",
|
|
109
|
+
error: parsed.operation === "recover"
|
|
110
|
+
? (/timeout/i.test(message) ? "session recovery control timed out" : "session recovery control unavailable") : message,
|
|
76
111
|
};
|
|
77
112
|
io.stdout(`${JSON.stringify(output, null, 2)}\n`);
|
|
78
113
|
return 1;
|
|
@@ -11,6 +11,7 @@ exports.resolveDocumentCommentContext = resolveDocumentCommentContext;
|
|
|
11
11
|
exports.projectDocumentCommentEnvelope = projectDocumentCommentEnvelope;
|
|
12
12
|
const node_crypto_1 = require("node:crypto");
|
|
13
13
|
exports.DOCUMENT_COMMENT_EVENT = "drive.notice.comment_add_v1";
|
|
14
|
+
const DOCUMENT_COMMENT_TARGET_PATTERN = /^document-comment:(doc|docx|sheet|file):([A-Za-z0-9_-]+):([A-Za-z0-9_-]+):(in-thread|top-level)$/;
|
|
14
15
|
function record(value) {
|
|
15
16
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
16
17
|
}
|
|
@@ -39,7 +40,7 @@ function documentCommentTarget(target, commentId, topLevel = false) {
|
|
|
39
40
|
return `document-comment:${target.fileType}:${target.fileToken}:${commentId}:${topLevel ? "top-level" : "in-thread"}`;
|
|
40
41
|
}
|
|
41
42
|
function parseDocumentCommentTarget(value) {
|
|
42
|
-
const match =
|
|
43
|
+
const match = DOCUMENT_COMMENT_TARGET_PATTERN.exec(value);
|
|
43
44
|
return match ? {
|
|
44
45
|
fileType: match[1], fileToken: match[2], commentId: match[3], topLevel: match[4] === "top-level",
|
|
45
46
|
} : null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
export const DOCUMENT_COMMENT_EVENT = "drive.notice.comment_add_v1";
|
|
3
|
+
const DOCUMENT_COMMENT_TARGET_PATTERN = /^document-comment:(doc|docx|sheet|file):([A-Za-z0-9_-]+):([A-Za-z0-9_-]+):(in-thread|top-level)$/;
|
|
3
4
|
function record(value) {
|
|
4
5
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
5
6
|
}
|
|
@@ -28,7 +29,7 @@ export function documentCommentTarget(target, commentId, topLevel = false) {
|
|
|
28
29
|
return `document-comment:${target.fileType}:${target.fileToken}:${commentId}:${topLevel ? "top-level" : "in-thread"}`;
|
|
29
30
|
}
|
|
30
31
|
export function parseDocumentCommentTarget(value) {
|
|
31
|
-
const match =
|
|
32
|
+
const match = DOCUMENT_COMMENT_TARGET_PATTERN.exec(value);
|
|
32
33
|
return match ? {
|
|
33
34
|
fileType: match[1], fileToken: match[2], commentId: match[3], topLevel: match[4] === "top-level",
|
|
34
35
|
} : null;
|
|
@@ -10,6 +10,7 @@ exports.shouldPreventiveReconnect = shouldPreventiveReconnect;
|
|
|
10
10
|
exports.countWakeEnvelopes = countWakeEnvelopes;
|
|
11
11
|
exports.projectActivityStatus = projectActivityStatus;
|
|
12
12
|
exports.projectSessionStatus = projectSessionStatus;
|
|
13
|
+
const inbox_projection_js_1 = require("../agent/inbox-projection.cjs");
|
|
13
14
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
14
15
|
const message_policy_js_1 = require("./message-policy.cjs");
|
|
15
16
|
function asRecord(value) {
|
|
@@ -337,7 +338,8 @@ class HostEnvelopeProjector {
|
|
|
337
338
|
: `本条存量提醒缺少可用的飞书 message_id/chat_id,无法安全推断原会话;请先用 ${this.larkCommand("im +chat-search")} 确认目标,禁止猜测发送`,
|
|
338
339
|
`这是你之前用 ${this.agentCommand("reminder schedule")} 设置的提醒,请按标题执行相应动作。管理: ${this.agentCommand("reminder list")} / ${this.agentCommand("reminder snooze")} / ${this.agentCommand("reminder cancel")}`,
|
|
339
340
|
].filter((line) => Boolean(line));
|
|
340
|
-
|
|
341
|
+
const envelope = {
|
|
342
|
+
kind: "reminder",
|
|
341
343
|
message_id: `rem_${reminder.reminderId.slice(0, 16)}_${seq}`,
|
|
342
344
|
seq,
|
|
343
345
|
sender_name: "定时提醒",
|
|
@@ -349,10 +351,12 @@ class HostEnvelopeProjector {
|
|
|
349
351
|
thread_id: null,
|
|
350
352
|
wake: true,
|
|
351
353
|
};
|
|
354
|
+
return { ...envelope, target: inbox_projection_js_1.RUNTIME_REMINDER_TARGET };
|
|
352
355
|
}
|
|
353
356
|
createRedeliveryEnvelope(agentId, wakeCount) {
|
|
354
357
|
const seq = this.nextSequence(agentId);
|
|
355
|
-
|
|
358
|
+
const envelope = {
|
|
359
|
+
kind: "redelivery",
|
|
356
360
|
message_id: `redeliver_${this.randomHex(6)}`,
|
|
357
361
|
seq,
|
|
358
362
|
sender_name: "系统",
|
|
@@ -363,6 +367,7 @@ class HostEnvelopeProjector {
|
|
|
363
367
|
timestamp: this.now().toISOString(),
|
|
364
368
|
thread_id: null,
|
|
365
369
|
};
|
|
370
|
+
return { ...envelope, target: inbox_projection_js_1.RUNTIME_REDELIVERY_TARGET };
|
|
366
371
|
}
|
|
367
372
|
}
|
|
368
373
|
exports.HostEnvelopeProjector = HostEnvelopeProjector;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RUNTIME_REDELIVERY_TARGET, RUNTIME_REMINDER_TARGET } from "../agent/inbox-projection.mjs";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import { createMessageEnvelope, targetAliases, targetFor, } from "./message-policy.mjs";
|
|
3
4
|
function asRecord(value) {
|
|
@@ -323,7 +324,8 @@ export class HostEnvelopeProjector {
|
|
|
323
324
|
: `本条存量提醒缺少可用的飞书 message_id/chat_id,无法安全推断原会话;请先用 ${this.larkCommand("im +chat-search")} 确认目标,禁止猜测发送`,
|
|
324
325
|
`这是你之前用 ${this.agentCommand("reminder schedule")} 设置的提醒,请按标题执行相应动作。管理: ${this.agentCommand("reminder list")} / ${this.agentCommand("reminder snooze")} / ${this.agentCommand("reminder cancel")}`,
|
|
325
326
|
].filter((line) => Boolean(line));
|
|
326
|
-
|
|
327
|
+
const envelope = {
|
|
328
|
+
kind: "reminder",
|
|
327
329
|
message_id: `rem_${reminder.reminderId.slice(0, 16)}_${seq}`,
|
|
328
330
|
seq,
|
|
329
331
|
sender_name: "定时提醒",
|
|
@@ -335,10 +337,12 @@ export class HostEnvelopeProjector {
|
|
|
335
337
|
thread_id: null,
|
|
336
338
|
wake: true,
|
|
337
339
|
};
|
|
340
|
+
return { ...envelope, target: RUNTIME_REMINDER_TARGET };
|
|
338
341
|
}
|
|
339
342
|
createRedeliveryEnvelope(agentId, wakeCount) {
|
|
340
343
|
const seq = this.nextSequence(agentId);
|
|
341
|
-
|
|
344
|
+
const envelope = {
|
|
345
|
+
kind: "redelivery",
|
|
342
346
|
message_id: `redeliver_${this.randomHex(6)}`,
|
|
343
347
|
seq,
|
|
344
348
|
sender_name: "系统",
|
|
@@ -349,6 +353,7 @@ export class HostEnvelopeProjector {
|
|
|
349
353
|
timestamp: this.now().toISOString(),
|
|
350
354
|
thread_id: null,
|
|
351
355
|
};
|
|
356
|
+
return { ...envelope, target: RUNTIME_REDELIVERY_TARGET };
|
|
352
357
|
}
|
|
353
358
|
}
|
|
354
359
|
export function countWakeEnvelopes(lines) {
|
|
@@ -200,6 +200,16 @@ 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 recordInboundDeliveryFailure = (agent, code) => {
|
|
204
|
+
const at = new Date().toISOString();
|
|
205
|
+
const reason = "Inbound Runtime delivery failed non-retryably; durable Inbox/ledger state is retained for recovery.";
|
|
206
|
+
const nextAction = "Inspect the delivery/status error, correct the Runtime or canonical Inbox state, then restart to replay safely.";
|
|
207
|
+
hostState.updateStatus(agent, {
|
|
208
|
+
inboundDeliveryHealth: { state: "error", code, at, reason, nextAction },
|
|
209
|
+
runtimeReadiness: { runtime: agent.runtime, state: "incompatible", reason, nextAction },
|
|
210
|
+
});
|
|
211
|
+
hostState.recordStatusError(agent, `${reason} ${nextAction} code=${code}`);
|
|
212
|
+
};
|
|
203
213
|
const agentStates = new Map();
|
|
204
214
|
const saveAgentState = (record) => {
|
|
205
215
|
try {
|
|
@@ -335,6 +345,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
335
345
|
return;
|
|
336
346
|
}
|
|
337
347
|
const telemetryMessageId = String(event.message_id || event.event_id || eventKey);
|
|
348
|
+
let canonicalInboxDurable = false;
|
|
338
349
|
if (wake)
|
|
339
350
|
telemetry?.beginMessage(agent.agentId, telemetryMessageId);
|
|
340
351
|
if (event.event_id)
|
|
@@ -345,58 +356,76 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
345
356
|
senderIdentity.ensureChatNames(agent, event.chat_id, 3_000),
|
|
346
357
|
event._sender_is_bot ? Promise.resolve(null) : senderIdentity.ensureSenderSignature(agent, event.sender_id, 3_000),
|
|
347
358
|
]);
|
|
348
|
-
const
|
|
349
|
-
|
|
359
|
+
const projected = envelopeProjector.projectInbound(agent, event, { anchorReply: wake, names, signature });
|
|
360
|
+
projected.target = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)({ ...projected, chat_id: event.chat_id, thread_id: event.thread_id });
|
|
350
361
|
if (wake)
|
|
351
|
-
|
|
352
|
-
const
|
|
362
|
+
projected.wake = true;
|
|
363
|
+
const candidate = (0, inbox_projection_js_1.projectInboxEnvelope)(projected, {
|
|
353
364
|
chat_id: event.chat_id,
|
|
354
365
|
thread_id: event.thread_id,
|
|
355
366
|
...(event.create_time !== undefined ? { create_time: String(event.create_time) } : {}),
|
|
356
367
|
sender_id: event.sender_id,
|
|
357
|
-
content: String(
|
|
368
|
+
content: String(projected.content ?? event.content ?? ""),
|
|
358
369
|
});
|
|
370
|
+
let append;
|
|
359
371
|
try {
|
|
360
|
-
stateStore(agent).
|
|
372
|
+
append = stateStore(agent).appendCanonicalInboxOnce(candidate);
|
|
361
373
|
}
|
|
362
374
|
catch (error) {
|
|
363
375
|
throw new Error(`inbox 写失败: ${errorMessage(error)}`);
|
|
364
376
|
}
|
|
365
|
-
|
|
366
|
-
//
|
|
377
|
+
canonicalInboxDurable = true;
|
|
378
|
+
// An event becomes permanently transport-seen only after the canonical
|
|
379
|
+
// append/dedupe decision is durable. Agent model-seen state is untouched.
|
|
367
380
|
if (event.event_id)
|
|
368
381
|
seenEventIds.add(eventKey);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
382
|
+
if (append.status === "duplicate_consumed")
|
|
383
|
+
return null;
|
|
384
|
+
const inboxEnvelope = append.envelope;
|
|
385
|
+
if (append.status === "appended")
|
|
386
|
+
hostState.appendConversation(agent, {
|
|
387
|
+
direction: "in", from: inboxEnvelope.sender_name, senderType: inboxEnvelope.sender_type,
|
|
388
|
+
target: String(inboxEnvelope.target || (0, message_policy_js_1.targetFor)(event).target), wake, text: event.content, messageId: inboxEnvelope.message_id,
|
|
389
|
+
at: inboxEnvelope.timestamp || new Date().toISOString(),
|
|
390
|
+
});
|
|
391
|
+
return inboxEnvelope;
|
|
375
392
|
};
|
|
376
|
-
const
|
|
393
|
+
const inboxEnvelope = wake && telemetry
|
|
377
394
|
? await telemetry.phase(telemetryMessageId, "feishu.receive", api_1.SpanKind.CONSUMER, receive)
|
|
378
395
|
: await receive();
|
|
379
|
-
if (!wake)
|
|
396
|
+
if (!wake || !inboxEnvelope)
|
|
380
397
|
return;
|
|
381
|
-
const receipt = await runtimeHost.deliver(agent.agentId,
|
|
382
|
-
if (receipt.status === "
|
|
398
|
+
const receipt = await runtimeHost.deliver(agent.agentId, inboxEnvelope);
|
|
399
|
+
if (receipt.status === "error") {
|
|
400
|
+
recordInboundDeliveryFailure(agent, "non_retryable_receipt");
|
|
383
401
|
hostState.appendStatusLog(agent, "deliverLog", {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}, 30);
|
|
389
|
-
if (envelope.sender_type === "human" || envelope.sender_type === "agent")
|
|
390
|
-
processingEyes.add(agent, String(envelope.message_id || ""));
|
|
391
|
-
if (receipt.status === "deferred")
|
|
392
|
-
log(`Runtime 暂缓投递,消息保留在 inbox seq=${envelope.seq}: ${receipt.reason}`);
|
|
402
|
+
at: new Date().toISOString(), status: "error",
|
|
403
|
+
reason: "non-retryable Runtime delivery error; durable recovery state retained",
|
|
404
|
+
}, 80);
|
|
405
|
+
return;
|
|
393
406
|
}
|
|
407
|
+
hostState.appendStatusLog(agent, "deliverLog", {
|
|
408
|
+
from: inboxEnvelope.sender_name,
|
|
409
|
+
target: String(inboxEnvelope.target || (0, message_policy_js_1.targetFor)(event).target),
|
|
410
|
+
excerpt: (0, host_business_state_js_1.safeConversationExcerpt)(event.content, 180),
|
|
411
|
+
at: new Date().toISOString(),
|
|
412
|
+
}, 30);
|
|
413
|
+
if (inboxEnvelope.sender_type === "human" || inboxEnvelope.sender_type === "agent")
|
|
414
|
+
processingEyes.add(agent, String(inboxEnvelope.message_id || ""));
|
|
415
|
+
if (receipt.status === "deferred")
|
|
416
|
+
log(`Runtime 暂缓投递,消息保留在 inbox seq=${inboxEnvelope.seq}: ${receipt.reason}`);
|
|
394
417
|
}
|
|
395
418
|
catch (error) {
|
|
396
419
|
if (wake)
|
|
397
420
|
telemetry?.delivery(agent.agentId, telemetryMessageId, "error");
|
|
398
|
-
|
|
399
|
-
|
|
421
|
+
if (canonicalInboxDurable) {
|
|
422
|
+
log(`onFeishuMessage Runtime delivery failed agent=${agent.name}; canonical Inbox retained`);
|
|
423
|
+
recordInboundDeliveryFailure(agent, "runtime_delivery_exception");
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
log(`onFeishuMessage 异常 agent=${agent.name}: ${error instanceof Error ? error.stack || error.message : String(error)}`);
|
|
427
|
+
hostState.recordStatusError(agent, `onFeishuMessage: ${errorMessage(error)}`);
|
|
428
|
+
}
|
|
400
429
|
}
|
|
401
430
|
finally {
|
|
402
431
|
if (event.event_id)
|
|
@@ -1241,16 +1270,24 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1241
1270
|
return;
|
|
1242
1271
|
}
|
|
1243
1272
|
if (message.type === "delivery") {
|
|
1273
|
+
if (message.status === "error")
|
|
1274
|
+
recordInboundDeliveryFailure(agent, "runtime_delivery_event");
|
|
1244
1275
|
hostState.appendStatusLog(agent, "deliverLog", {
|
|
1245
1276
|
at: new Date().toISOString(), deliveryId: message.deliveryId, messageId: message.messageId,
|
|
1246
|
-
status: message.status,
|
|
1277
|
+
status: message.status,
|
|
1278
|
+
...(message.status === "error"
|
|
1279
|
+
? { reason: "non-retryable Runtime delivery error; durable recovery state retained" }
|
|
1280
|
+
: (message.reason ? { reason: (0, host_business_state_js_1.safeConversationExcerpt)(message.reason, 120) } : {})),
|
|
1247
1281
|
}, 80);
|
|
1248
1282
|
return;
|
|
1249
1283
|
}
|
|
1250
1284
|
if (message.type === "session") {
|
|
1251
1285
|
const record = agentStates.get(agent.agentId);
|
|
1252
1286
|
if (record && record.state.sessions[message.runtime] !== message.sessionId) {
|
|
1253
|
-
|
|
1287
|
+
if (message.sessionId)
|
|
1288
|
+
record.state.sessions[message.runtime] = message.sessionId;
|
|
1289
|
+
else
|
|
1290
|
+
delete record.state.sessions[message.runtime];
|
|
1254
1291
|
saveAgentState(record);
|
|
1255
1292
|
log(`持久化 agent=${agent.name} runtime=${message.runtime} sessionId=${message.sessionId}`);
|
|
1256
1293
|
}
|
|
@@ -1266,7 +1303,8 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1266
1303
|
hostState.recordStatusError(agent, `auth: ${readiness.reason}; ${readiness.nextAction}`);
|
|
1267
1304
|
}
|
|
1268
1305
|
else {
|
|
1269
|
-
|
|
1306
|
+
const category = message.event.errorCategory || "provider";
|
|
1307
|
+
hostState.recordStatusError(agent, `${category}: Runtime input failed ${message.event.retryable ? "retryably" : "non-retryably"}; inspect delivery health and Runtime/provider configuration`);
|
|
1270
1308
|
}
|
|
1271
1309
|
}
|
|
1272
1310
|
};
|
|
@@ -1397,6 +1435,74 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1397
1435
|
: unavailable ? "reset committed but Runtime/channel readiness is unavailable"
|
|
1398
1436
|
: "reset committed but readiness did not converge before timeout" };
|
|
1399
1437
|
},
|
|
1438
|
+
async recoverSession(agentId, reason, waitReadyMs = 30_000) {
|
|
1439
|
+
const agent = agents.find((candidate) => candidate.agentId === agentId);
|
|
1440
|
+
if (!agent)
|
|
1441
|
+
throw Object.assign(new Error(`未知 Agent: ${agentId}`), { code: "unknown_agent" });
|
|
1442
|
+
if (reason !== "context-overflow")
|
|
1443
|
+
throw Object.assign(new Error("unsupported recovery reason"), { code: "recovery_refused" });
|
|
1444
|
+
if (!runtimeHost.recoverSession)
|
|
1445
|
+
throw Object.assign(new Error("Runtime session recovery 尚未就绪"), { code: "recovery_unavailable" });
|
|
1446
|
+
const connectionState = (status = hostState.readStatus(agent)) => {
|
|
1447
|
+
const connectedAt = Date.parse(String(status.connectedAt || ""));
|
|
1448
|
+
const channelConnected = Number.isFinite(connectedAt) && connectedAt >= Date.parse(daemonStartedAt) - 1000;
|
|
1449
|
+
return { channelConnected, reconnecting: (0, agent_readiness_js_1.isChannelReconnecting)(status) };
|
|
1450
|
+
};
|
|
1451
|
+
const readinessProjection = () => {
|
|
1452
|
+
const status = hostState.readStatus(agent);
|
|
1453
|
+
const turns = status.session && typeof status.session === "object"
|
|
1454
|
+
? Math.max(0, Number(status.session.turns) || 0) : 0;
|
|
1455
|
+
const runtimeReady = Boolean(status.runtimeReadiness && typeof status.runtimeReadiness === "object"
|
|
1456
|
+
&& status.runtimeReadiness.state === "ready");
|
|
1457
|
+
const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
|
|
1458
|
+
return { turns, runtimeReady, ...connectionState(status), pendingCount };
|
|
1459
|
+
};
|
|
1460
|
+
const initialProjection = readinessProjection();
|
|
1461
|
+
if (!initialProjection.channelConnected)
|
|
1462
|
+
throw Object.assign(new Error(`Agent ${agentId} channel is not connected`), { code: "channel_unavailable", ...initialProjection });
|
|
1463
|
+
if (initialProjection.reconnecting)
|
|
1464
|
+
throw Object.assign(new Error(`Agent ${agentId} channel is reconnecting`), { code: "channel_reconnecting", ...initialProjection });
|
|
1465
|
+
let recovery;
|
|
1466
|
+
try {
|
|
1467
|
+
recovery = await runtimeHost.recoverSession(agentId, reason);
|
|
1468
|
+
}
|
|
1469
|
+
catch (error) {
|
|
1470
|
+
throw Object.assign(error instanceof Error ? error : new Error(String(error)), readinessProjection());
|
|
1471
|
+
}
|
|
1472
|
+
const record = agentStates.get(agentId);
|
|
1473
|
+
try {
|
|
1474
|
+
if (record) {
|
|
1475
|
+
if (recovery.sessionId)
|
|
1476
|
+
record.state.sessions[agent.runtime] = recovery.sessionId;
|
|
1477
|
+
else
|
|
1478
|
+
delete record.state.sessions[agent.runtime];
|
|
1479
|
+
record.store.writeJson("agentState", record.state);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
catch (error) {
|
|
1483
|
+
const projection = readinessProjection();
|
|
1484
|
+
return { recoveryCommitted: true, generationChanged: recovery.generationChanged, sessionChanged: recovery.sessionChanged,
|
|
1485
|
+
...projection, rearmedCount: recovery.rearmedCount, replayStatus: "pending", remainingPendingCount: projection.pendingCount,
|
|
1486
|
+
readyForFreshScenario: false, inboundObserved: false, code: "state_persistence_failed",
|
|
1487
|
+
error: "recovery committed but Agent session state persistence failed" };
|
|
1488
|
+
}
|
|
1489
|
+
const deadline = Date.now() + Math.max(0, waitReadyMs);
|
|
1490
|
+
let projection = readinessProjection();
|
|
1491
|
+
do {
|
|
1492
|
+
projection = readinessProjection();
|
|
1493
|
+
if (projection.turns === 0 && projection.runtimeReady && projection.channelConnected && !projection.reconnecting)
|
|
1494
|
+
break;
|
|
1495
|
+
if (Date.now() >= deadline)
|
|
1496
|
+
break;
|
|
1497
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(25, Math.max(1, deadline - Date.now()))));
|
|
1498
|
+
} while (true);
|
|
1499
|
+
const replayStatus = projection.pendingCount === 0 ? "consumed" : "pending";
|
|
1500
|
+
const ready = projection.turns === 0 && projection.runtimeReady && projection.channelConnected && !projection.reconnecting;
|
|
1501
|
+
return { recoveryCommitted: true, generationChanged: recovery.generationChanged, sessionChanged: recovery.sessionChanged,
|
|
1502
|
+
...projection, rearmedCount: recovery.rearmedCount, replayStatus, remainingPendingCount: projection.pendingCount,
|
|
1503
|
+
readyForFreshScenario: ready, inboundObserved: false,
|
|
1504
|
+
...(!ready ? { code: "recovery_timeout", error: "recovery committed but Runtime/channel readiness did not converge before timeout" } : {}) };
|
|
1505
|
+
},
|
|
1400
1506
|
async start() {
|
|
1401
1507
|
try {
|
|
1402
1508
|
node_fs_1.default.mkdirSync(larkinHome, { recursive: true });
|