@zhushanwen/session-delivery 1.0.0 → 1.1.0

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/index.d.mts CHANGED
@@ -88,13 +88,12 @@ interface DeliveryPort {
88
88
  * 处理);void / `{accepted:true}` = 受理成功。旧实现返回 void 兼容。
89
89
  *
90
90
  * settle 契约:返回 promise 时实现必须 settle(resolve 或 reject)——悬挂的
91
- * promise 会让内核 in-flight 防重永久占位。内核侧按控制面单请求粒度设有界兜底
92
- * (默认 60s,C-proc-19):超时按发送失败收口(warn 留痕 + 走错误重试链)。
93
- * 已接受代价:迟到的原请求在超时强制失败后仍可能到达底层通道,与重试构成重复
94
- * 投递;通道内判重由适配器/对端按裸标记(clientUuid)负责,内核不建第二套判重。
91
+ * promise 会让内核 in-flight 防重永久占位、通路停摆。信任边界内不设防
92
+ * (ADR-0122 范围纪律):本机操作无响应 = 底层已挂,内核不建墙钟兜底,
93
+ * 处置 = 用户显式失败 + 重启应用。
95
94
  */
96
95
  send(msg: DeliveryMessage, intent: DeliveryIntent): Promise<SendReceipt | void> | SendReceipt | void;
97
- /** agent_settled 边沿订阅(D8)。缺省时内核退化退避轮询。返回退订函数。 */
96
+ /** agent_settled 边沿订阅(D8)。缺省时 busy 条目留守至外部 flush/send 触发。返回退订函数。 */
98
97
  subscribeSettled?(cb: () => void): () => void;
99
98
  }
100
99
  /** 内核配置(D4 策略默认值)。 */
@@ -106,13 +105,6 @@ interface DeliveryConfig {
106
105
  /** 合批依赖谓词(D4 must-fix #1)。true 时 send() 走合批窗口,false/缺省时立即投。
107
106
  * 禁止用 isIdle 代替。 */
108
107
  mergeHoldActive?: () => boolean;
109
- /** 退避参数。 */
110
- backoff?: {
111
- ms: number;
112
- max: number;
113
- };
114
- /** watch-dog 复核间隔(ms)。默认 30_000。 */
115
- watchdogMs?: number;
116
108
  /** 去重配置(条数 LRU)。 */
117
109
  dedupe?: {
118
110
  maxKeys: number;
@@ -133,8 +125,12 @@ interface DeliveryConfig {
133
125
  * 锚定在受理时点,后移到送达会让 agent 工具调用阻塞至目标 session 当前 turn 结束
134
126
  * (steer 类车道可达数十秒)。本注释即接口口径权威声明。
135
127
  *
128
+ * outcome 'rejected' = 失败终局通知(ADR-0122 首败即停:send 失败一次即移除条目并
129
+ * 逐条回调;断连事件 failInFlight 转 failed 终态同样逐条回调)。重试决策归消费方,
130
+ * 内核不建自动重试。
131
+ *
136
132
  * 异常契约:实现不应抛——回调异常由内核捕获并 warn 留痕,不影响条目状态机与
137
- * 其余条目的回调(回调异常 ≠ 投递失败,不进错误重试链)。
133
+ * 其余条目的回调(回调异常 ≠ 投递失败,不进失败重试链)。
138
134
  */
139
135
  onSettled?: (msg: DeliveryMessage, outcome: 'delivered' | 'rejected') => void;
140
136
  }
@@ -165,7 +161,8 @@ type DeliveryLane = 'direct' | 'steer' | 'queued';
165
161
  * - in-flight → delivered:送达回执到达(message_end 标记命中 / 适配器确认,D2)
166
162
  * - in-flight → queued:对账回收重投(滞留收回,D3;cancel 部分收回的其余条目同路径)
167
163
  * - in-flight/queued → cancelled:用户撤销(delivery.cancel)
168
- * - in-flight → failed:重试耗尽(sendAttempts 超 backoff.max,既有上限语义保留)
164
+ * - in-flight → failed:断连事件驱动的未确认终局(failInFlight,ADR-0122——pi 连接
165
+ * 断开时结果未知,显式失败交用户处置)
169
166
  * - failed → queued:用户重试(resync 单条重报);failed → cancelled:用户移除
170
167
  * 帧投影四态差异:cancelled 不进 session.delivery 帧(D5③),协议侧帧 state 无此值。
171
168
  */
@@ -187,8 +184,6 @@ interface DeliveryEntry {
187
184
  createdAt: number;
188
185
  /** 最近一次状态迁移时间(epoch ms)。 */
189
186
  updatedAt: number;
190
- /** 已尝试投递次数(重试耗尽判定源:sendAttempts > backoff.max → failed,§3.4)。 */
191
- sendAttempts: number;
192
187
  /** 终态落定时间(epoch ms;delivered/failed/cancelled 时有值,tombstone 提取源)。 */
193
188
  settledAt?: number;
194
189
  }
@@ -198,6 +193,11 @@ interface DeliveryEntry {
198
193
  * cancelled tombstone 防「cancel 确认帧断连窗口丢失 → 已撤销消息被 resync 复活」。
199
194
  * 栖身 runtime 进程内存、不跨 runtime 重启;reattach 场景(判重表已清空)判重锚
200
195
  * 回落 transcript 全量标记扫描(D5②/§3.4)。
196
+ *
197
+ * 语义边界(pi1-disposition-chat-flow D1②):tombstone 语义从「送达事实」扩为
198
+ * 「离开系统的事实」——handled 条目(pi 接管输入等受理即终局形态)复用送达确认
199
+ * 原语写入本记录,终态固定为 delivered(复用既有送达终态,不新造终态形态;对账器
200
+ * 见 tombstone 不重投,继承断线重连 resync 判重防线)。state 联合仅两值不变。
201
201
  */
202
202
  interface DeliveryTombstone {
203
203
  id: string;
@@ -230,17 +230,17 @@ interface DeliveryEntriesProjection {
230
230
  /**
231
231
  * 投递所有权内核实现:createDelivery(v2)。
232
232
  *
233
- * 在 v1 投递循环(入队 → dedupe → 合批窗口 → busy gate → port.send → backoff
234
- * 重试 → settled 边沿/watchdog 驱动)之上升级为条目制所有权内核(设计
235
- * .tmp/tech-design/delivery-ownership-kernel.md §3 D9①②③⑤ / D5②③ / D3,单元 u1)。
236
- * 零 pi 依赖不变:确认/回收由适配器(runtime registry)调 handle 方法驱动。
233
+ * 在 v1 投递循环(入队 → dedupe → 合批窗口 → busy gate → port.send → settled 边沿
234
+ * 驱动)之上升级为条目制所有权内核(设计 .tmp/tech-design/delivery-ownership-kernel.md
235
+ * §3 D9①②③⑤ / D5②③ / D3,单元 u1)。零 pi 依赖不变:确认/回收由适配器(runtime
236
+ * registry)调 handle 方法驱动。
237
237
  *
238
238
  * 五态状态机(D9①,types.ts 迁移表)+ 本文件登记的两处实施扩展:
239
239
  * - queued → in-flight:出站投递被受理(两阶段回执第一阶段,D2)
240
240
  * - in-flight → delivered:confirmDelivered(送达回执,D2)
241
241
  * - in-flight → queued:requeue(对账回收重投,D3)
242
242
  * - queued/failed → cancelled:cancel(本地移除 + tombstone / 用户 × 移除)
243
- * - in-flight → failed:重试耗尽(sendAttempts > backoff.max,v1 语义保留)
243
+ * - in-flight → failed:断连事件驱动的未确认终局(failInFlight,ADR-0122)
244
244
  * - failed → queued:requeue(用户重试/resync 单条重报)
245
245
  * - [扩展①] queued → delivered:confirmDelivered 也接受 queued 态——服务 reattach
246
246
  * 场景 rebuild 直确认重建(D3②:适配器扫描 transcript 判 delivered 后 send 重建
@@ -255,16 +255,26 @@ interface DeliveryEntriesProjection {
255
255
  * 已由底层通道受理——queued → in-flight + 出批 + checked waiter 受理口径 resolve。
256
256
  * 合批分段中途失败时已登记段留守 in-flight(等回执/对账),仅未受理段走失败面。
257
257
  *
258
+ * 失败语义(ADR-0122 首败即停,2026-10-05 用户裁决,backoff 自动重试链退役):
259
+ * port.send 失败(含 accepted:false)一次即收口——checked 条目 reject + 从内核移除;
260
+ * 非 checked 条目 onSettled('rejected') 逐条显式上报 + 从内核移除。重试决策归消费方
261
+ * (人看页面通知、agent 收失败回执),内核只做忠实投递与事实上报。
262
+ *
258
263
  * onSettled 记账口径(D9⑤ 升级):
259
264
  * - 'delivered' = 送达口径:仅 confirmDelivered 驱动回调(受理只转 in-flight,
260
265
  * 不回调——受理 ≠ 送达的机制化落地);per-message 契约(ext-simplify-08)在
261
266
  * confirmDelivered 路径同样成立——对每条消息各回调一次,msg 为该条原始消息
262
267
  * (非 composed 合批消息),additive meta(notifyId 等)随原消息引用原样
263
268
  * 透传到回调,内核不读不改。
264
- * - 'rejected' = 重试耗尽通知(v1 时点不变,判定源 = sendAttempts > max,仅通知)。
269
+ * - 'rejected' = 失败终局通知(send 首败移除 / failInFlight 断连未确认,仅通知)。
265
270
  * - 显式例外:sendChecked 的同步 settle 维持受理口径不变——promise 在 port.send
266
271
  * 受理成功时点 resolve(session_manager send 的 {queued:true} 契约锚,D9⑤ 锁定),
267
- * 与 onSettled 送达口径正交。
272
+ * 与 onSettled 送达口径正交。补充受理口径的第二 settle 路径(F1-11):条目在
273
+ * port.send promise settle 之前已终局 delivered 时(handled 终局路径——适配器在
274
+ * port.send 实现内部先 confirmDelivered 再让 promise settle,finalizeEntry 同步
275
+ * 摘批后 onSendOk 的 settleChecked 只 settle 当前批成员),受理确认随 confirmDelivered
276
+ * 同步了结(resolveWaitersOf)——契约「受理成功即 resolve」对命令条目成立,
277
+ * waiter 不因 settle 时序倒挂滞留。
268
278
  *
269
279
  * 双视图(D9②/D5③):entriesFull() = 全量视图(活跃条目 + 全量 tombstone,对账/
270
280
  * 判重消费);projection() = 投影视图(活跃全量 + delivered 最近 50 条完整条目,
@@ -277,10 +287,12 @@ interface DeliveryEntriesProjection {
277
287
  * id 的消息不参与幂等判重(v1「无 dedupe 配置不去重」语义保持)。
278
288
  *
279
289
  * 其余 v1 机制逐条保持:busy gate(isIdle + 内核在途内查双条件——hasPendingMessages
280
- * 自镜像四件套已按 msg-pipeline-debloat D2 拆除,busy 判定内查 active 表)、backoff
281
- * 有限重试、合批窗口、dedupe LRU、30s watchdog、dispose 语义(丢弃不触发
282
- * onSettled、checked 挂账 reject)。depth() 口径保持 v1 = 尚未受理的消息数
283
- * (受理转 in-flight 后不计;「在途未确认」数经 entriesFull() 全量视图消费)。
290
+ * 自镜像四件套已按 msg-pipeline-debloat D2 拆除,busy 判定内查 active 表)、合批窗口、
291
+ * settled 边沿驱动、dedupe LRU、dispose 语义(丢弃不触发 onSettled、checked 挂账
292
+ * reject)。[ADR-0122 退役登记] backoff 自动重试链、port.send settle 挂死兜底
293
+ * (60s)、watchdog 30s 定时复核、无订阅装配 busy 退避轮询——时间平抑/补偿类机制
294
+ * 全部删除,busy 等待归 settled 边沿与外部触发。depth() 口径保持 v1 = 尚未受理的
295
+ * 消息数(受理转 in-flight 后不计;「在途未确认」数经 entriesFull() 全量视图消费)。
284
296
  */
285
297
 
286
298
  /**
@@ -321,6 +333,19 @@ interface DeliverySubmitOptions {
321
333
  * 'acceptance' 并登记来源清单(ADR-0074 词条)。
322
334
  */
323
335
  receiptAnchor?: 'marker' | 'acceptance';
336
+ /**
337
+ * 无标记条目标志(pi1-disposition-chat-flow D14⑥ 起源;技能/芯片通路并入后为
338
+ * 「出站不注标条目」统一标志——命令 / 手打 skill 与 prompt 模板 / `<taiji-skill>`
339
+ * 芯片注入文本)。true = 条目出站文本不带投递标记、终局凭据不走 message_end
340
+ * 标记回执(pi disposition 响应 / 断连事件驱动,适配器职责)。内核两处行为:
341
+ * - 组批隔离:与普通条目合批会被 buildBatchPayload 以 BATCH_SEP 拼接,pi 命令解析
342
+ * 与适配器全文身份匹配对拼接文本必然失效——批内含无标记条目时只取队首一条单独
343
+ * 成批(isolateUnmarkedEntry)。
344
+ * - 首败即停(ADR-0122 统一语义后不再差异化,全条目同形态)。
345
+ * 'acceptance' 锚条目(agent 通路)出站同样无标记但走合批、以受理即终态,不置
346
+ * 本标志。
347
+ */
348
+ unmarked?: boolean;
324
349
  }
325
350
  /**
326
351
  * send() 受理结果(b31-D6:失败双表面窄口收口)。调用方忽略返回值不受影响
@@ -373,6 +398,13 @@ interface DeliveryHandleV2 extends DeliveryHandle {
373
398
  * 仅未受理段走失败面——已进底层通道的文本不再误报「投递失败」。
374
399
  */
375
400
  confirmAccepted(id: string): boolean;
401
+ /**
402
+ * 断连事件驱动的显式失败终局(ADR-0122 命令终局事件化的断连腿):把全部 in-flight
403
+ * 条目批量转 failed 终态(留守活跃集等用户处置:resync 重试 / cancel 移除),
404
+ * 逐条 onSettled('rejected') 显式上报。queued 条目不触碰(未触达底层通道,随队列
405
+ * 存活,重连后照常投递)。幂等:无 in-flight 条目返回 0。@returns 终态化条数。
406
+ */
407
+ failInFlight(reason: string): number;
376
408
  /**
377
409
  * 回收重置 queued 至队首(保持 ids 相对序,D3 own 处置/failed 重试)。
378
410
  * 只接受 in-flight/failed;queued 幂等跳过(已在队列);cancelled/delivered
package/dist/index.mjs CHANGED
@@ -44,12 +44,9 @@ var LruSet = class {
44
44
  // src/delivery.ts
45
45
  var DEFAULT_CONFIG = {
46
46
  intent: "interrupt-at-turn-boundary",
47
- mergeWindowMs: 0,
48
- backoff: { ms: 100, max: 50 },
49
- watchdogMs: 3e4
47
+ mergeWindowMs: 0
50
48
  };
51
49
  var DEFAULT_DELIVERED_WINDOW = 50;
52
- var PORT_SEND_SETTLE_TIMEOUT_MS = 6e4;
53
50
  function isThenable(v) {
54
51
  return !!v && typeof v.then === "function";
55
52
  }
@@ -81,6 +78,10 @@ function buildBatchPayload(messages) {
81
78
  payload: images.length > 0 ? { kind: "text", content, images } : { kind: "text", content }
82
79
  };
83
80
  }
81
+ function isolateUnmarkedEntry(batch) {
82
+ const firstUnmarked = batch.find((e) => e.unmarked);
83
+ return firstUnmarked !== void 0 ? [firstUnmarked] : batch;
84
+ }
84
85
  function settleChecked(batch, err, checkedPending) {
85
86
  const rejected = /* @__PURE__ */ new Set();
86
87
  if (checkedPending.length === 0) return rejected;
@@ -118,8 +119,7 @@ function resolveWarnSink(config) {
118
119
  function createDelivery(port, config) {
119
120
  const cfg = {
120
121
  ...DEFAULT_CONFIG,
121
- ...config,
122
- backoff: config?.backoff ?? DEFAULT_CONFIG.backoff
122
+ ...config
123
123
  };
124
124
  const active = [];
125
125
  const activeIndex = /* @__PURE__ */ new Map();
@@ -128,11 +128,7 @@ function createDelivery(port, config) {
128
128
  const checkedPending = [];
129
129
  let inflightBatch = [];
130
130
  let inFlight = false;
131
- let sendAttempts = 0;
132
131
  let mergeTimer;
133
- let backoffTimer;
134
- let hangTimer;
135
- let watchdogTimer;
136
132
  let disposed = false;
137
133
  let settledUnsub;
138
134
  let missingKeyWarned = false;
@@ -155,7 +151,6 @@ function createDelivery(port, config) {
155
151
  payload: e.payload,
156
152
  createdAt: e.createdAt,
157
153
  updatedAt: e.updatedAt,
158
- sendAttempts: e.sendAttempts,
159
154
  ...e.settledAt !== void 0 ? { settledAt: e.settledAt } : {}
160
155
  };
161
156
  }
@@ -207,6 +202,7 @@ function createDelivery(port, config) {
207
202
  e.settledAt = ts;
208
203
  tombstones.set(e.id, { id: e.id, state, lane: e.lane, settledAt: ts });
209
204
  if (state === "delivered") {
205
+ resolveWaitersOf(e);
210
206
  deliveredLog.push(e);
211
207
  if (deliveredLog.length > DEFAULT_DELIVERED_WINDOW) deliveredLog.shift();
212
208
  }
@@ -225,6 +221,14 @@ function createDelivery(port, config) {
225
221
  }
226
222
  }
227
223
  }
224
+ function resolveWaitersOf(e) {
225
+ for (let i = checkedPending.length - 1; i >= 0; i--) {
226
+ if (checkedPending[i].entry === e) {
227
+ checkedPending[i].resolve();
228
+ checkedPending.splice(i, 1);
229
+ }
230
+ }
231
+ }
228
232
  function clearMergeTimer() {
229
233
  if (mergeTimer !== void 0) {
230
234
  clearTimeout(mergeTimer);
@@ -242,18 +246,6 @@ function createDelivery(port, config) {
242
246
  clearMergeTimer();
243
247
  armMergeTimer();
244
248
  }
245
- function clearBackoffTimer() {
246
- if (backoffTimer !== void 0) {
247
- clearTimeout(backoffTimer);
248
- backoffTimer = void 0;
249
- }
250
- }
251
- function clearHangTimer() {
252
- if (hangTimer !== void 0) {
253
- clearTimeout(hangTimer);
254
- hangTimer = void 0;
255
- }
256
- }
257
249
  function ensureSettledSub() {
258
250
  if (settledUnsub || !port.subscribeSettled) return;
259
251
  settledUnsub = port.subscribeSettled(() => {
@@ -269,30 +261,12 @@ function createDelivery(port, config) {
269
261
  settledUnsub = void 0;
270
262
  }
271
263
  }
272
- function startWatchdog() {
273
- if (watchdogTimer !== void 0) return;
274
- if (!port.subscribeSettled) return;
275
- watchdogTimer = setInterval(() => {
276
- if (disposed || inFlight) return;
277
- if (!hasQueuedEntries()) return;
278
- if (!isBusySafe()) {
279
- flush();
280
- }
281
- }, cfg.watchdogMs);
282
- }
283
- function stopWatchdog() {
284
- if (watchdogTimer !== void 0) {
285
- clearInterval(watchdogTimer);
286
- watchdogTimer = void 0;
287
- }
288
- }
289
264
  function attemptSend() {
290
265
  if (inflightBatch.some((e) => !inRegistry(e))) {
291
266
  inflightBatch = inflightBatch.filter((e) => inRegistry(e));
292
267
  }
293
268
  if (inflightBatch.length === 0) {
294
269
  inFlight = false;
295
- sendAttempts = 0;
296
270
  pump();
297
271
  return;
298
272
  }
@@ -302,30 +276,9 @@ function createDelivery(port, config) {
302
276
  try {
303
277
  const result = port.send(composed, intent);
304
278
  if (isThenable(result)) {
305
- let settled = false;
306
- clearHangTimer();
307
- hangTimer = setTimeout(() => {
308
- if (settled || disposed) return;
309
- settled = true;
310
- hangTimer = void 0;
311
- warn(
312
- `port.send hung; force-failed after ${PORT_SEND_SETTLE_TIMEOUT_MS}ms (retries may duplicate-deliver; see DeliveryPort.send settle contract)`
313
- );
314
- onSendFail(new Error(`port.send promise did not settle within ${PORT_SEND_SETTLE_TIMEOUT_MS}ms`));
315
- }, PORT_SEND_SETTLE_TIMEOUT_MS);
316
279
  result.then(
317
- (receipt) => {
318
- if (settled) return;
319
- settled = true;
320
- clearHangTimer();
321
- onSendReceipt(receipt);
322
- },
323
- (err) => {
324
- if (settled) return;
325
- settled = true;
326
- clearHangTimer();
327
- onSendFail(err);
328
- }
280
+ (receipt) => onSendReceipt(receipt),
281
+ (err) => onSendFail(err)
329
282
  );
330
283
  } else {
331
284
  onSendReceipt(result);
@@ -346,7 +299,6 @@ function createDelivery(port, config) {
346
299
  const batch = inflightBatch;
347
300
  inFlight = false;
348
301
  inflightBatch = [];
349
- sendAttempts = 0;
350
302
  settleChecked(batch, void 0, checkedPending);
351
303
  let changed = false;
352
304
  for (const e of batch) {
@@ -366,49 +318,24 @@ function createDelivery(port, config) {
366
318
  }
367
319
  function onSendFail(err) {
368
320
  if (disposed) return;
369
- sendAttempts++;
370
- for (const e of inflightBatch) e.sendAttempts = sendAttempts;
371
- const rejected = settleChecked(inflightBatch, err, checkedPending);
372
- if (rejected.size > 0) {
373
- inflightBatch = inflightBatch.filter((e) => !rejected.has(e));
374
- for (const e of rejected) removeActive(e);
375
- notifyChange();
376
- }
377
- if (inflightBatch.length === 0) {
378
- inFlight = false;
379
- inflightBatch = [];
380
- sendAttempts = 0;
381
- warn("port.send failed", err);
382
- pump();
383
- return;
321
+ const batch = inflightBatch;
322
+ inFlight = false;
323
+ inflightBatch = [];
324
+ warn("port.send failed (first-failure stop, no kernel retry)", err);
325
+ const rejected = settleChecked(batch, err, checkedPending);
326
+ let changed = false;
327
+ for (const e of batch) {
328
+ if (!inRegistry(e)) continue;
329
+ if (rejected.has(e)) continue;
330
+ callOnSettled(e.msg, "rejected");
384
331
  }
385
- if (sendAttempts > cfg.backoff.max) {
386
- const failedBatch = inflightBatch;
387
- inFlight = false;
388
- inflightBatch = [];
389
- sendAttempts = 0;
390
- warn("port.send failed after max retries", err);
391
- let changed = false;
392
- for (const e of failedBatch) {
393
- if (disposed) break;
394
- if (!inRegistry(e)) continue;
395
- e.state = "failed";
396
- const ts = now();
397
- e.updatedAt = ts;
398
- e.settledAt = ts;
399
- changed = true;
400
- callOnSettled(e.msg, "rejected");
401
- }
402
- if (changed) notifyChange();
403
- pump();
404
- return;
332
+ for (const e of batch) {
333
+ if (!inRegistry(e)) continue;
334
+ removeActive(e);
335
+ changed = true;
405
336
  }
406
- if (sendAttempts === 1) warn("port.send failed, retrying with backoff", err);
407
- backoffTimer = setTimeout(() => {
408
- backoffTimer = void 0;
409
- if (disposed || !inFlight) return;
410
- attemptSend();
411
- }, cfg.backoff.ms);
337
+ if (changed) notifyChange();
338
+ pump();
412
339
  }
413
340
  function pump() {
414
341
  if (disposed || inFlight) return;
@@ -418,43 +345,29 @@ function createDelivery(port, config) {
418
345
  if (inRegistry(w.entry) && w.entry.state === "queued") batch.push(w.entry);
419
346
  }
420
347
  if (batch.length > 0) {
421
- inflightBatch = batch;
348
+ inflightBatch = isolateUnmarkedEntry(batch);
422
349
  inFlight = true;
423
- sendAttempts = 0;
424
350
  attemptSend();
425
351
  return;
426
352
  }
427
353
  }
428
354
  if (hasQueuedEntries()) {
429
- scheduleFlush(0);
355
+ scheduleFlush();
430
356
  return;
431
357
  }
432
- if (checkedPending.length === 0) stopWatchdog();
433
358
  }
434
359
  function doSend() {
435
360
  if (disposed || inFlight) return;
436
- const batch = active.filter((e) => e.state === "queued");
361
+ const batch = isolateUnmarkedEntry(active.filter((e) => e.state === "queued"));
437
362
  if (batch.length === 0) return;
438
363
  inFlight = true;
439
364
  inflightBatch = batch;
440
- sendAttempts = 0;
441
365
  attemptSend();
442
366
  }
443
- function scheduleFlush(attempt) {
367
+ function scheduleFlush() {
444
368
  if (disposed || !hasQueuedEntries()) return;
445
369
  if (inFlight) return;
446
- clearBackoffTimer();
447
- if (isBusySafe() && attempt < cfg.backoff.max) {
448
- if (port.subscribeSettled) {
449
- startWatchdog();
450
- return;
451
- }
452
- backoffTimer = setTimeout(() => {
453
- backoffTimer = void 0;
454
- scheduleFlush(attempt + 1);
455
- }, cfg.backoff.ms);
456
- return;
457
- }
370
+ if (isBusySafe()) return;
458
371
  doSend();
459
372
  }
460
373
  const warnSink = resolveWarnSink(config);
@@ -487,10 +400,10 @@ function createDelivery(port, config) {
487
400
  payload: msg.payload,
488
401
  createdAt: ts,
489
402
  updatedAt: ts,
490
- sendAttempts: 0,
491
403
  msg,
492
404
  cancelRequested: false,
493
- receiptAnchor: opts?.receiptAnchor ?? "marker"
405
+ receiptAnchor: opts?.receiptAnchor ?? "marker",
406
+ unmarked: opts?.unmarked ?? false
494
407
  };
495
408
  active.push(entry);
496
409
  activeIndex.set(entry.id, entry);
@@ -520,7 +433,7 @@ function createDelivery(port, config) {
520
433
  }
521
434
  clearMergeTimer();
522
435
  ensureSettledSub();
523
- scheduleFlush(0);
436
+ scheduleFlush();
524
437
  return { kind: "accepted", id: entry.id };
525
438
  }
526
439
  async function sendChecked(msg, opts) {
@@ -540,7 +453,6 @@ function createDelivery(port, config) {
540
453
  if (!inFlight) {
541
454
  inflightBatch = [entry];
542
455
  inFlight = true;
543
- sendAttempts = 0;
544
456
  attemptSend();
545
457
  }
546
458
  });
@@ -548,7 +460,7 @@ function createDelivery(port, config) {
548
460
  function flush() {
549
461
  if (disposed) return;
550
462
  clearMergeTimer();
551
- scheduleFlush(0);
463
+ scheduleFlush();
552
464
  }
553
465
  function depth() {
554
466
  let n = 0;
@@ -616,13 +528,12 @@ function createDelivery(port, config) {
616
528
  const idx = active.indexOf(e);
617
529
  if (idx !== -1) active.splice(idx, 1);
618
530
  e.state = "queued";
619
- e.sendAttempts = 0;
620
531
  e.updatedAt = now();
621
532
  e.settledAt = void 0;
622
533
  }
623
534
  active.unshift(...found);
624
535
  notifyChange();
625
- scheduleFlush(0);
536
+ scheduleFlush();
626
537
  return found.length;
627
538
  }
628
539
  function cancel(id) {
@@ -651,17 +562,34 @@ function createDelivery(port, config) {
651
562
  finalizeEntry(e, "cancelled");
652
563
  return { kind: "cancelled", entry: snapshot(e) };
653
564
  }
565
+ function failInFlight(reason) {
566
+ if (disposed) return 0;
567
+ const ts = now();
568
+ let count = 0;
569
+ for (const e of active) {
570
+ if (e.state !== "in-flight") continue;
571
+ const batchIdx = inflightBatch.indexOf(e);
572
+ if (batchIdx !== -1) inflightBatch.splice(batchIdx, 1);
573
+ e.state = "failed";
574
+ e.updatedAt = ts;
575
+ e.settledAt = ts;
576
+ count++;
577
+ callOnSettled(e.msg, "rejected");
578
+ }
579
+ if (count > 0) {
580
+ warn(`failInFlight: ${count} in-flight entry(ies) marked failed (${reason})`);
581
+ notifyChange();
582
+ }
583
+ return count;
584
+ }
654
585
  function drain() {
655
586
  if (disposed) return [];
656
587
  clearMergeTimer();
657
- clearBackoffTimer();
658
- clearHangTimer();
659
588
  const drained = active.slice();
660
589
  active.length = 0;
661
590
  activeIndex.clear();
662
591
  inflightBatch = [];
663
592
  inFlight = false;
664
- sendAttempts = 0;
665
593
  const ts = now();
666
594
  const result = drained.map((e) => {
667
595
  tombstones.set(e.id, { id: e.id, state: "cancelled", lane: e.lane, settledAt: ts });
@@ -671,16 +599,12 @@ function createDelivery(port, config) {
671
599
  w.reject(new DeliveryReclaimError("drained"));
672
600
  }
673
601
  checkedPending.length = 0;
674
- stopWatchdog();
675
602
  if (drained.length > 0) notifyChange();
676
603
  return result;
677
604
  }
678
605
  function dispose() {
679
606
  disposed = true;
680
607
  clearMergeTimer();
681
- clearBackoffTimer();
682
- clearHangTimer();
683
- stopWatchdog();
684
608
  teardownSettledSub();
685
609
  active.length = 0;
686
610
  activeIndex.clear();
@@ -695,7 +619,7 @@ function createDelivery(port, config) {
695
619
  checkedPending.length = 0;
696
620
  dedupSet?.clear();
697
621
  }
698
- return { send, sendChecked, flush, depth, entriesFull, projection, onChange, confirmDelivered, confirmAccepted, requeue, cancel, drain, dispose };
622
+ return { send, sendChecked, flush, depth, entriesFull, projection, onChange, confirmDelivered, confirmAccepted, requeue, cancel, failInFlight, drain, dispose };
699
623
  }
700
624
  export {
701
625
  DeliveryReclaimError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/session-delivery",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Session message delivery kernel: zero-pi-dependency strategy layer for queueing, batching, dedup, and gated flush of messages into pi sessions",
5
5
  "main": "dist/index.mjs",
6
6
  "module": "dist/index.mjs",