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.
@@ -105,25 +105,150 @@ class HostReminderOrchestrator {
105
105
  }
106
106
  deliver(agent, reminder, overdueMs) {
107
107
  const recurrence = reminder.repeat ? this.store.parseRepeat(reminder.repeat, reminder.tz) : null;
108
- const envelope = withCanonicalTarget(this.options.envelopeProjector.createReminderEnvelope(agent.agentId, {
108
+ const projected = this.options.envelopeProjector.createReminderEnvelope(agent.agentId, {
109
109
  reminderId: reminder.reminderId,
110
110
  title: reminder.title,
111
111
  repeat: reminder.repeat ? String(reminder.repeat) : null,
112
112
  fireAt: reminder.fireAt,
113
113
  msgRef: reminder.msgRef,
114
114
  channel: reminder.channel ? String(reminder.channel) : null,
115
- }, overdueMs, reminder.repeat && recurrence && !("error" in recurrence) ? (recurrence.description || null) : null));
115
+ deliveryTarget: reminder.deliveryTarget ?? null,
116
+ deliveryAnchor: reminder.deliveryAnchor ?? null,
117
+ }, overdueMs, reminder.repeat && recurrence && !("error" in recurrence) ? (recurrence.description || null) : null);
118
+ const envelope = withCanonicalTarget({ ...projected,
119
+ // The projector also derives safe routing for pre-0.4.14 records; do not
120
+ // overwrite that migration result with the absent fields from the record.
121
+ deliveryTarget: reminder.deliveryTarget ?? projected.deliveryTarget ?? null,
122
+ deliveryAnchor: reminder.deliveryAnchor ?? projected.deliveryAnchor ?? null });
123
+ const auditableDelivery = typeof envelope.deliveryTarget === "string" && envelope.deliveryTarget.length > 0;
124
+ // Persist the pending audit marker before the Inbox row becomes pollable.
125
+ // An already-active Agent can poll and answer the wake immediately, and
126
+ // RuntimeHost.deliver may execute the whole turn before its promise settles
127
+ // (notably with Pi); without a durable pending marker that outbound write
128
+ // would be invisible to the audit. Internal/no-delivery reminders still
129
+ // wake the Runtime, but have no user-facing delivery to audit.
130
+ if (auditableDelivery && !this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_pending", {
131
+ outcome: "awaiting_runtime", deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
132
+ })) {
133
+ this.log(`reminder delivery pending 审计写失败 id=#${reminder.reminderId.slice(0, 8)}(拒绝唤醒)`);
134
+ return;
135
+ }
136
+ let stateStore;
137
+ let deliveryAnchorBound = false;
116
138
  try {
117
- this.options.stateStore(agent).appendNdjson("inbox", envelope);
139
+ stateStore = this.options.stateStore(agent);
140
+ if (envelope.deliveryTarget && envelope.deliveryAnchor) {
141
+ stateStore.bindInboxDeliveryAnchor(envelope.deliveryAnchor, envelope.deliveryTarget);
142
+ deliveryAnchorBound = true;
143
+ }
144
+ stateStore.appendNdjson("inbox", envelope);
118
145
  }
119
146
  catch (error) {
147
+ if (deliveryAnchorBound && stateStore?.unbindInboxDeliveryAnchor && envelope.deliveryTarget && envelope.deliveryAnchor) {
148
+ try {
149
+ stateStore.unbindInboxDeliveryAnchor(envelope.deliveryAnchor, envelope.deliveryTarget);
150
+ }
151
+ catch (rollbackError) {
152
+ this.log("reminder Inbox anchor 回滚失败", rollbackError.message);
153
+ }
154
+ }
120
155
  this.log("reminder inbox 写失败", error.message);
156
+ // The wake row never became durable, so the pending marker must not stay
157
+ // open awaiting a Runtime turn that can never poll this occurrence.
158
+ if (auditableDelivery) {
159
+ this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
160
+ outcome: "inbox_write_failed", reason: String(error.message || error),
161
+ deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
162
+ });
163
+ }
121
164
  return;
122
165
  }
123
- if (this.options.deliveryTarget)
124
- void this.options.deliveryTarget.deliver(agent.agentId, envelope);
125
- else
166
+ if (!this.options.deliveryTarget) {
126
167
  this.log(`reminder 触发但 Runtime Host 未就绪 id=#${reminder.reminderId.slice(0, 8)}(仅入 inbox)`);
168
+ if (auditableDelivery) {
169
+ this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
170
+ outcome: "runtime_unavailable", occurrenceId: envelope.message_id,
171
+ });
172
+ }
173
+ return;
174
+ }
175
+ try {
176
+ const recordReceipt = (receipt) => {
177
+ if (!auditableDelivery)
178
+ return;
179
+ const status = receipt && typeof receipt === "object" ? String(receipt.status || "") : "";
180
+ const reason = receipt && typeof receipt === "object" ? String(receipt.reason || "") : "";
181
+ // RuntimeHost.deliver only acknowledges Inbox persistence/wake submission.
182
+ // It does not prove that the Agent later committed an outbound Feishu
183
+ // message, so accepted, duplicate, and an empty receipt remain pending.
184
+ if (!status || status === "accepted" || status === "duplicate") {
185
+ this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_pending", {
186
+ outcome: status || "empty", deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
187
+ });
188
+ }
189
+ else if (status === "deferred") {
190
+ this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_pending", {
191
+ outcome: "deferred", ...(reason ? { reason } : {}), deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
192
+ });
193
+ }
194
+ else {
195
+ this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
196
+ outcome: "error", ...(reason ? { reason } : {}), deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
197
+ });
198
+ }
199
+ };
200
+ const result = this.options.deliveryTarget.deliver(agent.agentId, envelope);
201
+ if (result && typeof result.then === "function") {
202
+ void Promise.resolve(result).then(recordReceipt, (error) => this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
203
+ outcome: "rejected", reason: String(error?.message || error), deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
204
+ }));
205
+ }
206
+ else {
207
+ recordReceipt(result);
208
+ }
209
+ }
210
+ catch (error) {
211
+ if (typeof envelope.deliveryTarget === "string" && envelope.deliveryTarget.length > 0) {
212
+ this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
213
+ outcome: "rejected", reason: String(error.message || error), deliveryTarget: envelope.deliveryTarget,
214
+ occurrenceId: envelope.message_id,
215
+ });
216
+ }
217
+ this.log(`reminder delivery 失败 id=#${reminder.reminderId.slice(0, 8)}: ${error.message}`);
218
+ }
219
+ }
220
+ recordDeliveryOutcome(agent, reminderId, eventType, metadata) {
221
+ try {
222
+ return this.store.mutate(this.options.stateStore(agent).paths.reminders, (store) => {
223
+ const reminder = store.reminders.find((candidate) => candidate.reminderId === reminderId);
224
+ if (!reminder)
225
+ return false;
226
+ const occurrenceId = typeof metadata.occurrenceId === "string" ? metadata.occurrenceId : null;
227
+ const occurrenceEvents = occurrenceId
228
+ ? reminder.events?.filter((event) => event.metadata?.occurrenceId === occurrenceId)
229
+ : undefined;
230
+ const hasOccurrenceMetadata = reminder.events?.some((event) => typeof event.metadata?.occurrenceId === "string");
231
+ const last = occurrenceId
232
+ ? (occurrenceEvents?.at(-1) ?? (hasOccurrenceMetadata && reminder.events?.at(-1)?.eventType !== "fired"
233
+ ? undefined : reminder.events?.at(-1)))
234
+ : reminder.events?.at(-1);
235
+ if (!last || !["fired", "delivery_pending", "delivery_failed"].includes(last.eventType))
236
+ return false;
237
+ // A late Runtime receipt must not resurrect an occurrence finalized as failed.
238
+ if (eventType === "delivery_pending" && last.eventType === "delivery_failed")
239
+ return false;
240
+ // A synchronous outbound can finish inside deliveryTarget.deliver().
241
+ // Never let its later Runtime receipt callback downgrade that success.
242
+ if (eventType !== "delivery_succeeded" && last.eventType === "delivery_succeeded")
243
+ return false;
244
+ this.store.appendEvent(reminder, eventType, "system", null, reminder.status === "scheduled" ? reminder.fireAt : null, this.now(), metadata);
245
+ return true;
246
+ });
247
+ }
248
+ catch (error) {
249
+ this.log(`reminder delivery audit 写失败 id=#${reminderId.slice(0, 8)}: ${error.message}`);
250
+ return false;
251
+ }
127
252
  }
128
253
  handleFire(message) {
129
254
  const agent = this.options.agents.find((candidate) => candidate.agentId === message.agentId);
@@ -163,7 +288,9 @@ class HostReminderOrchestrator {
163
288
  reminder.firedAt = this.store.nowIso(now);
164
289
  }
165
290
  reminder.version = Number(reminder.version) + 1;
166
- this.store.appendEvent(reminder, "fired", "system", null, reminder.status === "scheduled" ? reminder.fireAt : null, now);
291
+ this.store.appendEvent(reminder, "fired", "system", null, reminder.status === "scheduled" ? reminder.fireAt : null, now, {
292
+ deliveryTarget: reminder.deliveryTarget ?? null, deliveryAnchor: reminder.deliveryAnchor ?? null,
293
+ });
167
294
  store.reminders = store.reminders.filter((candidate) => candidate.status === "scheduled" || now - Date.parse(candidate.firedAt || candidate.createdAt || "") < 30 * 86_400_000);
168
295
  action = "deliver";
169
296
  fired = { ...reminder };
@@ -69,25 +69,150 @@ export class HostReminderOrchestrator {
69
69
  }
70
70
  deliver(agent, reminder, overdueMs) {
71
71
  const recurrence = reminder.repeat ? this.store.parseRepeat(reminder.repeat, reminder.tz) : null;
72
- const envelope = withCanonicalTarget(this.options.envelopeProjector.createReminderEnvelope(agent.agentId, {
72
+ const projected = this.options.envelopeProjector.createReminderEnvelope(agent.agentId, {
73
73
  reminderId: reminder.reminderId,
74
74
  title: reminder.title,
75
75
  repeat: reminder.repeat ? String(reminder.repeat) : null,
76
76
  fireAt: reminder.fireAt,
77
77
  msgRef: reminder.msgRef,
78
78
  channel: reminder.channel ? String(reminder.channel) : null,
79
- }, overdueMs, reminder.repeat && recurrence && !("error" in recurrence) ? (recurrence.description || null) : null));
79
+ deliveryTarget: reminder.deliveryTarget ?? null,
80
+ deliveryAnchor: reminder.deliveryAnchor ?? null,
81
+ }, overdueMs, reminder.repeat && recurrence && !("error" in recurrence) ? (recurrence.description || null) : null);
82
+ const envelope = withCanonicalTarget({ ...projected,
83
+ // The projector also derives safe routing for pre-0.4.14 records; do not
84
+ // overwrite that migration result with the absent fields from the record.
85
+ deliveryTarget: reminder.deliveryTarget ?? projected.deliveryTarget ?? null,
86
+ deliveryAnchor: reminder.deliveryAnchor ?? projected.deliveryAnchor ?? null });
87
+ const auditableDelivery = typeof envelope.deliveryTarget === "string" && envelope.deliveryTarget.length > 0;
88
+ // Persist the pending audit marker before the Inbox row becomes pollable.
89
+ // An already-active Agent can poll and answer the wake immediately, and
90
+ // RuntimeHost.deliver may execute the whole turn before its promise settles
91
+ // (notably with Pi); without a durable pending marker that outbound write
92
+ // would be invisible to the audit. Internal/no-delivery reminders still
93
+ // wake the Runtime, but have no user-facing delivery to audit.
94
+ if (auditableDelivery && !this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_pending", {
95
+ outcome: "awaiting_runtime", deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
96
+ })) {
97
+ this.log(`reminder delivery pending 审计写失败 id=#${reminder.reminderId.slice(0, 8)}(拒绝唤醒)`);
98
+ return;
99
+ }
100
+ let stateStore;
101
+ let deliveryAnchorBound = false;
80
102
  try {
81
- this.options.stateStore(agent).appendNdjson("inbox", envelope);
103
+ stateStore = this.options.stateStore(agent);
104
+ if (envelope.deliveryTarget && envelope.deliveryAnchor) {
105
+ stateStore.bindInboxDeliveryAnchor(envelope.deliveryAnchor, envelope.deliveryTarget);
106
+ deliveryAnchorBound = true;
107
+ }
108
+ stateStore.appendNdjson("inbox", envelope);
82
109
  }
83
110
  catch (error) {
111
+ if (deliveryAnchorBound && stateStore?.unbindInboxDeliveryAnchor && envelope.deliveryTarget && envelope.deliveryAnchor) {
112
+ try {
113
+ stateStore.unbindInboxDeliveryAnchor(envelope.deliveryAnchor, envelope.deliveryTarget);
114
+ }
115
+ catch (rollbackError) {
116
+ this.log("reminder Inbox anchor 回滚失败", rollbackError.message);
117
+ }
118
+ }
84
119
  this.log("reminder inbox 写失败", error.message);
120
+ // The wake row never became durable, so the pending marker must not stay
121
+ // open awaiting a Runtime turn that can never poll this occurrence.
122
+ if (auditableDelivery) {
123
+ this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
124
+ outcome: "inbox_write_failed", reason: String(error.message || error),
125
+ deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
126
+ });
127
+ }
85
128
  return;
86
129
  }
87
- if (this.options.deliveryTarget)
88
- void this.options.deliveryTarget.deliver(agent.agentId, envelope);
89
- else
130
+ if (!this.options.deliveryTarget) {
90
131
  this.log(`reminder 触发但 Runtime Host 未就绪 id=#${reminder.reminderId.slice(0, 8)}(仅入 inbox)`);
132
+ if (auditableDelivery) {
133
+ this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
134
+ outcome: "runtime_unavailable", occurrenceId: envelope.message_id,
135
+ });
136
+ }
137
+ return;
138
+ }
139
+ try {
140
+ const recordReceipt = (receipt) => {
141
+ if (!auditableDelivery)
142
+ return;
143
+ const status = receipt && typeof receipt === "object" ? String(receipt.status || "") : "";
144
+ const reason = receipt && typeof receipt === "object" ? String(receipt.reason || "") : "";
145
+ // RuntimeHost.deliver only acknowledges Inbox persistence/wake submission.
146
+ // It does not prove that the Agent later committed an outbound Feishu
147
+ // message, so accepted, duplicate, and an empty receipt remain pending.
148
+ if (!status || status === "accepted" || status === "duplicate") {
149
+ this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_pending", {
150
+ outcome: status || "empty", deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
151
+ });
152
+ }
153
+ else if (status === "deferred") {
154
+ this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_pending", {
155
+ outcome: "deferred", ...(reason ? { reason } : {}), deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
156
+ });
157
+ }
158
+ else {
159
+ this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
160
+ outcome: "error", ...(reason ? { reason } : {}), deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
161
+ });
162
+ }
163
+ };
164
+ const result = this.options.deliveryTarget.deliver(agent.agentId, envelope);
165
+ if (result && typeof result.then === "function") {
166
+ void Promise.resolve(result).then(recordReceipt, (error) => this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
167
+ outcome: "rejected", reason: String(error?.message || error), deliveryTarget: envelope.deliveryTarget, occurrenceId: envelope.message_id,
168
+ }));
169
+ }
170
+ else {
171
+ recordReceipt(result);
172
+ }
173
+ }
174
+ catch (error) {
175
+ if (typeof envelope.deliveryTarget === "string" && envelope.deliveryTarget.length > 0) {
176
+ this.recordDeliveryOutcome(agent, reminder.reminderId, "delivery_failed", {
177
+ outcome: "rejected", reason: String(error.message || error), deliveryTarget: envelope.deliveryTarget,
178
+ occurrenceId: envelope.message_id,
179
+ });
180
+ }
181
+ this.log(`reminder delivery 失败 id=#${reminder.reminderId.slice(0, 8)}: ${error.message}`);
182
+ }
183
+ }
184
+ recordDeliveryOutcome(agent, reminderId, eventType, metadata) {
185
+ try {
186
+ return this.store.mutate(this.options.stateStore(agent).paths.reminders, (store) => {
187
+ const reminder = store.reminders.find((candidate) => candidate.reminderId === reminderId);
188
+ if (!reminder)
189
+ return false;
190
+ const occurrenceId = typeof metadata.occurrenceId === "string" ? metadata.occurrenceId : null;
191
+ const occurrenceEvents = occurrenceId
192
+ ? reminder.events?.filter((event) => event.metadata?.occurrenceId === occurrenceId)
193
+ : undefined;
194
+ const hasOccurrenceMetadata = reminder.events?.some((event) => typeof event.metadata?.occurrenceId === "string");
195
+ const last = occurrenceId
196
+ ? (occurrenceEvents?.at(-1) ?? (hasOccurrenceMetadata && reminder.events?.at(-1)?.eventType !== "fired"
197
+ ? undefined : reminder.events?.at(-1)))
198
+ : reminder.events?.at(-1);
199
+ if (!last || !["fired", "delivery_pending", "delivery_failed"].includes(last.eventType))
200
+ return false;
201
+ // A late Runtime receipt must not resurrect an occurrence finalized as failed.
202
+ if (eventType === "delivery_pending" && last.eventType === "delivery_failed")
203
+ return false;
204
+ // A synchronous outbound can finish inside deliveryTarget.deliver().
205
+ // Never let its later Runtime receipt callback downgrade that success.
206
+ if (eventType !== "delivery_succeeded" && last.eventType === "delivery_succeeded")
207
+ return false;
208
+ this.store.appendEvent(reminder, eventType, "system", null, reminder.status === "scheduled" ? reminder.fireAt : null, this.now(), metadata);
209
+ return true;
210
+ });
211
+ }
212
+ catch (error) {
213
+ this.log(`reminder delivery audit 写失败 id=#${reminderId.slice(0, 8)}: ${error.message}`);
214
+ return false;
215
+ }
91
216
  }
92
217
  handleFire(message) {
93
218
  const agent = this.options.agents.find((candidate) => candidate.agentId === message.agentId);
@@ -127,7 +252,9 @@ export class HostReminderOrchestrator {
127
252
  reminder.firedAt = this.store.nowIso(now);
128
253
  }
129
254
  reminder.version = Number(reminder.version) + 1;
130
- this.store.appendEvent(reminder, "fired", "system", null, reminder.status === "scheduled" ? reminder.fireAt : null, now);
255
+ this.store.appendEvent(reminder, "fired", "system", null, reminder.status === "scheduled" ? reminder.fireAt : null, now, {
256
+ deliveryTarget: reminder.deliveryTarget ?? null, deliveryAnchor: reminder.deliveryAnchor ?? null,
257
+ });
131
258
  store.reminders = store.reminders.filter((candidate) => candidate.status === "scheduled" || now - Date.parse(candidate.firedAt || candidate.createdAt || "") < 30 * 86_400_000);
132
259
  action = "deliver";
133
260
  fired = { ...reminder };
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RUNTIME_REDELIVERY_TARGET = exports.RUNTIME_REMINDER_TARGET = void 0;
4
4
  exports.isCanonicalInboxTarget = isCanonicalInboxTarget;
5
+ exports.isUserDeliveryTarget = isUserDeliveryTarget;
5
6
  exports.targetKeyOfInboxEnvelope = targetKeyOfInboxEnvelope;
6
7
  exports.projectInboxEnvelope = projectInboxEnvelope;
7
8
  exports.targetOfInboxEnvelope = targetOfInboxEnvelope;
@@ -50,6 +51,10 @@ function isCanonicalInboxTarget(target) {
50
51
  || target === exports.RUNTIME_REDELIVERY_TARGET
51
52
  || isDocumentCommentTarget(target);
52
53
  }
54
+ /** Targets that can safely receive a user-facing reminder. Runtime wake targets are not delivery destinations. */
55
+ function isUserDeliveryTarget(target) {
56
+ return isChatTarget(target) || isThreadTarget(target) || isDocumentCommentTarget(target);
57
+ }
53
58
  function invalidTarget(target) {
54
59
  return new Error(`Invalid canonical Inbox target ${JSON.stringify(target)}; expected chat:<nonempty>, thread:<nonempty>, runtime:reminder, runtime:redelivery, or document-comment:<nonempty>`);
55
60
  }
@@ -41,6 +41,10 @@ export function isCanonicalInboxTarget(target) {
41
41
  || target === RUNTIME_REDELIVERY_TARGET
42
42
  || isDocumentCommentTarget(target);
43
43
  }
44
+ /** Targets that can safely receive a user-facing reminder. Runtime wake targets are not delivery destinations. */
45
+ export function isUserDeliveryTarget(target) {
46
+ return isChatTarget(target) || isThreadTarget(target) || isDocumentCommentTarget(target);
47
+ }
44
48
  function invalidTarget(target) {
45
49
  return new Error(`Invalid canonical Inbox target ${JSON.stringify(target)}; expected chat:<nonempty>, thread:<nonempty>, runtime:reminder, runtime:redelivery, or document-comment:<nonempty>`);
46
50
  }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.auditReminderDelivery = auditReminderDelivery;
37
+ const ReminderStore = __importStar(require("./reminder-store.cjs"));
38
+ /** Record only committed, non-dry-run outbound attempts for the consumed reminder. */
39
+ function auditReminderDelivery(input) {
40
+ if (input.dryRun)
41
+ return;
42
+ const current = (input.stateStore.resolveCurrentReminders?.() ?? [input.stateStore.resolveCurrentReminder()]).filter((candidate) => Boolean(candidate)).find((candidate) => {
43
+ if (input.reminderId && candidate.reminderId !== input.reminderId)
44
+ return false;
45
+ if (input.deliveryAnchor && candidate.deliveryAnchor !== input.deliveryAnchor)
46
+ return false;
47
+ const currentChatId = candidate.deliveryTarget.match(/^chat:(oc_[A-Za-z0-9_-]+)$/)?.[1];
48
+ const outboundChatId = currentChatId && input.resolveChatId ? input.resolveChatId(input.target) : null;
49
+ return candidate.deliveryTarget === input.target || outboundChatId === currentChatId;
50
+ });
51
+ if (!current)
52
+ return;
53
+ // Match events against the consumed occurrence even when the outbound did
54
+ // not name one: a snooze/update/cancel appended after polling must not hide
55
+ // the pending occurrence that this committed write actually fulfilled.
56
+ const anchor = input.deliveryAnchor || current.deliveryAnchor;
57
+ let persisted = false;
58
+ ReminderStore.mutate(input.stateStore.paths.reminders, (store) => {
59
+ const reminder = store.reminders.find((candidate) => candidate.reminderId === current.reminderId);
60
+ const occurrenceEvents = anchor
61
+ ? reminder?.events?.filter((event) => event.metadata?.occurrenceId === anchor)
62
+ : undefined;
63
+ const hasOccurrenceMetadata = reminder?.events?.some((event) => typeof event.metadata?.occurrenceId === "string");
64
+ const last = anchor
65
+ ? (occurrenceEvents?.at(-1) ?? (hasOccurrenceMetadata ? undefined : reminder?.events?.at(-1)))
66
+ : reminder?.events?.at(-1);
67
+ if (!reminder || !last || !["delivery_pending", "delivery_failed"].includes(last.eventType))
68
+ return;
69
+ if (input.finalize && !input.succeeded && last.eventType === "delivery_failed")
70
+ return;
71
+ if (!input.succeeded && last.eventType === "delivery_failed")
72
+ return;
73
+ ReminderStore.appendEvent(reminder, input.succeeded ? "delivery_succeeded" : "delivery_failed", "agent", input.agentId, reminder.status === "scheduled" ? reminder.fireAt : null, Date.now(), {
74
+ deliveryTarget: input.target,
75
+ occurrenceId: current.deliveryAnchor,
76
+ ...((input.messageId || current.deliveryMessageId) ? { messageId: input.messageId || current.deliveryMessageId } : {}),
77
+ ...(input.reason ? { reason: input.reason } : {}),
78
+ });
79
+ persisted = true;
80
+ });
81
+ // Clear only after delivery_succeeded was durably appended; otherwise keep
82
+ // the occurrence context so a later reconciliation can still record the
83
+ // committed write instead of silently dropping its audit trail.
84
+ if (input.succeeded && persisted)
85
+ input.stateStore.clearCurrentReminder(current.reminderId, current.deliveryAnchor);
86
+ }
@@ -0,0 +1,50 @@
1
+ import * as ReminderStore from "./reminder-store.mjs";
2
+ /** Record only committed, non-dry-run outbound attempts for the consumed reminder. */
3
+ export function auditReminderDelivery(input) {
4
+ if (input.dryRun)
5
+ return;
6
+ const current = (input.stateStore.resolveCurrentReminders?.() ?? [input.stateStore.resolveCurrentReminder()]).filter((candidate) => Boolean(candidate)).find((candidate) => {
7
+ if (input.reminderId && candidate.reminderId !== input.reminderId)
8
+ return false;
9
+ if (input.deliveryAnchor && candidate.deliveryAnchor !== input.deliveryAnchor)
10
+ return false;
11
+ const currentChatId = candidate.deliveryTarget.match(/^chat:(oc_[A-Za-z0-9_-]+)$/)?.[1];
12
+ const outboundChatId = currentChatId && input.resolveChatId ? input.resolveChatId(input.target) : null;
13
+ return candidate.deliveryTarget === input.target || outboundChatId === currentChatId;
14
+ });
15
+ if (!current)
16
+ return;
17
+ // Match events against the consumed occurrence even when the outbound did
18
+ // not name one: a snooze/update/cancel appended after polling must not hide
19
+ // the pending occurrence that this committed write actually fulfilled.
20
+ const anchor = input.deliveryAnchor || current.deliveryAnchor;
21
+ let persisted = false;
22
+ ReminderStore.mutate(input.stateStore.paths.reminders, (store) => {
23
+ const reminder = store.reminders.find((candidate) => candidate.reminderId === current.reminderId);
24
+ const occurrenceEvents = anchor
25
+ ? reminder?.events?.filter((event) => event.metadata?.occurrenceId === anchor)
26
+ : undefined;
27
+ const hasOccurrenceMetadata = reminder?.events?.some((event) => typeof event.metadata?.occurrenceId === "string");
28
+ const last = anchor
29
+ ? (occurrenceEvents?.at(-1) ?? (hasOccurrenceMetadata ? undefined : reminder?.events?.at(-1)))
30
+ : reminder?.events?.at(-1);
31
+ if (!reminder || !last || !["delivery_pending", "delivery_failed"].includes(last.eventType))
32
+ return;
33
+ if (input.finalize && !input.succeeded && last.eventType === "delivery_failed")
34
+ return;
35
+ if (!input.succeeded && last.eventType === "delivery_failed")
36
+ return;
37
+ ReminderStore.appendEvent(reminder, input.succeeded ? "delivery_succeeded" : "delivery_failed", "agent", input.agentId, reminder.status === "scheduled" ? reminder.fireAt : null, Date.now(), {
38
+ deliveryTarget: input.target,
39
+ occurrenceId: current.deliveryAnchor,
40
+ ...((input.messageId || current.deliveryMessageId) ? { messageId: input.messageId || current.deliveryMessageId } : {}),
41
+ ...(input.reason ? { reason: input.reason } : {}),
42
+ });
43
+ persisted = true;
44
+ });
45
+ // Clear only after delivery_succeeded was durably appended; otherwise keep
46
+ // the occurrence context so a later reconciliation can still record the
47
+ // committed write instead of silently dropping its audit trail.
48
+ if (input.succeeded && persisted)
49
+ input.stateStore.clearCurrentReminder(current.reminderId, current.deliveryAnchor);
50
+ }
@@ -35,9 +35,32 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.createReminderRoutes = createReminderRoutes;
37
37
  const ReminderStore = __importStar(require("./reminder-store.cjs"));
38
+ const inbox_projection_js_1 = require("./inbox-projection.cjs");
38
39
  function versionOf(reminder) {
39
40
  return Number(reminder.version || 0);
40
41
  }
42
+ const VALID_IM_ANCHOR = /^om_[A-Za-z0-9_-]+$/;
43
+ const VALID_COMMENT_ANCHOR = /^doc_comment_[A-Za-z0-9_-]+$/;
44
+ const VALID_CHAT_TARGET = /^chat:oc_[A-Za-z0-9_-]+$/;
45
+ const VALID_THREAD_TARGET = /^thread:oc_[A-Za-z0-9_-]+:omt_[A-Za-z0-9_-]+$/;
46
+ const VALID_DOCUMENT_COMMENT_TARGET = /^document-comment:(doc|docx|sheet|file):[A-Za-z0-9_-]+:[A-Za-z0-9_-]+:(in-thread|top-level)$/;
47
+ function userTarget(value, field) {
48
+ const raw = String(value ?? "").trim();
49
+ // --channel historically accepted an oc_ chat id; retain that unambiguous form.
50
+ const target = /^oc_[A-Za-z0-9_-]+$/.test(raw) ? `chat:${raw}` : raw;
51
+ const complete = VALID_CHAT_TARGET.test(target) || VALID_THREAD_TARGET.test(target) || VALID_DOCUMENT_COMMENT_TARGET.test(target);
52
+ if (!complete || !(0, inbox_projection_js_1.isCanonicalInboxTarget)(target) || !(0, inbox_projection_js_1.isUserDeliveryTarget)(target)) {
53
+ throw new Error(`${field} 必须是完整可投递的 canonical target(chat:oc_<id>、thread:oc_<chat>:omt_<thread> 或完整 document-comment locator)`);
54
+ }
55
+ return target;
56
+ }
57
+ function validAnchor(value, field = "message-id", target) {
58
+ const anchor = String(value ?? "").trim();
59
+ const valid = VALID_IM_ANCHOR.test(anchor) || (target?.startsWith("document-comment:") === true && VALID_COMMENT_ANCHOR.test(anchor));
60
+ if (!valid)
61
+ throw new Error(`${field} 必须是有效的 Feishu om_ message_id,或 document-comment source 的 doc_comment_ anchor`);
62
+ return anchor;
63
+ }
41
64
  function createReminderRoutes(options) {
42
65
  const now = options.now ?? Date.now;
43
66
  const timeZone = options.timeZone ?? (() => Intl.DateTimeFormat().resolvedOptions().timeZone);
@@ -54,6 +77,60 @@ function createReminderRoutes(options) {
54
77
  return { ok: false, status: 400, error: "title 不能为空" };
55
78
  const current = now();
56
79
  const tz = typeof body.tz === "string" && body.tz ? body.tz : timeZone();
80
+ const internal = body.internal === true || body.noDelivery === true;
81
+ let deliveryTarget = null;
82
+ let deliveryAnchor = null;
83
+ if (internal) {
84
+ if (body.deliveryTarget !== undefined || body.channel !== undefined || body.msgId !== undefined) {
85
+ return { ok: false, status: 400, error: "internal/no-delivery reminder 不应携带 user-facing delivery target" };
86
+ }
87
+ }
88
+ else {
89
+ try {
90
+ const hasExplicitTarget = body.deliveryTarget !== undefined && body.deliveryTarget !== null && Boolean(String(body.deliveryTarget).trim());
91
+ const hasExplicitChannel = body.channel !== undefined && body.channel !== null && Boolean(String(body.channel).trim());
92
+ // deliveryTarget and channel are destination aliases. Preferring one
93
+ // over a contradicting other could route to the wrong conversation,
94
+ // so a disagreement fails closed instead of being silently resolved.
95
+ if (hasExplicitTarget && hasExplicitChannel
96
+ && userTarget(body.deliveryTarget, "delivery target") !== userTarget(body.channel, "channel")) {
97
+ return { ok: false, status: 400, error: "deliveryTarget 与 channel 指向不同目的地;请只提供其中一个" };
98
+ }
99
+ const explicit = hasExplicitTarget ? body.deliveryTarget : hasExplicitChannel ? body.channel : undefined;
100
+ if (explicit !== undefined)
101
+ deliveryTarget = userTarget(explicit, "delivery target");
102
+ if (body.msgId !== undefined && body.msgId !== null && String(body.msgId).trim()) {
103
+ const anchorTarget = options.resolveMessageTarget?.(String(body.msgId).trim()) || null;
104
+ deliveryAnchor = validAnchor(body.msgId, "message-id", anchorTarget || deliveryTarget);
105
+ if (deliveryTarget && !anchorTarget) {
106
+ return { ok: false, status: 400, error: `message-id ${String(body.msgId).trim()} 无法验证其所属 Inbox target;请省略 anchor 或提供可验证的 anchor` };
107
+ }
108
+ if (!deliveryTarget && !anchorTarget) {
109
+ return { ok: false, status: 400, error: `message-id ${deliveryAnchor} 没有当前 Inbox 的 canonical delivery target` };
110
+ }
111
+ if (anchorTarget) {
112
+ const normalizedAnchorTarget = userTarget(anchorTarget, "message-id target");
113
+ if (deliveryTarget && deliveryTarget !== normalizedAnchorTarget) {
114
+ return { ok: false, status: 400, error: "delivery target 与 message-id 所属 Inbox target 冲突" };
115
+ }
116
+ deliveryTarget = deliveryTarget || normalizedAnchorTarget;
117
+ }
118
+ }
119
+ if (deliveryTarget && (deliveryTarget.startsWith("thread:") || deliveryTarget.startsWith("document-comment:")) && !deliveryAnchor) {
120
+ return { ok: false, status: 400, error: `${deliveryTarget} 必须同时提供可验证的 message-id anchor` };
121
+ }
122
+ if (!deliveryTarget) {
123
+ const source = options.currentInboxSource?.() || null;
124
+ if (!source)
125
+ return { ok: false, status: 400, error: "user-facing reminder 必须显式指定 delivery target,或从当前 Inbox source 派生" };
126
+ deliveryTarget = userTarget(source.deliveryTarget, "current Inbox target");
127
+ deliveryAnchor = validAnchor(source.deliveryAnchor, "current Inbox anchor", deliveryTarget);
128
+ }
129
+ }
130
+ catch (error) {
131
+ return { ok: false, status: 400, error: error.message };
132
+ }
133
+ }
57
134
  let recurrence = null;
58
135
  if (body.repeat !== undefined) {
59
136
  recurrence = ReminderStore.parseRepeat(body.repeat, tz);
@@ -78,14 +155,17 @@ function createReminderRoutes(options) {
78
155
  const reminder = {
79
156
  reminderId: ReminderStore.newId(), ownerAgentId: options.agentId, title,
80
157
  fireAt: ReminderStore.nowIso(fireAtMs), firedAt: null, createdAt: ReminderStore.nowIso(current),
81
- status: "scheduled", msgRef: body.msgId ?? null, msgPermalink: null,
158
+ status: "scheduled", msgRef: deliveryAnchor, msgPermalink: null,
159
+ deliveryTarget, deliveryAnchor, deliveryMode: internal ? "internal" : "user",
82
160
  repeat: body.repeat ?? null, tz: body.repeat !== undefined ? tz : null,
83
161
  channel: body.channel ?? null, payload: body.payload ?? null,
84
162
  version: 1, events: [],
85
163
  };
86
- ReminderStore.appendEvent(reminder, "scheduled", "agent", options.agentId, reminder.fireAt, current);
164
+ ReminderStore.appendEvent(reminder, "scheduled", "agent", options.agentId, reminder.fireAt, current, {
165
+ deliveryTarget, deliveryAnchor, deliveryMode: internal ? "internal" : "user",
166
+ });
87
167
  ReminderStore.mutate(options.stateFile, (store) => store.reminders.push(reminder));
88
- options.log(`reminder 已建 #${reminder.reminderId.slice(0, 8)} fireAt=${reminder.fireAt}${reminder.repeat ? ` repeat=${String(reminder.repeat)}` : ""}`);
168
+ options.log(`reminder 已建 #${reminder.reminderId.slice(0, 8)} fireAt=${reminder.fireAt}${reminder.repeat ? ` repeat=${String(reminder.repeat)}` : ""} deliveryTarget=${deliveryTarget || "none"}`);
89
169
  return { ok: true, status: 200, data: { reminder: ReminderStore.toSummary(reminder), ...(warning ? { warning } : {}) } };
90
170
  }
91
171
  if (method === "GET" && !reminderId) {