larkin 0.3.3 → 0.4.1

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.
@@ -1,22 +1,118 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RUNTIME_REDELIVERY_TARGET = exports.RUNTIME_REMINDER_TARGET = void 0;
4
+ exports.isCanonicalInboxTarget = isCanonicalInboxTarget;
3
5
  exports.targetKeyOfInboxEnvelope = targetKeyOfInboxEnvelope;
4
6
  exports.projectInboxEnvelope = projectInboxEnvelope;
5
7
  exports.targetOfInboxEnvelope = targetOfInboxEnvelope;
6
8
  exports.projectInboxEvents = projectInboxEvents;
7
9
  exports.projectInboxCheck = projectInboxCheck;
10
+ exports.RUNTIME_REMINDER_TARGET = "runtime:reminder";
11
+ exports.RUNTIME_REDELIVERY_TARGET = "runtime:redelivery";
12
+ const CHAT_TARGET_PREFIX = "chat:";
13
+ const THREAD_TARGET_PREFIX = "thread:";
14
+ const DOCUMENT_COMMENT_TARGET_PREFIX = "document-comment:";
15
+ const REMINDER_MESSAGE_PREFIX = "rem_";
16
+ const REDELIVERY_MESSAGE_PREFIX = "redeliver_";
17
+ function hasNonemptySuffix(value, prefix) {
18
+ return value.startsWith(prefix) && value.length > prefix.length;
19
+ }
20
+ function internalTargetOfInboxEnvelope(envelope) {
21
+ const messageId = typeof envelope.message_id === "string" ? envelope.message_id : "";
22
+ const reminderKind = envelope.kind === "reminder";
23
+ const redeliveryKind = envelope.kind === "redelivery";
24
+ const reminderMarker = messageId.startsWith(REMINDER_MESSAGE_PREFIX);
25
+ const redeliveryMarker = messageId.startsWith(REDELIVERY_MESSAGE_PREFIX);
26
+ const reminderId = hasNonemptySuffix(messageId, REMINDER_MESSAGE_PREFIX);
27
+ const redeliveryId = hasNonemptySuffix(messageId, REDELIVERY_MESSAGE_PREFIX);
28
+ const hasInternalMarker = reminderKind || redeliveryKind || reminderMarker || redeliveryMarker;
29
+ if (!hasInternalMarker)
30
+ return null;
31
+ if (reminderKind && reminderId && !redeliveryKind && !redeliveryMarker)
32
+ return exports.RUNTIME_REMINDER_TARGET;
33
+ if (redeliveryKind && redeliveryId && !reminderKind && !reminderMarker)
34
+ return exports.RUNTIME_REDELIVERY_TARGET;
35
+ throw new Error(`Inbox internal source requires matching kind and message_id prefix; received kind=${JSON.stringify(envelope.kind ?? null)} message_id=${JSON.stringify(envelope.message_id ?? null)}`);
36
+ }
37
+ function isChatTarget(target) {
38
+ return hasNonemptySuffix(target, CHAT_TARGET_PREFIX);
39
+ }
40
+ function isThreadTarget(target) {
41
+ return hasNonemptySuffix(target, THREAD_TARGET_PREFIX);
42
+ }
43
+ function isDocumentCommentTarget(target) {
44
+ return hasNonemptySuffix(target, DOCUMENT_COMMENT_TARGET_PREFIX);
45
+ }
46
+ function isCanonicalInboxTarget(target) {
47
+ return isChatTarget(target)
48
+ || isThreadTarget(target)
49
+ || target === exports.RUNTIME_REMINDER_TARGET
50
+ || target === exports.RUNTIME_REDELIVERY_TARGET
51
+ || isDocumentCommentTarget(target);
52
+ }
53
+ function invalidTarget(target) {
54
+ return new Error(`Invalid canonical Inbox target ${JSON.stringify(target)}; expected chat:<nonempty>, thread:<nonempty>, runtime:reminder, runtime:redelivery, or document-comment:<nonempty>`);
55
+ }
56
+ function optionalLocator(envelope, key) {
57
+ const value = envelope[key];
58
+ if (value === undefined || value === null || value === "")
59
+ return "";
60
+ if (typeof value !== "string")
61
+ throw new Error(`Inbox ${key} must be a string when present; received ${JSON.stringify(value)}`);
62
+ return value;
63
+ }
8
64
  function targetKeyOfInboxEnvelope(envelope) {
9
65
  if (!envelope)
10
- return "runtime:unknown";
11
- if (envelope.kind === "document_comment" && typeof envelope.target === "string" && envelope.target) {
12
- return envelope.target;
66
+ throw new Error("Inbox envelope is required to derive a canonical target");
67
+ const internalTarget = internalTargetOfInboxEnvelope(envelope);
68
+ const chatId = optionalLocator(envelope, "chat_id");
69
+ const threadId = optionalLocator(envelope, "thread_id");
70
+ if (threadId && !chatId) {
71
+ throw new Error(`Inbox thread locator requires nonempty chat_id and thread_id; received chat_id=${JSON.stringify(chatId)} thread_id=${JSON.stringify(threadId)}`);
13
72
  }
14
- if (typeof envelope.chat_id === "string" && envelope.chat_id) {
15
- return typeof envelope.thread_id === "string" && envelope.thread_id
16
- ? `thread:${envelope.chat_id}:${envelope.thread_id}`
17
- : `chat:${envelope.chat_id}`;
73
+ const locatorTarget = chatId ? (threadId ? `thread:${chatId}:${threadId}` : `chat:${chatId}`) : null;
74
+ const hasExplicitTarget = Object.prototype.hasOwnProperty.call(envelope, "target");
75
+ if (hasExplicitTarget) {
76
+ if (typeof envelope.target !== "string" || envelope.target.length === 0)
77
+ throw invalidTarget(String(envelope.target));
78
+ const target = envelope.target;
79
+ if (!isCanonicalInboxTarget(target))
80
+ throw invalidTarget(target);
81
+ if (isChatTarget(target) || isThreadTarget(target)) {
82
+ if (internalTarget || envelope.kind === "document_comment")
83
+ throw new Error(`Inbox chat/thread target conflicts with source kind/message_id`);
84
+ if (locatorTarget && locatorTarget !== target) {
85
+ throw new Error(`Inbox chat/thread target conflicts with locator target ${JSON.stringify(locatorTarget)}`);
86
+ }
87
+ return target;
88
+ }
89
+ if (target === exports.RUNTIME_REMINDER_TARGET || target === exports.RUNTIME_REDELIVERY_TARGET) {
90
+ if (locatorTarget)
91
+ throw new Error(`Inbox runtime target cannot carry chat_id/thread_id locators`);
92
+ if (internalTarget !== target)
93
+ throw new Error(`Inbox target ${target} requires exact matching kind and message_id prefix`);
94
+ return target;
95
+ }
96
+ if (isDocumentCommentTarget(target)) {
97
+ if (envelope.kind !== "document_comment" || internalTarget)
98
+ throw new Error(`Inbox document-comment target requires kind=document_comment and no internal source`);
99
+ if (locatorTarget)
100
+ throw new Error(`Inbox document-comment target cannot carry chat_id/thread_id locators`);
101
+ return target;
102
+ }
103
+ throw invalidTarget(target);
18
104
  }
19
- return targetOfInboxEnvelope(envelope) || "runtime:system";
105
+ if (envelope.kind === "document_comment") {
106
+ throw new Error("Inbox document_comment requires an explicit document-comment:<nonempty> target");
107
+ }
108
+ if (locatorTarget) {
109
+ if (internalTarget)
110
+ throw new Error(`Inbox internal source cannot be derived as a chat/thread target`);
111
+ return locatorTarget;
112
+ }
113
+ if (internalTarget)
114
+ throw new Error(`Inbox internal source requires an explicit ${internalTarget} target`);
115
+ throw new Error(`Inbox envelope has no canonical target locator (message_id=${JSON.stringify(envelope.message_id ?? null)}, kind=${JSON.stringify(envelope.kind ?? null)})`);
20
116
  }
21
117
  /** Enrich the durable local Inbox without changing the canonical Runtime envelope. */
22
118
  function projectInboxEnvelope(canonical, locators) {
@@ -32,22 +128,14 @@ function projectInboxEnvelope(canonical, locators) {
32
128
  content: locators.content !== undefined ? locators.content : (typeof canonical.content === "string" ? canonical.content : ""),
33
129
  };
34
130
  }
35
- /** Preserve the target/thread format consumed by the existing Agent CLI. */
131
+ /** Return an allowlisted canonical target when the envelope is locatable. */
36
132
  function targetOfInboxEnvelope(envelope) {
37
- if (!envelope)
38
- return null;
39
- if (envelope.kind === "document_comment") {
40
- return typeof envelope.target === "string" && envelope.target ? envelope.target : null;
133
+ try {
134
+ return targetKeyOfInboxEnvelope(envelope);
41
135
  }
42
- if (envelope.channel_type === "thread" && envelope.parent_channel_name) {
43
- const base = envelope.parent_channel_type === "dm"
44
- ? `dm:@${String(envelope.parent_channel_name)}`
45
- : `#${String(envelope.parent_channel_name)}`;
46
- return `${base}:${String(envelope.channel_name || "").slice(0, 8)}`;
136
+ catch {
137
+ return null;
47
138
  }
48
- return envelope.channel_type === "dm"
49
- ? `dm:@${String(envelope.channel_name)}`
50
- : `#${String(envelope.channel_name)}`;
51
139
  }
52
140
  /** Project persisted inbox envelopes into the exact agentApi events response data shape. */
53
141
  function projectInboxEvents(envelopes) {
@@ -56,7 +144,7 @@ function projectInboxEvents(envelopes) {
56
144
  events: envelopes,
57
145
  last_seen_msgId: last ? (last.message_id || null) : null,
58
146
  last_seen_seq: last ? (last.seq ?? null) : null,
59
- reply_target: targetOfInboxEnvelope(last),
147
+ reply_target: last ? targetKeyOfInboxEnvelope(last) : null,
60
148
  pending_notice_ids: [],
61
149
  wake_reason: null,
62
150
  has_more: false,
@@ -64,9 +152,12 @@ function projectInboxEvents(envelopes) {
64
152
  }
65
153
  /** Bounded, content-light and deterministic projection for the read-only check command. */
66
154
  function projectInboxCheck(envelopes, onlyTarget) {
155
+ if (onlyTarget && !isCanonicalInboxTarget(onlyTarget)) {
156
+ throw new Error(`Invalid canonical Inbox check target ${JSON.stringify(onlyTarget)}`);
157
+ }
67
158
  const byTarget = new Map();
68
159
  for (const envelope of envelopes) {
69
- const target = typeof envelope.target === "string" && envelope.target ? envelope.target : targetKeyOfInboxEnvelope(envelope);
160
+ const target = targetKeyOfInboxEnvelope(envelope);
70
161
  if (onlyTarget && onlyTarget !== target)
71
162
  continue;
72
163
  const rows = byTarget.get(target) ?? [];
@@ -1,15 +1,109 @@
1
+ export const RUNTIME_REMINDER_TARGET = "runtime:reminder";
2
+ export const RUNTIME_REDELIVERY_TARGET = "runtime:redelivery";
3
+ const CHAT_TARGET_PREFIX = "chat:";
4
+ const THREAD_TARGET_PREFIX = "thread:";
5
+ const DOCUMENT_COMMENT_TARGET_PREFIX = "document-comment:";
6
+ const REMINDER_MESSAGE_PREFIX = "rem_";
7
+ const REDELIVERY_MESSAGE_PREFIX = "redeliver_";
8
+ function hasNonemptySuffix(value, prefix) {
9
+ return value.startsWith(prefix) && value.length > prefix.length;
10
+ }
11
+ function internalTargetOfInboxEnvelope(envelope) {
12
+ const messageId = typeof envelope.message_id === "string" ? envelope.message_id : "";
13
+ const reminderKind = envelope.kind === "reminder";
14
+ const redeliveryKind = envelope.kind === "redelivery";
15
+ const reminderMarker = messageId.startsWith(REMINDER_MESSAGE_PREFIX);
16
+ const redeliveryMarker = messageId.startsWith(REDELIVERY_MESSAGE_PREFIX);
17
+ const reminderId = hasNonemptySuffix(messageId, REMINDER_MESSAGE_PREFIX);
18
+ const redeliveryId = hasNonemptySuffix(messageId, REDELIVERY_MESSAGE_PREFIX);
19
+ const hasInternalMarker = reminderKind || redeliveryKind || reminderMarker || redeliveryMarker;
20
+ if (!hasInternalMarker)
21
+ return null;
22
+ if (reminderKind && reminderId && !redeliveryKind && !redeliveryMarker)
23
+ return RUNTIME_REMINDER_TARGET;
24
+ if (redeliveryKind && redeliveryId && !reminderKind && !reminderMarker)
25
+ return RUNTIME_REDELIVERY_TARGET;
26
+ throw new Error(`Inbox internal source requires matching kind and message_id prefix; received kind=${JSON.stringify(envelope.kind ?? null)} message_id=${JSON.stringify(envelope.message_id ?? null)}`);
27
+ }
28
+ function isChatTarget(target) {
29
+ return hasNonemptySuffix(target, CHAT_TARGET_PREFIX);
30
+ }
31
+ function isThreadTarget(target) {
32
+ return hasNonemptySuffix(target, THREAD_TARGET_PREFIX);
33
+ }
34
+ function isDocumentCommentTarget(target) {
35
+ return hasNonemptySuffix(target, DOCUMENT_COMMENT_TARGET_PREFIX);
36
+ }
37
+ export function isCanonicalInboxTarget(target) {
38
+ return isChatTarget(target)
39
+ || isThreadTarget(target)
40
+ || target === RUNTIME_REMINDER_TARGET
41
+ || target === RUNTIME_REDELIVERY_TARGET
42
+ || isDocumentCommentTarget(target);
43
+ }
44
+ function invalidTarget(target) {
45
+ return new Error(`Invalid canonical Inbox target ${JSON.stringify(target)}; expected chat:<nonempty>, thread:<nonempty>, runtime:reminder, runtime:redelivery, or document-comment:<nonempty>`);
46
+ }
47
+ function optionalLocator(envelope, key) {
48
+ const value = envelope[key];
49
+ if (value === undefined || value === null || value === "")
50
+ return "";
51
+ if (typeof value !== "string")
52
+ throw new Error(`Inbox ${key} must be a string when present; received ${JSON.stringify(value)}`);
53
+ return value;
54
+ }
1
55
  export function targetKeyOfInboxEnvelope(envelope) {
2
56
  if (!envelope)
3
- return "runtime:unknown";
4
- if (envelope.kind === "document_comment" && typeof envelope.target === "string" && envelope.target) {
5
- return envelope.target;
57
+ throw new Error("Inbox envelope is required to derive a canonical target");
58
+ const internalTarget = internalTargetOfInboxEnvelope(envelope);
59
+ const chatId = optionalLocator(envelope, "chat_id");
60
+ const threadId = optionalLocator(envelope, "thread_id");
61
+ if (threadId && !chatId) {
62
+ throw new Error(`Inbox thread locator requires nonempty chat_id and thread_id; received chat_id=${JSON.stringify(chatId)} thread_id=${JSON.stringify(threadId)}`);
6
63
  }
7
- if (typeof envelope.chat_id === "string" && envelope.chat_id) {
8
- return typeof envelope.thread_id === "string" && envelope.thread_id
9
- ? `thread:${envelope.chat_id}:${envelope.thread_id}`
10
- : `chat:${envelope.chat_id}`;
64
+ const locatorTarget = chatId ? (threadId ? `thread:${chatId}:${threadId}` : `chat:${chatId}`) : null;
65
+ const hasExplicitTarget = Object.prototype.hasOwnProperty.call(envelope, "target");
66
+ if (hasExplicitTarget) {
67
+ if (typeof envelope.target !== "string" || envelope.target.length === 0)
68
+ throw invalidTarget(String(envelope.target));
69
+ const target = envelope.target;
70
+ if (!isCanonicalInboxTarget(target))
71
+ throw invalidTarget(target);
72
+ if (isChatTarget(target) || isThreadTarget(target)) {
73
+ if (internalTarget || envelope.kind === "document_comment")
74
+ throw new Error(`Inbox chat/thread target conflicts with source kind/message_id`);
75
+ if (locatorTarget && locatorTarget !== target) {
76
+ throw new Error(`Inbox chat/thread target conflicts with locator target ${JSON.stringify(locatorTarget)}`);
77
+ }
78
+ return target;
79
+ }
80
+ if (target === RUNTIME_REMINDER_TARGET || target === RUNTIME_REDELIVERY_TARGET) {
81
+ if (locatorTarget)
82
+ throw new Error(`Inbox runtime target cannot carry chat_id/thread_id locators`);
83
+ if (internalTarget !== target)
84
+ throw new Error(`Inbox target ${target} requires exact matching kind and message_id prefix`);
85
+ return target;
86
+ }
87
+ if (isDocumentCommentTarget(target)) {
88
+ if (envelope.kind !== "document_comment" || internalTarget)
89
+ throw new Error(`Inbox document-comment target requires kind=document_comment and no internal source`);
90
+ if (locatorTarget)
91
+ throw new Error(`Inbox document-comment target cannot carry chat_id/thread_id locators`);
92
+ return target;
93
+ }
94
+ throw invalidTarget(target);
11
95
  }
12
- return targetOfInboxEnvelope(envelope) || "runtime:system";
96
+ if (envelope.kind === "document_comment") {
97
+ throw new Error("Inbox document_comment requires an explicit document-comment:<nonempty> target");
98
+ }
99
+ if (locatorTarget) {
100
+ if (internalTarget)
101
+ throw new Error(`Inbox internal source cannot be derived as a chat/thread target`);
102
+ return locatorTarget;
103
+ }
104
+ if (internalTarget)
105
+ throw new Error(`Inbox internal source requires an explicit ${internalTarget} target`);
106
+ throw new Error(`Inbox envelope has no canonical target locator (message_id=${JSON.stringify(envelope.message_id ?? null)}, kind=${JSON.stringify(envelope.kind ?? null)})`);
13
107
  }
14
108
  /** Enrich the durable local Inbox without changing the canonical Runtime envelope. */
15
109
  export function projectInboxEnvelope(canonical, locators) {
@@ -25,22 +119,14 @@ export function projectInboxEnvelope(canonical, locators) {
25
119
  content: locators.content !== undefined ? locators.content : (typeof canonical.content === "string" ? canonical.content : ""),
26
120
  };
27
121
  }
28
- /** Preserve the target/thread format consumed by the existing Agent CLI. */
122
+ /** Return an allowlisted canonical target when the envelope is locatable. */
29
123
  export function targetOfInboxEnvelope(envelope) {
30
- if (!envelope)
31
- return null;
32
- if (envelope.kind === "document_comment") {
33
- return typeof envelope.target === "string" && envelope.target ? envelope.target : null;
124
+ try {
125
+ return targetKeyOfInboxEnvelope(envelope);
34
126
  }
35
- if (envelope.channel_type === "thread" && envelope.parent_channel_name) {
36
- const base = envelope.parent_channel_type === "dm"
37
- ? `dm:@${String(envelope.parent_channel_name)}`
38
- : `#${String(envelope.parent_channel_name)}`;
39
- return `${base}:${String(envelope.channel_name || "").slice(0, 8)}`;
127
+ catch {
128
+ return null;
40
129
  }
41
- return envelope.channel_type === "dm"
42
- ? `dm:@${String(envelope.channel_name)}`
43
- : `#${String(envelope.channel_name)}`;
44
130
  }
45
131
  /** Project persisted inbox envelopes into the exact agentApi events response data shape. */
46
132
  export function projectInboxEvents(envelopes) {
@@ -49,7 +135,7 @@ export function projectInboxEvents(envelopes) {
49
135
  events: envelopes,
50
136
  last_seen_msgId: last ? (last.message_id || null) : null,
51
137
  last_seen_seq: last ? (last.seq ?? null) : null,
52
- reply_target: targetOfInboxEnvelope(last),
138
+ reply_target: last ? targetKeyOfInboxEnvelope(last) : null,
53
139
  pending_notice_ids: [],
54
140
  wake_reason: null,
55
141
  has_more: false,
@@ -57,9 +143,12 @@ export function projectInboxEvents(envelopes) {
57
143
  }
58
144
  /** Bounded, content-light and deterministic projection for the read-only check command. */
59
145
  export function projectInboxCheck(envelopes, onlyTarget) {
146
+ if (onlyTarget && !isCanonicalInboxTarget(onlyTarget)) {
147
+ throw new Error(`Invalid canonical Inbox check target ${JSON.stringify(onlyTarget)}`);
148
+ }
60
149
  const byTarget = new Map();
61
150
  for (const envelope of envelopes) {
62
- const target = typeof envelope.target === "string" && envelope.target ? envelope.target : targetKeyOfInboxEnvelope(envelope);
151
+ const target = targetKeyOfInboxEnvelope(envelope);
63
152
  if (onlyTarget && onlyTarget !== target)
64
153
  continue;
65
154
  const rows = byTarget.get(target) ?? [];
@@ -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 = /^document-comment:(doc|docx|sheet|file):([A-Za-z0-9_-]+):([A-Za-z0-9_-]+):(in-thread|top-level)$/.exec(value);
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 = /^document-comment:(doc|docx|sheet|file):([A-Za-z0-9_-]+):([A-Za-z0-9_-]+):(in-thread|top-level)$/.exec(value);
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
- return {
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
- return {
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
- return {
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
- return {
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 envelope = envelopeProjector.projectInbound(agent, event, { anchorReply: wake, names, signature });
349
- envelope.target = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)({ ...envelope, chat_id: event.chat_id, thread_id: event.thread_id });
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
- envelope.wake = true;
352
- const inboxEnvelope = (0, inbox_projection_js_1.projectInboxEnvelope)(envelope, {
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(envelope.content ?? event.content ?? ""),
368
+ content: String(projected.content ?? event.content ?? ""),
358
369
  });
370
+ let append;
359
371
  try {
360
- stateStore(agent).appendNdjson("inbox", inboxEnvelope);
372
+ append = stateStore(agent).appendCanonicalInboxOnce(candidate);
361
373
  }
362
374
  catch (error) {
363
375
  throw new Error(`inbox 写失败: ${errorMessage(error)}`);
364
376
  }
365
- // An event becomes permanently seen only after its canonical Inbox append
366
- // is durable. Failures remain eligible for same-process redelivery.
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
- hostState.appendConversation(agent, {
370
- direction: "in", from: envelope.sender_name, senderType: envelope.sender_type,
371
- target: (0, message_policy_js_1.targetFor)(event).target, wake, text: event.content, messageId: envelope.message_id,
372
- at: envelope.timestamp || new Date().toISOString(),
373
- });
374
- return envelope;
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 envelope = wake && telemetry
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, envelope);
382
- if (receipt.status === "accepted" || receipt.status === "duplicate" || receipt.status === "deferred") {
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
- from: envelope.sender_name,
385
- target: (0, message_policy_js_1.targetFor)(event).target,
386
- excerpt: (0, host_business_state_js_1.safeConversationExcerpt)(event.content, 180),
387
- at: new Date().toISOString(),
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
- log(`onFeishuMessage 异常 agent=${agent.name}: ${error instanceof Error ? error.stack || error.message : String(error)}`);
399
- hostState.recordStatusError(agent, `onFeishuMessage: ${errorMessage(error)}`);
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,9 +1270,14 @@ 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, ...(message.reason ? { reason: (0, host_business_state_js_1.safeConversationExcerpt)(message.reason, 120) } : {}),
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
  }
@@ -1266,7 +1300,8 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1266
1300
  hostState.recordStatusError(agent, `auth: ${readiness.reason}; ${readiness.nextAction}`);
1267
1301
  }
1268
1302
  else {
1269
- hostState.recordStatusError(agent, `${message.event.errorCategory || "provider"}: ${message.event.message}${message.event.nextAction ? `; ${message.event.nextAction}` : ""}`);
1303
+ const category = message.event.errorCategory || "provider";
1304
+ hostState.recordStatusError(agent, `${category}: Runtime input failed ${message.event.retryable ? "retryably" : "non-retryably"}; inspect delivery health and Runtime/provider configuration`);
1270
1305
  }
1271
1306
  }
1272
1307
  };