larkin 0.4.13 → 0.4.14
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/dist/agent/agent-state-store.cjs +237 -0
- package/dist/agent/agent-state-store.mjs +238 -1
- package/dist/agent/context-prompt.cjs +2 -1
- package/dist/agent/context-prompt.mjs +2 -1
- package/dist/agent/host-reminder-orchestrator.cjs +134 -7
- package/dist/agent/host-reminder-orchestrator.mjs +134 -7
- package/dist/agent/inbox-projection.cjs +5 -0
- package/dist/agent/inbox-projection.mjs +4 -0
- package/dist/agent/reminder-delivery-audit.cjs +86 -0
- package/dist/agent/reminder-delivery-audit.mjs +50 -0
- package/dist/agent/reminder-routes.cjs +83 -3
- package/dist/agent/reminder-routes.mjs +83 -3
- package/dist/agent/reminder-store.cjs +5 -2
- package/dist/agent/reminder-store.mjs +5 -2
- package/dist/agent/transport-business-context.cjs +26 -2
- package/dist/agent/transport-business-context.mjs +26 -2
- package/dist/app/agent-cli.mjs +33 -2
- package/dist/app/lark-cli.mjs +79 -11
- package/dist/feishu/host-business-state.cjs +35 -14
- package/dist/feishu/host-business-state.mjs +35 -14
- package/dist/feishu/interaction-orchestrator.cjs +8 -1
- package/dist/feishu/interaction-orchestrator.mjs +8 -1
- package/dist/feishu/outbound-transport.cjs +37 -3
- package/dist/feishu/outbound-transport.mjs +37 -3
- package/dist/runtime/runtime-host.cjs +37 -0
- package/dist/runtime/runtime-host.mjs +37 -0
- package/package.json +1 -1
|
@@ -302,31 +302,50 @@ export class HostEnvelopeProjector {
|
|
|
302
302
|
}
|
|
303
303
|
createReminderEnvelope(agentId, reminder, overdueMs, repeatDescription) {
|
|
304
304
|
const seq = this.nextSequence(agentId);
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
305
|
+
const legacyChannel = typeof reminder.channel === "string" ? reminder.channel.trim() : "";
|
|
306
|
+
const legacyChatTarget = /^oc_[A-Za-z0-9_-]+$/.test(legacyChannel) ? `chat:${legacyChannel}` : null;
|
|
307
|
+
const deliveryTarget = typeof reminder.deliveryTarget === "string" && reminder.deliveryTarget
|
|
308
|
+
? reminder.deliveryTarget : legacyChatTarget;
|
|
309
|
+
const anchorMessageId = typeof reminder.deliveryAnchor === "string" && /^om_[A-Za-z0-9_-]+$/.test(reminder.deliveryAnchor)
|
|
310
|
+
? reminder.deliveryAnchor
|
|
311
|
+
: typeof reminder.msgRef === "string" && /^om_[A-Za-z0-9_-]+$/.test(reminder.msgRef)
|
|
312
|
+
? reminder.msgRef
|
|
313
|
+
: null;
|
|
314
|
+
const commentAnchorId = deliveryTarget?.startsWith("document-comment:") && typeof reminder.deliveryAnchor === "string"
|
|
315
|
+
&& /^doc_comment_[A-Za-z0-9_-]+$/.test(reminder.deliveryAnchor) ? reminder.deliveryAnchor : null;
|
|
316
|
+
const anchorId = anchorMessageId || commentAnchorId;
|
|
317
|
+
const chatId = deliveryTarget?.match(/^chat:(oc_[A-Za-z0-9_-]+)$/)?.[1] || null;
|
|
308
318
|
const lines = [
|
|
309
319
|
`[定时提醒触发] ${reminder.title}`,
|
|
310
320
|
`提醒ID: #${reminder.reminderId.slice(0, 8)}` + (reminder.repeat && repeatDescription
|
|
311
321
|
? ` 重复: ${repeatDescription}(下次已自动排在 ${reminder.fireAt})`
|
|
312
322
|
: " 类型: 一次性"),
|
|
313
323
|
overdueMs > 120_000 ? `注意: 原定时间已过 ${Math.round(overdueMs / 60_000)} 分钟(Runtime Host 离线期间错过,现补触发)` : null,
|
|
314
|
-
|
|
324
|
+
deliveryTarget ? `原始 deliveryTarget: ${deliveryTarget}`
|
|
325
|
+
: anchorId ? "这是升级前存量 user-facing reminder,优先回复其安全锚点;不得向标题中的任何人或第三方发送消息"
|
|
326
|
+
: "本条为 internal/no-delivery reminder,不得向标题中的任何人或第三方发送消息",
|
|
327
|
+
anchorId ? `锚定消息: ${anchorId}` : reminder.msgRef ? `历史锚点 ${String(reminder.msgRef)} 不是可用的 delivery anchor,不能用于回复` : null,
|
|
315
328
|
anchorMessageId
|
|
316
|
-
?
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
:
|
|
321
|
-
?
|
|
322
|
-
:
|
|
323
|
-
?
|
|
324
|
-
|
|
329
|
+
? [
|
|
330
|
+
`回复原会话: ${this.larkCommand(`im +messages-reply --message-id ${anchorMessageId}${deliveryTarget?.startsWith("thread:") ? " --reply-in-thread" : ""} ...`)}`,
|
|
331
|
+
chatId ? `若 ${anchorMessageId} 无法由当前 Inbox 解析(例如 interaction_* 卡片锚点或已淘汰的历史消息),改用聊天兜底发送: ${this.larkCommand(`im +messages-send --chat-id ${chatId} ...`)};仅使用这个持久化 chat_id,不得猜测收件人` : null,
|
|
332
|
+
].filter((line) => Boolean(line)).join("\n")
|
|
333
|
+
: commentAnchorId
|
|
334
|
+
? `回复原文档评论: ${this.agentCommand(`comment reply --message-id ${commentAnchorId} --text ...`)}`
|
|
335
|
+
: deliveryTarget
|
|
336
|
+
? chatId
|
|
337
|
+
? `发送到原始 target: ${deliveryTarget}: ${this.larkCommand(`im +messages-send --chat-id ${chatId} ...`)}(不得从提醒标题推断收件人)`
|
|
338
|
+
: `发送到原始 target: ${deliveryTarget}(不得从提醒标题推断收件人)`
|
|
339
|
+
: null,
|
|
325
340
|
`这是你之前用 ${this.agentCommand("reminder schedule")} 设置的提醒,请按标题执行相应动作。管理: ${this.agentCommand("reminder list")} / ${this.agentCommand("reminder snooze")} / ${this.agentCommand("reminder cancel")}`,
|
|
326
341
|
].filter((line) => Boolean(line));
|
|
327
342
|
const envelope = {
|
|
328
343
|
kind: "reminder",
|
|
329
|
-
|
|
344
|
+
// The random suffix keeps each firing's occurrence identity unique across
|
|
345
|
+
// Host restarts: the in-memory seq restarts at 1 and would otherwise let a
|
|
346
|
+
// recurring reminder reuse a prior occurrenceId in the delivery audit.
|
|
347
|
+
message_id: `rem_${reminder.reminderId.slice(0, 16)}_${seq}_${this.randomHex(6)}`,
|
|
348
|
+
reminderId: reminder.reminderId,
|
|
330
349
|
seq,
|
|
331
350
|
sender_name: "定时提醒",
|
|
332
351
|
sender_type: "system",
|
|
@@ -336,6 +355,8 @@ export class HostEnvelopeProjector {
|
|
|
336
355
|
timestamp: this.now().toISOString(),
|
|
337
356
|
thread_id: null,
|
|
338
357
|
wake: true,
|
|
358
|
+
deliveryTarget,
|
|
359
|
+
deliveryAnchor: anchorId,
|
|
339
360
|
};
|
|
340
361
|
return { ...envelope, target: RUNTIME_REMINDER_TARGET };
|
|
341
362
|
}
|
|
@@ -93,6 +93,8 @@ class HostInteractionOrchestrator {
|
|
|
93
93
|
const reminderId = `int_${(0, node_crypto_1.createHash)("sha256").update(run.run_id).digest("hex").slice(0, 24)}`;
|
|
94
94
|
const current = this.now();
|
|
95
95
|
const fireAt = effect.args.fire_at ? Date.parse(effect.args.fire_at) : current + Number(effect.args.delay_seconds) * 1_000;
|
|
96
|
+
const deliveryTarget = typeof run.chat_id === "string" && run.chat_id ? `chat:${run.chat_id}` : null;
|
|
97
|
+
const deliveryAnchor = typeof run.message_id === "string" && /^om_[A-Za-z0-9_-]+$/.test(run.message_id) ? run.message_id : null;
|
|
96
98
|
const reminder = ReminderStore.mutate(file, (store) => {
|
|
97
99
|
const existing = store.reminders.find((candidate) => candidate.reminderId === reminderId);
|
|
98
100
|
if (existing)
|
|
@@ -107,6 +109,9 @@ class HostInteractionOrchestrator {
|
|
|
107
109
|
status: "scheduled",
|
|
108
110
|
msgRef: run.message_id,
|
|
109
111
|
msgPermalink: null,
|
|
112
|
+
deliveryTarget,
|
|
113
|
+
deliveryAnchor,
|
|
114
|
+
deliveryMode: "user",
|
|
110
115
|
repeat: null,
|
|
111
116
|
tz: null,
|
|
112
117
|
channel: run.chat_id,
|
|
@@ -114,7 +119,9 @@ class HostInteractionOrchestrator {
|
|
|
114
119
|
version: 1,
|
|
115
120
|
events: [],
|
|
116
121
|
};
|
|
117
|
-
ReminderStore.appendEvent(created, "scheduled", "interaction", run.run_id, created.fireAt, current
|
|
122
|
+
ReminderStore.appendEvent(created, "scheduled", "interaction", run.run_id, created.fireAt, current, {
|
|
123
|
+
deliveryTarget, deliveryAnchor, deliveryMode: "user",
|
|
124
|
+
});
|
|
118
125
|
store.reminders.push(created);
|
|
119
126
|
return created;
|
|
120
127
|
}, Math.max(1, Math.min(350, deadline - this.now() - 100)));
|
|
@@ -57,6 +57,8 @@ export class HostInteractionOrchestrator {
|
|
|
57
57
|
const reminderId = `int_${createHash("sha256").update(run.run_id).digest("hex").slice(0, 24)}`;
|
|
58
58
|
const current = this.now();
|
|
59
59
|
const fireAt = effect.args.fire_at ? Date.parse(effect.args.fire_at) : current + Number(effect.args.delay_seconds) * 1_000;
|
|
60
|
+
const deliveryTarget = typeof run.chat_id === "string" && run.chat_id ? `chat:${run.chat_id}` : null;
|
|
61
|
+
const deliveryAnchor = typeof run.message_id === "string" && /^om_[A-Za-z0-9_-]+$/.test(run.message_id) ? run.message_id : null;
|
|
60
62
|
const reminder = ReminderStore.mutate(file, (store) => {
|
|
61
63
|
const existing = store.reminders.find((candidate) => candidate.reminderId === reminderId);
|
|
62
64
|
if (existing)
|
|
@@ -71,6 +73,9 @@ export class HostInteractionOrchestrator {
|
|
|
71
73
|
status: "scheduled",
|
|
72
74
|
msgRef: run.message_id,
|
|
73
75
|
msgPermalink: null,
|
|
76
|
+
deliveryTarget,
|
|
77
|
+
deliveryAnchor,
|
|
78
|
+
deliveryMode: "user",
|
|
74
79
|
repeat: null,
|
|
75
80
|
tz: null,
|
|
76
81
|
channel: run.chat_id,
|
|
@@ -78,7 +83,9 @@ export class HostInteractionOrchestrator {
|
|
|
78
83
|
version: 1,
|
|
79
84
|
events: [],
|
|
80
85
|
};
|
|
81
|
-
ReminderStore.appendEvent(created, "scheduled", "interaction", run.run_id, created.fireAt, current
|
|
86
|
+
ReminderStore.appendEvent(created, "scheduled", "interaction", run.run_id, created.fireAt, current, {
|
|
87
|
+
deliveryTarget, deliveryAnchor, deliveryMode: "user",
|
|
88
|
+
});
|
|
82
89
|
store.reminders.push(created);
|
|
83
90
|
return created;
|
|
84
91
|
}, Math.max(1, Math.min(350, deadline - this.now() - 100)));
|
|
@@ -155,16 +155,30 @@ function createOutboundTransport(options) {
|
|
|
155
155
|
};
|
|
156
156
|
const handleSend = (body) => {
|
|
157
157
|
const target = body.target || "";
|
|
158
|
+
const targetKey = String(target);
|
|
159
|
+
const report = (succeeded, details = {}) => {
|
|
160
|
+
if (options.dryRun)
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
options.onDeliveryOutcome?.({ target: targetKey, succeeded, ...details });
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
options.log("delivery outcome hook 失败:", error.message);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
158
169
|
const content = String(body.content ?? "").replace(/\n+$/, "");
|
|
159
170
|
const chatId = options.resolveChatId(target);
|
|
160
171
|
if (!chatId) {
|
|
161
|
-
|
|
172
|
+
const error = `该 target 没有已知 chat_id:${JSON.stringify(target)}。只能回复 canonical Inbox 给出的确切会话;不会兜底发往默认群。`;
|
|
173
|
+
report(false, { reason: error });
|
|
174
|
+
return { ok: false, status: 400, error };
|
|
162
175
|
}
|
|
163
176
|
const idempotencyKey = body.idempotencyKey
|
|
164
177
|
|| node_crypto_1.default.createHash("sha256").update(`${String(target)}\0${content}`).digest("hex").slice(0, 32);
|
|
165
178
|
const replyContext = options.replyContextFor(String(target)) || options.replyContextFor(chatId);
|
|
166
179
|
const attachmentIds = Array.isArray(body.attachmentIds) ? body.attachmentIds.filter(Boolean) : [];
|
|
167
180
|
let messageId = "sent";
|
|
181
|
+
let committedProviderCalls = 0;
|
|
168
182
|
if (content !== "" || attachmentIds.length === 0) {
|
|
169
183
|
const replyInThread = Boolean(replyContext?.in_topic && replyContext.reply_to);
|
|
170
184
|
const result = replyInThread
|
|
@@ -172,7 +186,9 @@ function createOutboundTransport(options) {
|
|
|
172
186
|
: options.sendText(chatId, content, idempotencyKey);
|
|
173
187
|
if (result.code !== 0) {
|
|
174
188
|
options.log("send 失败 FULL:", `${result.stdout || ""} || ${result.stderr || ""}`);
|
|
175
|
-
|
|
189
|
+
const error = `lark-cli send failed: ${(result.stdout || result.stderr || "").trim().split("\n")[0]}`;
|
|
190
|
+
report(false, { reason: error });
|
|
191
|
+
return { ok: false, status: 502, error };
|
|
176
192
|
}
|
|
177
193
|
try {
|
|
178
194
|
const parsed = JSON.parse(result.stdout);
|
|
@@ -181,8 +197,10 @@ function createOutboundTransport(options) {
|
|
|
181
197
|
catch {
|
|
182
198
|
messageId = options.dryRun ? "dryrun" : "sent";
|
|
183
199
|
}
|
|
200
|
+
committedProviderCalls += 1;
|
|
184
201
|
options.log(`${replyInThread ? "reply-in-thread" : "send"} → chat=${chatId} len=${content.length} id=${messageId}${options.dryRun ? " (dry-run)" : ""}`);
|
|
185
202
|
}
|
|
203
|
+
const missingAttachmentIds = [];
|
|
186
204
|
if (attachmentIds.length) {
|
|
187
205
|
const index = loadAttachmentIndex();
|
|
188
206
|
for (let position = 0; position < attachmentIds.length; position += 1) {
|
|
@@ -190,21 +208,34 @@ function createOutboundTransport(options) {
|
|
|
190
208
|
const attachment = index[attachmentId];
|
|
191
209
|
if (!attachment) {
|
|
192
210
|
options.log(`attachment 未找到 id=${attachmentId}(跳过)`);
|
|
211
|
+
missingAttachmentIds.push(attachmentId);
|
|
193
212
|
continue;
|
|
194
213
|
}
|
|
195
214
|
const result = options.sendMedia(chatId, replyContext, attachment, `${idempotencyKey}:a${position}`);
|
|
196
215
|
if (result.code !== 0) {
|
|
197
216
|
options.log("attachment 发送失败 FULL:", `${result.stdout || ""} || ${result.stderr || ""}`);
|
|
198
|
-
|
|
217
|
+
const error = `lark-cli 发送附件失败: ${(result.stdout || result.stderr || "").trim().split("\n")[0]}`;
|
|
218
|
+
report(false, { reason: error });
|
|
219
|
+
return { ok: false, status: 502, error };
|
|
199
220
|
}
|
|
200
221
|
try {
|
|
201
222
|
const parsed = JSON.parse(result.stdout);
|
|
202
223
|
messageId = parsed?.data?.message_id || messageId;
|
|
203
224
|
}
|
|
204
225
|
catch { /* retain prior message id */ }
|
|
226
|
+
committedProviderCalls += 1;
|
|
205
227
|
options.log(`attach → chat=${chatId} name=${attachment.filename} id=${messageId}${options.dryRun ? " (dry-run)" : ""}`);
|
|
206
228
|
}
|
|
207
229
|
}
|
|
230
|
+
// Empty content plus attachment IDs that are all absent from the index
|
|
231
|
+
// reaches this point without any provider call; claiming success here would
|
|
232
|
+
// record delivery_succeeded for a reminder turn in which the user received
|
|
233
|
+
// nothing.
|
|
234
|
+
if (committedProviderCalls === 0) {
|
|
235
|
+
const error = `没有可发送的内容:附件不存在(${missingAttachmentIds.join(", ")}),且消息正文为空。`;
|
|
236
|
+
report(false, { reason: error });
|
|
237
|
+
return { ok: false, status: 400, error };
|
|
238
|
+
}
|
|
208
239
|
options.appendConversation({
|
|
209
240
|
direction: "out",
|
|
210
241
|
from: options.botDisplayName() || options.agentName,
|
|
@@ -215,6 +246,9 @@ function createOutboundTransport(options) {
|
|
|
215
246
|
messageId,
|
|
216
247
|
at: new Date().toISOString(),
|
|
217
248
|
});
|
|
249
|
+
// This is deliberately after every guarded provider call succeeds and after
|
|
250
|
+
// the local outbound projection, not at Runtime acceptance.
|
|
251
|
+
report(true, { messageId });
|
|
218
252
|
return { ok: true, status: 200, data: { ok: true, state: "sent", messageId, messageSeq: 1 } };
|
|
219
253
|
};
|
|
220
254
|
return { handleSend, stageUpload };
|
|
@@ -149,16 +149,30 @@ export function createOutboundTransport(options) {
|
|
|
149
149
|
};
|
|
150
150
|
const handleSend = (body) => {
|
|
151
151
|
const target = body.target || "";
|
|
152
|
+
const targetKey = String(target);
|
|
153
|
+
const report = (succeeded, details = {}) => {
|
|
154
|
+
if (options.dryRun)
|
|
155
|
+
return;
|
|
156
|
+
try {
|
|
157
|
+
options.onDeliveryOutcome?.({ target: targetKey, succeeded, ...details });
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
options.log("delivery outcome hook 失败:", error.message);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
152
163
|
const content = String(body.content ?? "").replace(/\n+$/, "");
|
|
153
164
|
const chatId = options.resolveChatId(target);
|
|
154
165
|
if (!chatId) {
|
|
155
|
-
|
|
166
|
+
const error = `该 target 没有已知 chat_id:${JSON.stringify(target)}。只能回复 canonical Inbox 给出的确切会话;不会兜底发往默认群。`;
|
|
167
|
+
report(false, { reason: error });
|
|
168
|
+
return { ok: false, status: 400, error };
|
|
156
169
|
}
|
|
157
170
|
const idempotencyKey = body.idempotencyKey
|
|
158
171
|
|| crypto.createHash("sha256").update(`${String(target)}\0${content}`).digest("hex").slice(0, 32);
|
|
159
172
|
const replyContext = options.replyContextFor(String(target)) || options.replyContextFor(chatId);
|
|
160
173
|
const attachmentIds = Array.isArray(body.attachmentIds) ? body.attachmentIds.filter(Boolean) : [];
|
|
161
174
|
let messageId = "sent";
|
|
175
|
+
let committedProviderCalls = 0;
|
|
162
176
|
if (content !== "" || attachmentIds.length === 0) {
|
|
163
177
|
const replyInThread = Boolean(replyContext?.in_topic && replyContext.reply_to);
|
|
164
178
|
const result = replyInThread
|
|
@@ -166,7 +180,9 @@ export function createOutboundTransport(options) {
|
|
|
166
180
|
: options.sendText(chatId, content, idempotencyKey);
|
|
167
181
|
if (result.code !== 0) {
|
|
168
182
|
options.log("send 失败 FULL:", `${result.stdout || ""} || ${result.stderr || ""}`);
|
|
169
|
-
|
|
183
|
+
const error = `lark-cli send failed: ${(result.stdout || result.stderr || "").trim().split("\n")[0]}`;
|
|
184
|
+
report(false, { reason: error });
|
|
185
|
+
return { ok: false, status: 502, error };
|
|
170
186
|
}
|
|
171
187
|
try {
|
|
172
188
|
const parsed = JSON.parse(result.stdout);
|
|
@@ -175,8 +191,10 @@ export function createOutboundTransport(options) {
|
|
|
175
191
|
catch {
|
|
176
192
|
messageId = options.dryRun ? "dryrun" : "sent";
|
|
177
193
|
}
|
|
194
|
+
committedProviderCalls += 1;
|
|
178
195
|
options.log(`${replyInThread ? "reply-in-thread" : "send"} → chat=${chatId} len=${content.length} id=${messageId}${options.dryRun ? " (dry-run)" : ""}`);
|
|
179
196
|
}
|
|
197
|
+
const missingAttachmentIds = [];
|
|
180
198
|
if (attachmentIds.length) {
|
|
181
199
|
const index = loadAttachmentIndex();
|
|
182
200
|
for (let position = 0; position < attachmentIds.length; position += 1) {
|
|
@@ -184,21 +202,34 @@ export function createOutboundTransport(options) {
|
|
|
184
202
|
const attachment = index[attachmentId];
|
|
185
203
|
if (!attachment) {
|
|
186
204
|
options.log(`attachment 未找到 id=${attachmentId}(跳过)`);
|
|
205
|
+
missingAttachmentIds.push(attachmentId);
|
|
187
206
|
continue;
|
|
188
207
|
}
|
|
189
208
|
const result = options.sendMedia(chatId, replyContext, attachment, `${idempotencyKey}:a${position}`);
|
|
190
209
|
if (result.code !== 0) {
|
|
191
210
|
options.log("attachment 发送失败 FULL:", `${result.stdout || ""} || ${result.stderr || ""}`);
|
|
192
|
-
|
|
211
|
+
const error = `lark-cli 发送附件失败: ${(result.stdout || result.stderr || "").trim().split("\n")[0]}`;
|
|
212
|
+
report(false, { reason: error });
|
|
213
|
+
return { ok: false, status: 502, error };
|
|
193
214
|
}
|
|
194
215
|
try {
|
|
195
216
|
const parsed = JSON.parse(result.stdout);
|
|
196
217
|
messageId = parsed?.data?.message_id || messageId;
|
|
197
218
|
}
|
|
198
219
|
catch { /* retain prior message id */ }
|
|
220
|
+
committedProviderCalls += 1;
|
|
199
221
|
options.log(`attach → chat=${chatId} name=${attachment.filename} id=${messageId}${options.dryRun ? " (dry-run)" : ""}`);
|
|
200
222
|
}
|
|
201
223
|
}
|
|
224
|
+
// Empty content plus attachment IDs that are all absent from the index
|
|
225
|
+
// reaches this point without any provider call; claiming success here would
|
|
226
|
+
// record delivery_succeeded for a reminder turn in which the user received
|
|
227
|
+
// nothing.
|
|
228
|
+
if (committedProviderCalls === 0) {
|
|
229
|
+
const error = `没有可发送的内容:附件不存在(${missingAttachmentIds.join(", ")}),且消息正文为空。`;
|
|
230
|
+
report(false, { reason: error });
|
|
231
|
+
return { ok: false, status: 400, error };
|
|
232
|
+
}
|
|
202
233
|
options.appendConversation({
|
|
203
234
|
direction: "out",
|
|
204
235
|
from: options.botDisplayName() || options.agentName,
|
|
@@ -209,6 +240,9 @@ export function createOutboundTransport(options) {
|
|
|
209
240
|
messageId,
|
|
210
241
|
at: new Date().toISOString(),
|
|
211
242
|
});
|
|
243
|
+
// This is deliberately after every guarded provider call succeeds and after
|
|
244
|
+
// the local outbound projection, not at Runtime acceptance.
|
|
245
|
+
report(true, { messageId });
|
|
212
246
|
return { ok: true, status: 200, data: { ok: true, state: "sent", messageId, messageSeq: 1 } };
|
|
213
247
|
};
|
|
214
248
|
return { handleSend, stageUpload };
|
|
@@ -9,6 +9,7 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
|
9
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
11
|
const agent_cli_capabilities_js_1 = require("../agent/agent-cli-capabilities.cjs");
|
|
12
|
+
const reminder_delivery_audit_js_1 = require("../agent/reminder-delivery-audit.cjs");
|
|
12
13
|
const inbox_projection_js_1 = require("../agent/inbox-projection.cjs");
|
|
13
14
|
const api_1 = require("@opentelemetry/api");
|
|
14
15
|
const provider_error_classifier_js_1 = require("./provider-error-classifier.cjs");
|
|
@@ -1071,6 +1072,33 @@ function createRuntimeHost(options) {
|
|
|
1071
1072
|
agent.piProactiveCompaction = tracked;
|
|
1072
1073
|
return tracked;
|
|
1073
1074
|
};
|
|
1075
|
+
// Reminder audit contexts are in-turn capabilities. Finalize any survivors
|
|
1076
|
+
// at both turn boundaries: turn end reconciles the turn that consumed them,
|
|
1077
|
+
// and turn start expires contexts left by a crashed/aborted prior turn so a
|
|
1078
|
+
// direct outbound cannot be falsely attributed to a stale reminder.
|
|
1079
|
+
const finalizeReminderContexts = (agent, reason) => {
|
|
1080
|
+
const legacyReminder = agent.stateStore?.resolveCurrentReminders ? null : agent.stateStore?.resolveCurrentReminder?.() ?? null;
|
|
1081
|
+
const reminderContexts = agent.stateStore?.resolveCurrentReminders?.() ?? (legacyReminder ? [legacyReminder] : []);
|
|
1082
|
+
let reminderAuditComplete = true;
|
|
1083
|
+
if (agent.stateStore?.paths?.reminders && agent.stateStore.clearCurrentReminder) {
|
|
1084
|
+
for (const reminder of reminderContexts) {
|
|
1085
|
+
try {
|
|
1086
|
+
(0, reminder_delivery_audit_js_1.auditReminderDelivery)({ stateStore: agent.stateStore, agentId: agent.config.agentId,
|
|
1087
|
+
reminderId: reminder.reminderId, deliveryAnchor: reminder.deliveryAnchor, target: reminder.deliveryTarget,
|
|
1088
|
+
succeeded: reminder.deliveryCommitted === true, finalize: true,
|
|
1089
|
+
...(reminder.deliveryMessageId ? { messageId: reminder.deliveryMessageId } : {}),
|
|
1090
|
+
...(!reminder.deliveryCommitted ? { reason } : {}) });
|
|
1091
|
+
}
|
|
1092
|
+
catch {
|
|
1093
|
+
// Retain the context if the failure could not be durably recorded;
|
|
1094
|
+
// a later matching outbound or turn can reconcile it.
|
|
1095
|
+
reminderAuditComplete = false;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (reminderAuditComplete)
|
|
1100
|
+
agent.stateStore?.clearCurrentReminders?.();
|
|
1101
|
+
};
|
|
1074
1102
|
const observe = (agent, session, event) => {
|
|
1075
1103
|
if (agent.session !== session)
|
|
1076
1104
|
return; // Ignore late output from a replaced child.
|
|
@@ -1113,6 +1141,10 @@ function createRuntimeHost(options) {
|
|
|
1113
1141
|
...(event.model ? { model: event.model } : {}), ...(event.reasoningEffort ? { reasoningEffort: event.reasoningEffort } : {}) });
|
|
1114
1142
|
}
|
|
1115
1143
|
else if (event.type === "turn-start") {
|
|
1144
|
+
// Clear a source left by a crashed/aborted prior turn before a new turn
|
|
1145
|
+
// can execute a direct task without an Inbox poll.
|
|
1146
|
+
agent.stateStore?.clearCurrentInboxSource?.();
|
|
1147
|
+
finalizeReminderContexts(agent, "Runtime turn started before a prior turn reconciled this reminder delivery");
|
|
1116
1148
|
agent.busy = true;
|
|
1117
1149
|
agent.turnInProgress = true;
|
|
1118
1150
|
agent.turnHadFailure = false;
|
|
@@ -1134,6 +1166,11 @@ function createRuntimeHost(options) {
|
|
|
1134
1166
|
agent.busy = false;
|
|
1135
1167
|
emit({ type: "activity", agentId: agent.config.agentId, activity: "idle", activityKind: "idle", detailKind: "turn_ended" });
|
|
1136
1168
|
reconcileAcceptedAtTurnEnd(agent);
|
|
1169
|
+
// last_source and reminder audit contexts are in-turn capabilities,
|
|
1170
|
+
// never cross-turn defaults. A later direct Runtime task must fail
|
|
1171
|
+
// closed instead of inheriting a previous turn's chat or reminder.
|
|
1172
|
+
agent.stateStore?.clearCurrentInboxSource?.();
|
|
1173
|
+
finalizeReminderContexts(agent, "Runtime turn ended without a matching Feishu write");
|
|
1137
1174
|
if (agent.backgroundCompletionInFlight) {
|
|
1138
1175
|
if (agent.turnHadFailure)
|
|
1139
1176
|
failBackgroundCompletionWake(agent);
|
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { agentCliPromptCapabilities } from "../agent/agent-cli-capabilities.mjs";
|
|
5
|
+
import { auditReminderDelivery } from "../agent/reminder-delivery-audit.mjs";
|
|
5
6
|
import { isCanonicalInboxTarget, targetKeyOfInboxEnvelope } from "../agent/inbox-projection.mjs";
|
|
6
7
|
import { SpanKind } from "@opentelemetry/api";
|
|
7
8
|
import { buildStrictProviderErrorInput, classifyStrictProviderError } from "./provider-error-classifier.mjs";
|
|
@@ -1062,6 +1063,33 @@ export function createRuntimeHost(options) {
|
|
|
1062
1063
|
agent.piProactiveCompaction = tracked;
|
|
1063
1064
|
return tracked;
|
|
1064
1065
|
};
|
|
1066
|
+
// Reminder audit contexts are in-turn capabilities. Finalize any survivors
|
|
1067
|
+
// at both turn boundaries: turn end reconciles the turn that consumed them,
|
|
1068
|
+
// and turn start expires contexts left by a crashed/aborted prior turn so a
|
|
1069
|
+
// direct outbound cannot be falsely attributed to a stale reminder.
|
|
1070
|
+
const finalizeReminderContexts = (agent, reason) => {
|
|
1071
|
+
const legacyReminder = agent.stateStore?.resolveCurrentReminders ? null : agent.stateStore?.resolveCurrentReminder?.() ?? null;
|
|
1072
|
+
const reminderContexts = agent.stateStore?.resolveCurrentReminders?.() ?? (legacyReminder ? [legacyReminder] : []);
|
|
1073
|
+
let reminderAuditComplete = true;
|
|
1074
|
+
if (agent.stateStore?.paths?.reminders && agent.stateStore.clearCurrentReminder) {
|
|
1075
|
+
for (const reminder of reminderContexts) {
|
|
1076
|
+
try {
|
|
1077
|
+
auditReminderDelivery({ stateStore: agent.stateStore, agentId: agent.config.agentId,
|
|
1078
|
+
reminderId: reminder.reminderId, deliveryAnchor: reminder.deliveryAnchor, target: reminder.deliveryTarget,
|
|
1079
|
+
succeeded: reminder.deliveryCommitted === true, finalize: true,
|
|
1080
|
+
...(reminder.deliveryMessageId ? { messageId: reminder.deliveryMessageId } : {}),
|
|
1081
|
+
...(!reminder.deliveryCommitted ? { reason } : {}) });
|
|
1082
|
+
}
|
|
1083
|
+
catch {
|
|
1084
|
+
// Retain the context if the failure could not be durably recorded;
|
|
1085
|
+
// a later matching outbound or turn can reconcile it.
|
|
1086
|
+
reminderAuditComplete = false;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (reminderAuditComplete)
|
|
1091
|
+
agent.stateStore?.clearCurrentReminders?.();
|
|
1092
|
+
};
|
|
1065
1093
|
const observe = (agent, session, event) => {
|
|
1066
1094
|
if (agent.session !== session)
|
|
1067
1095
|
return; // Ignore late output from a replaced child.
|
|
@@ -1104,6 +1132,10 @@ export function createRuntimeHost(options) {
|
|
|
1104
1132
|
...(event.model ? { model: event.model } : {}), ...(event.reasoningEffort ? { reasoningEffort: event.reasoningEffort } : {}) });
|
|
1105
1133
|
}
|
|
1106
1134
|
else if (event.type === "turn-start") {
|
|
1135
|
+
// Clear a source left by a crashed/aborted prior turn before a new turn
|
|
1136
|
+
// can execute a direct task without an Inbox poll.
|
|
1137
|
+
agent.stateStore?.clearCurrentInboxSource?.();
|
|
1138
|
+
finalizeReminderContexts(agent, "Runtime turn started before a prior turn reconciled this reminder delivery");
|
|
1107
1139
|
agent.busy = true;
|
|
1108
1140
|
agent.turnInProgress = true;
|
|
1109
1141
|
agent.turnHadFailure = false;
|
|
@@ -1125,6 +1157,11 @@ export function createRuntimeHost(options) {
|
|
|
1125
1157
|
agent.busy = false;
|
|
1126
1158
|
emit({ type: "activity", agentId: agent.config.agentId, activity: "idle", activityKind: "idle", detailKind: "turn_ended" });
|
|
1127
1159
|
reconcileAcceptedAtTurnEnd(agent);
|
|
1160
|
+
// last_source and reminder audit contexts are in-turn capabilities,
|
|
1161
|
+
// never cross-turn defaults. A later direct Runtime task must fail
|
|
1162
|
+
// closed instead of inheriting a previous turn's chat or reminder.
|
|
1163
|
+
agent.stateStore?.clearCurrentInboxSource?.();
|
|
1164
|
+
finalizeReminderContexts(agent, "Runtime turn ended without a matching Feishu write");
|
|
1128
1165
|
if (agent.backgroundCompletionInFlight) {
|
|
1129
1166
|
if (agent.turnHadFailure)
|
|
1130
1167
|
failBackgroundCompletionWake(agent);
|