@zhushanwen/session-delivery 1.1.0 → 2.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 +376 -85
- package/dist/index.mjs +264 -67
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -6,11 +6,29 @@
|
|
|
6
6
|
* - 意图驱动:调用方声明 intent,内核处理与 session 运行状态的冲突
|
|
7
7
|
* - 判别联合 payload:适配器声明 supportedPayloads 能力
|
|
8
8
|
*
|
|
9
|
-
* v2
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* /
|
|
9
|
+
* v2 增量(投递所有权内核,v2 设计批次——设计文档未入 git,裁决随本仓 commit
|
|
10
|
+
* 历史可溯):
|
|
11
|
+
* 条目状态机/lane/条目双形态(活跃条目 + 终局记录)/entriesFull()+projection() 双视图;行为实现在
|
|
12
|
+
* 本包 src/delivery.ts。lane 字面量中的 'steer' 是投递路径语义名(设计 §1.1 术语表
|
|
13
|
+
* /D1),非 pi API 词汇——intent → 底层参数的翻译在适配器内部,本包只记录投递路径,
|
|
13
14
|
* 零依赖纪律对 lane 不破例。
|
|
15
|
+
*
|
|
16
|
+
* v3 增量(统一消息投递内核,设计 .tmp/tech-design/unified-delivery.md §3.5):
|
|
17
|
+
* - DeliveryStore 持久化存储端口(persist / ack / discard / loadUndelivered /
|
|
18
|
+
* loadTerminalIds / hasTerminal):耐久承诺先于投递尝试;终局写持久终局;判重
|
|
19
|
+
* 第三级查持久记录。两个实现在两半区适配层(runtime 半区磁盘 outbox / port 半区
|
|
20
|
+
* session entries),本包零 fs 零 pi 知识。
|
|
21
|
+
* - 送达凭据三档(DeliveryReceiptAnchor):'marker' / 'acceptance' / 'details-id'
|
|
22
|
+
* (ADR-0074 词条扩展);details-id 的判定逻辑以可注入的凭据检查函数
|
|
23
|
+
* (DeliveryProofCheck)形态承载在本内核。
|
|
24
|
+
* - 失败保留策略(DeliveryFailurePolicy):'forget' 首败即停 / 'retain' 留账重投,
|
|
25
|
+
* 缺省按载荷类型 custom=retain / text=forget。
|
|
26
|
+
* - 判重统一:v2 的 DeliveryTombstone 独立机制与类型退役,判重 = 在途条目 → 内存
|
|
27
|
+
* 已完结 id 索引(loadTerminalIds 播种 + 终局即时登记)→ 持久记录(store.hasTerminal)。
|
|
28
|
+
* entriesFull() 的 tombstones 视图字段与结构保持(双视图消费方零改动),元素类型
|
|
29
|
+
* 由 DeliveryTerminalRecord 承载。
|
|
30
|
+
* - 受理即出站(唯一出站形态,v2 队列持有分支已删):受理后立即 port.send,无
|
|
31
|
+
* 忙闲门禁、无合批窗口(合批归 port 提交侧承载)。
|
|
14
32
|
*/
|
|
15
33
|
/** 投递意图(D3):turn 边界抢占 / run 结束后注入,均含 idle 唤醒语义。 */
|
|
16
34
|
type DeliveryIntent = 'interrupt-at-turn-boundary' | 'after-run';
|
|
@@ -64,23 +82,43 @@ interface DeliveryMessage {
|
|
|
64
82
|
meta?: DeliveryMeta;
|
|
65
83
|
}
|
|
66
84
|
/**
|
|
67
|
-
* port
|
|
85
|
+
* 设计内等待原因(F1-13):port 半区投递端的「注入门禁 hold」——这些拒绝不是失败
|
|
86
|
+
* (host.sendMessage 未调用,条目从未被注入),内核不计首败,条目留守等投递端下一
|
|
87
|
+
* 边沿重新提交。两端唯一契约点:内核消费(onSendReceipt 的 deferred 分流)与 port
|
|
88
|
+
* 产单(`{ accepted: false, deferred }`)共用本词表——新增成员属内核行为变更,不是
|
|
89
|
+
* 端口私货。
|
|
90
|
+
* - 'merge-commit-hold':port 合批提交窗的注入门禁(窗口结束 kernel.flush 重提)。
|
|
91
|
+
* - 'compaction-wait':port compaction 提交侧等待的第二道防线(覆盖提交同步链与
|
|
92
|
+
* compaction 边沿交叠的残余窗口;compaction 终态边沿复位唤醒重提)。
|
|
93
|
+
*/
|
|
94
|
+
type DeliveryDeferReason = 'merge-commit-hold' | 'compaction-wait';
|
|
95
|
+
/**
|
|
96
|
+
* port.send 显式受理回执(U2 回执口径扩展位)。
|
|
68
97
|
*
|
|
69
98
|
* 受理 = 底层通道接受了消息(进入投递路径),**不等于送达事实**——送达判定由
|
|
70
99
|
* 调用方按自身回执锚点确认(如扩展侧扫描 custom_message entry)。旧 port 实现
|
|
71
100
|
* 返回 void 不受影响(void = 受理未知,按成功处理,与既有内核语义一致)。
|
|
101
|
+
*
|
|
102
|
+
* 拒绝分支为判别联合(deferred 与 reason 互斥,MF-1-4):
|
|
103
|
+
* - `{ accepted: false, deferred }` = 设计内等待(见 DeliveryDeferReason)——内核
|
|
104
|
+
* 不计首败,条目留守(onSendDeferred);
|
|
105
|
+
* - `{ accepted: false, reason }` = 显式拒绝(消息未进入任何通道,走错误重试路径,
|
|
106
|
+
* reason 进 warn 日志与 reject 链)。
|
|
72
107
|
*/
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
108
|
+
type SendReceipt = {
|
|
109
|
+
accepted: true;
|
|
110
|
+
} | {
|
|
111
|
+
accepted: false;
|
|
112
|
+
deferred: DeliveryDeferReason;
|
|
113
|
+
} | {
|
|
114
|
+
accepted: false;
|
|
77
115
|
reason?: string;
|
|
78
|
-
}
|
|
116
|
+
};
|
|
79
117
|
/** 内核与外部世界的唯一接口(D2 端口注入)。 */
|
|
80
118
|
interface DeliveryPort {
|
|
81
119
|
/** 本通路支持的 payload kind(D9 fail-fast)。 */
|
|
82
120
|
supportedPayloads: readonly DeliveryPayload['kind'][];
|
|
83
|
-
/** 主 agent
|
|
121
|
+
/** 主 agent 是否空闲(受理即出站下内核不读——port 契约与诊断面保留)。 */
|
|
84
122
|
isIdle(): boolean;
|
|
85
123
|
/**
|
|
86
124
|
* 投递消息(intent → pi 参数的翻译在适配器内部)。
|
|
@@ -93,18 +131,118 @@ interface DeliveryPort {
|
|
|
93
131
|
* 处置 = 用户显式失败 + 重启应用。
|
|
94
132
|
*/
|
|
95
133
|
send(msg: DeliveryMessage, intent: DeliveryIntent): Promise<SendReceipt | void> | SendReceipt | void;
|
|
96
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* agent_settled 边沿订阅(D8):边沿即 flush 触发(retain 失败留守条目的重投时机)。
|
|
136
|
+
* 缺省时留守条目归外部 flush/send 触发。返回退订函数。
|
|
137
|
+
*/
|
|
97
138
|
subscribeSettled?(cb: () => void): () => void;
|
|
98
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* 送达凭据三档(v3,ADR-0074 词条扩展;提交方申报制,缺省 'marker'):
|
|
142
|
+
* - 'marker':出站文本尾附裸标记、送达以回执命中为准(文本投递路径主通路)。
|
|
143
|
+
* - 'acceptance':受理即送达——无回执锚点条目的「受理 = 送达」是唯一可判事实,
|
|
144
|
+
* port.send 受理成功即落 delivered 终态(v1 记账口径 / D9⑤ 显式例外)。
|
|
145
|
+
* - 'details-id'(v3 新增):通知条目送达判定 = 对话记录中出现 details 携带本条目
|
|
146
|
+
* id 的通知条目(deliveryId 由注入端附加,业务字段保持原样)。判定逻辑不在内核
|
|
147
|
+
* ——内核以可注入的凭据检查函数(DeliveryProofCheck)承载函数位与调用点
|
|
148
|
+
* (handle.confirmDeliveredByProof),对话记录读取由装配方实现(零 pi 知识)。
|
|
149
|
+
* 申报 'details-id' 的条目受理后维持 in-flight 等回执(与 'marker' 同形态),
|
|
150
|
+
* 由装配方在回执证据到达时调 confirmDeliveredByProof 驱动终局。
|
|
151
|
+
*/
|
|
152
|
+
type DeliveryReceiptAnchor = 'marker' | 'acceptance' | 'details-id';
|
|
153
|
+
/**
|
|
154
|
+
* 失败保留策略(v3):port.send 失败(首败,ADR-0122 无内核自动重试)时条目的处置。
|
|
155
|
+
* - 'forget':首败即停 + 显式上报后从内核移除(缺省 text 现状;重试决策归
|
|
156
|
+
* 消费方——人看页面通知可重发)。store 已 persist 的记录同步 discard(失败条目
|
|
157
|
+
* 无判重语义,防重启后被 loadUndelivered 复活成死条目)。
|
|
158
|
+
* - 'retain':失败条目留守——内存态转回 queued 等下一 settled 边沿 / 外部 flush
|
|
159
|
+
* 重入队(事件驱动,无定时器);store 记录保留(不 ack 不 discard),重启后经
|
|
160
|
+
* loadUndelivered 重入队(通知投递路径无人值守语义,与账本现状同构)。
|
|
161
|
+
* - 'failed'(chat-flow-consistency 第 11 条):受理失败条目转 **failed 终态留守**——
|
|
162
|
+
* 不自动重投(无 retain 的边沿重投,等用户处置:resync 重试 / cancel 撤销)、
|
|
163
|
+
* 不 store.discard(重启后经 loadUndelivered 重放,重放带 transcript 判重防线),
|
|
164
|
+
* 与断连 failInFlight 的 failed 同态同出路(队列区一行 + 重试入口双呈现)。
|
|
165
|
+
*
|
|
166
|
+
* 缺省按载荷类型:custom = 'retain' / text = 'forget'(§3.4 提交选项缺省值表:
|
|
167
|
+
* 交互在席 vs 无人值守),提交方可显式覆写。sendChecked 挂起条目缺省/显式 'retain'
|
|
168
|
+
* 按 'forget' 处置(同步失败回执契约与 retain 边沿重投矛盾——调用方已收到失败,
|
|
169
|
+
* 自动重投会产生「失败但送达」的矛盾观测;一次性 warn);显式 'failed' 放行——
|
|
170
|
+
* waiter reject 是受理口径通知,failed 留守是条目状态,两者不矛盾(failed 不自动
|
|
171
|
+
* 重投,「失败但送达」观测不存在)。
|
|
172
|
+
*/
|
|
173
|
+
type DeliveryFailurePolicy = 'forget' | 'retain' | 'failed';
|
|
174
|
+
/**
|
|
175
|
+
* 送达凭据检查函数(v3,'details-id' 凭据的判定承载形态):判定「对话记录中是否已
|
|
176
|
+
* 存在 details 携带该 id 的通知条目」。由装配方注入实现(实现读对话记录——pi 半区
|
|
177
|
+
* 扫 session entries;内核零 pi 零 fs 知识,只持有函数位与调用点)。同步判定:
|
|
178
|
+
* 装配方内部缓存扫描结果时恒 O(1);未注入时 details-id 条目无法确认送达(留守)。
|
|
179
|
+
*/
|
|
180
|
+
type DeliveryProofCheck = (id: string) => boolean;
|
|
181
|
+
/**
|
|
182
|
+
* DeliveryStore 的持久记录(persist 的写入单元 / loadUndelivered 的读取单元):
|
|
183
|
+
* 重建投递所需的最小信息快照。字段是内核条目的持久化派生数据——重启重入队时装配方经
|
|
184
|
+
* loadUndelivered 取回后按原 id / lane / receiptAnchor 重新提交(幂等判重锚不丢)。
|
|
185
|
+
*/
|
|
186
|
+
interface DeliveryStoreRecord {
|
|
187
|
+
/** 客户端幂等 id(= 条目 id)。 */
|
|
188
|
+
id: string;
|
|
189
|
+
/** 投递路径(重入队时按原路径重报)。 */
|
|
190
|
+
lane: DeliveryLane;
|
|
191
|
+
/** 载荷(原样)。 */
|
|
192
|
+
payload: DeliveryPayload;
|
|
193
|
+
/** 期望注入时机(persist 时按 msg.intent ?? config.intent 解析定型)。 */
|
|
194
|
+
intent: DeliveryIntent;
|
|
195
|
+
/** 记录写入时间(epoch ms)。 */
|
|
196
|
+
recordedAt: number;
|
|
197
|
+
/** 送达凭据申报(重入队时保真重报)。 */
|
|
198
|
+
receiptAnchor?: DeliveryReceiptAnchor;
|
|
199
|
+
/** 无标记出站条目标志(重入队时保真重报,组批隔离判定源)。 */
|
|
200
|
+
unmarked?: boolean;
|
|
201
|
+
/** additive meta(原样透传债权等消费方字段,重入队时保真重报)。 */
|
|
202
|
+
meta?: DeliveryMeta;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 持久化存储端口(v3,统一语言词条「持久化存储端口」):内核与耐久面的唯一接口。
|
|
206
|
+
* 两个实现在两半区适配层(本包零 fs 零 pi 知识):runtime 半区 = 磁盘 outbox 文件
|
|
207
|
+
* (文本投递路径,波 4 接线);port 半区 = session entry 读写(通知投递路径,波 2 接线)。
|
|
208
|
+
*
|
|
209
|
+
* 内核行为契约(设计 §3.5.1):
|
|
210
|
+
* - persist:send 受理前调用(耐久承诺先于投递尝试,与账本 record 先于 deliver 同
|
|
211
|
+
* 律)。persist 抛错 = 承诺未落 → 内核拒收该条目(send 返回 persist-failed /
|
|
212
|
+
* sendChecked 同步 throw),不进入投递流程。
|
|
213
|
+
* - ack:终局 delivered / cancelled 后调用(写持久终局——已撤销消息的防复活凭据
|
|
214
|
+
* 同样在持久终局记录里)。ack 抛错不回滚状态机(终局事实已发生),warn 留痕。
|
|
215
|
+
* - discard:forget 失败条目的记录无痕移除(无终局无判重语义——区别于 ack,不进
|
|
216
|
+
* 终局记录,防重启后 loadUndelivered 把已弃条目当未投递复活)。
|
|
217
|
+
* - loadUndelivered:供重启 / 重放时重入队(装配方启动时取回未终局记录重新提交)。
|
|
218
|
+
* - loadTerminalIds:启动 / 重放时构建已完结 id 内存索引(判重第二级的播种源,
|
|
219
|
+
* 对齐账本 ackedIds 播种语义)。
|
|
220
|
+
* - hasTerminal:判重第三级「查持久记录」的按 id 查询(内存索引 miss 时的持久
|
|
221
|
+
* 兜底——宁可误吞不误重投)。实现方自负缓存(如读盘一次建 Set)。
|
|
222
|
+
*
|
|
223
|
+
* 同步签名纪律:persist / ack / discard / hasTerminal 在内核热路径上,同步语义使
|
|
224
|
+
* 「承诺先于尝试」构造性成立(调用返回 = 动作已落);loadUndelivered / loadTerminalIds
|
|
225
|
+
* 在启动 / 重放路径,装配方在装配内核前同步加载即可。异步介质由实现方内部缓冲,
|
|
226
|
+
* 不进端口签名。
|
|
227
|
+
*/
|
|
228
|
+
interface DeliveryStore {
|
|
229
|
+
/** 受理前写入记录(耐久承诺先于投递尝试)。抛错 = 承诺未写入,内核拒收。 */
|
|
230
|
+
persist(record: DeliveryStoreRecord): void;
|
|
231
|
+
/** 终局(delivered / cancelled)后写持久终局(从待投差集移除、进终局记录)。 */
|
|
232
|
+
ack(id: string): void;
|
|
233
|
+
/** forget 失败条目的记录无痕移除(无终局形态,区别于 ack)。 */
|
|
234
|
+
discard(id: string): void;
|
|
235
|
+
/** 未终局记录全量(重启 / 重放重入队的读取源)。 */
|
|
236
|
+
loadUndelivered(): readonly DeliveryStoreRecord[];
|
|
237
|
+
/** 已完结 id 全量(启动 / 重放时构建内存索引的播种源)。 */
|
|
238
|
+
loadTerminalIds(): readonly string[];
|
|
239
|
+
/** 持久终局按 id 查询(判重第三级)。 */
|
|
240
|
+
hasTerminal(id: string): boolean;
|
|
241
|
+
}
|
|
99
242
|
/** 内核配置(D4 策略默认值)。 */
|
|
100
243
|
interface DeliveryConfig {
|
|
101
244
|
/** 默认意图:'interrupt-at-turn-boundary'(D3)。 */
|
|
102
245
|
intent?: DeliveryIntent;
|
|
103
|
-
/** 合批窗口(ms):0 = 关;>0 = 滑动窗口合批。 */
|
|
104
|
-
mergeWindowMs?: number;
|
|
105
|
-
/** 合批依赖谓词(D4 must-fix #1)。true 时 send() 走合批窗口,false/缺省时立即投。
|
|
106
|
-
* 禁止用 isIdle 代替。 */
|
|
107
|
-
mergeHoldActive?: () => boolean;
|
|
108
246
|
/** 去重配置(条数 LRU)。 */
|
|
109
247
|
dedupe?: {
|
|
110
248
|
maxKeys: number;
|
|
@@ -123,7 +261,7 @@ interface DeliveryConfig {
|
|
|
123
261
|
* (受理 ≠ 送达——SendReceipt 注释口径的机制化落地)。**显式例外:sendChecked 的
|
|
124
262
|
* 同步 settle 维持受理口径不变**——session_manager send 的「立即受理确认」契约
|
|
125
263
|
* 锚定在受理时点,后移到送达会让 agent 工具调用阻塞至目标 session 当前 turn 结束
|
|
126
|
-
* (steer
|
|
264
|
+
* (steer 类投递路径可达数十秒)。本注释即接口口径权威声明。
|
|
127
265
|
*
|
|
128
266
|
* outcome 'rejected' = 失败终局通知(ADR-0122 首败即停:send 失败一次即移除条目并
|
|
129
267
|
* 逐条回调;断连事件 failInFlight 转 failed 终态同样逐条回调)。重试决策归消费方,
|
|
@@ -133,13 +271,23 @@ interface DeliveryConfig {
|
|
|
133
271
|
* 其余条目的回调(回调异常 ≠ 投递失败,不进失败重试链)。
|
|
134
272
|
*/
|
|
135
273
|
onSettled?: (msg: DeliveryMessage, outcome: 'delivered' | 'rejected') => void;
|
|
274
|
+
/**
|
|
275
|
+
* 持久化存储端口(v3)。缺省 undefined = 无耐久面(v2 行为:纯内存条目集)——
|
|
276
|
+
* 既有 runtime 装配不传 = 逐字节不变(波 1 零行为变化锚)。提供时内核行为契约见
|
|
277
|
+
* DeliveryStore(受理前 persist / 终局 ack / forget 失败 discard / 构造期
|
|
278
|
+
* loadTerminalIds 播种判重索引)。
|
|
279
|
+
*/
|
|
280
|
+
store?: DeliveryStore;
|
|
281
|
+
/**
|
|
282
|
+
* 送达凭据检查函数(v3):'details-id' 凭据条目的送达判定承载(见 DeliveryProofCheck)。
|
|
283
|
+
* 缺省 undefined = details-id 条目无法经 confirmDeliveredByProof 确认(留守 in-flight)。
|
|
284
|
+
*/
|
|
285
|
+
proofCheck?: DeliveryProofCheck;
|
|
136
286
|
}
|
|
137
287
|
/** 投递句柄(createDelivery 返回)。 */
|
|
138
288
|
interface DeliveryHandle {
|
|
139
|
-
/** 唯一常规入口(D4
|
|
140
|
-
send(msg: DeliveryMessage
|
|
141
|
-
merge?: boolean;
|
|
142
|
-
}): void;
|
|
289
|
+
/** 唯一常规入口(D4 入口收敛);受理即出站立即投。 */
|
|
290
|
+
send(msg: DeliveryMessage): void;
|
|
143
291
|
/** 入队 + 可达性同步确认。reject = 入队失败。 */
|
|
144
292
|
sendChecked(msg: DeliveryMessage): Promise<void>;
|
|
145
293
|
/** 强制投递尝试(shutdown flush / 外部重触发 / settled 边沿内部复用)。 */
|
|
@@ -150,8 +298,8 @@ interface DeliveryHandle {
|
|
|
150
298
|
dispose(): void;
|
|
151
299
|
}
|
|
152
300
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
301
|
+
* 投递路径(D1)。lane 在条目创建时由 runtime 内核适配层判定(读权威 occupancy
|
|
302
|
+
* 派生视图 + 内核自身队列态,单一判定源)后记录,此后不变。三档为投递路径语义名
|
|
155
303
|
* (设计 §1.1 术语表),非底层通道 API 词汇。
|
|
156
304
|
*/
|
|
157
305
|
type DeliveryLane = 'direct' | 'steer' | 'queued';
|
|
@@ -163,8 +311,13 @@ type DeliveryLane = 'direct' | 'steer' | 'queued';
|
|
|
163
311
|
* - in-flight/queued → cancelled:用户撤销(delivery.cancel)
|
|
164
312
|
* - in-flight → failed:断连事件驱动的未确认终局(failInFlight,ADR-0122——pi 连接
|
|
165
313
|
* 断开时结果未知,显式失败交用户处置)
|
|
314
|
+
* - queued → failed:出站投递受理失败(onFailure='failed',chat-flow-consistency
|
|
315
|
+
* 第 11 条——文本投递受理失败留守 failed 行,与断连未确认同出路,等用户处置)
|
|
316
|
+
* - failed → delivered:确定性送达事实(resync transcript 预检命中 / 迟到送达回执,
|
|
317
|
+
* 第 11 条歧义性终结——首败时文本可能已进 transcript,delivered 事实终结 failed
|
|
318
|
+
* 猜测,行随事实消失)
|
|
166
319
|
* - failed → queued:用户重试(resync 单条重报);failed → cancelled:用户移除
|
|
167
|
-
*
|
|
320
|
+
* 帧派生视图四态差异:cancelled 不进 session.delivery 帧(D5③),协议侧帧 state 无此值。
|
|
168
321
|
*/
|
|
169
322
|
type DeliveryEntryState = 'queued' | 'in-flight' | 'delivered' | 'failed' | 'cancelled';
|
|
170
323
|
/**
|
|
@@ -176,7 +329,7 @@ interface DeliveryEntry {
|
|
|
176
329
|
/** 客户端幂等 id(= 协议层 clientUuid;裸标记身份源 D2,判重锚 D5②)。 */
|
|
177
330
|
id: string;
|
|
178
331
|
state: DeliveryEntryState;
|
|
179
|
-
/**
|
|
332
|
+
/** 投递路径(创建时判定后记录,不变,D1)。 */
|
|
180
333
|
lane: DeliveryLane;
|
|
181
334
|
/** 载荷(一期仅 text,DeliveryPayload 联合保留扩展位)。 */
|
|
182
335
|
payload: DeliveryPayload;
|
|
@@ -188,39 +341,44 @@ interface DeliveryEntry {
|
|
|
188
341
|
settledAt?: number;
|
|
189
342
|
}
|
|
190
343
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
344
|
+
* 终局记录(v3):delivered 与 cancelled 的轻量元数据——条目离开系统的事实的内存
|
|
345
|
+
* 派生数据。v2 的 DeliveryTombstone 独立判重机制与类型随 v3 退役(它像独立机制只因没有
|
|
346
|
+
* 持久存储):判重统一为「在途条目 → 内存已完结 id 索引(store.loadTerminalIds 播种
|
|
347
|
+
* + 终局即时登记)→ 持久记录(store.hasTerminal)」三级(设计 §3.5.5),终态唯一
|
|
348
|
+
* 事实源 = 投递日志(DeliveryStore 终局记录);本类型只承载条目视图面——
|
|
349
|
+
* entriesFull().tombstones 视图字段与结构保持不变(双视图消费方零改动,波 1 零行为
|
|
350
|
+
* 变化锚),cancel 的 already-final 结果同样由它承载。
|
|
196
351
|
*
|
|
197
|
-
* 语义边界(pi1-disposition-chat-flow D1
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* 见 tombstone 不重投,继承断线重连 resync 判重防线)。state 联合仅两值不变。
|
|
352
|
+
* 语义边界(pi1-disposition-chat-flow D1②):终局语义 = 「离开系统的事实」——
|
|
353
|
+
* handled 条目(pi 接管输入等受理即终局形态)复用送达确认原语写入,终态固定为
|
|
354
|
+
* delivered。state 联合仅两值不变。
|
|
201
355
|
*/
|
|
202
|
-
interface
|
|
356
|
+
interface DeliveryTerminalRecord {
|
|
203
357
|
id: string;
|
|
204
|
-
/**
|
|
358
|
+
/** 仅两终态(活跃态不产终局记录)。 */
|
|
205
359
|
state: Extract<DeliveryEntryState, 'delivered' | 'cancelled'>;
|
|
206
360
|
lane: DeliveryLane;
|
|
207
361
|
/** 终态落定时间(epoch ms)。 */
|
|
208
362
|
settledAt: number;
|
|
209
363
|
}
|
|
210
364
|
/**
|
|
211
|
-
* 全量视图(D9②):对账器与判重消费。活跃条目(完整字段)+
|
|
365
|
+
* 全量视图(D9②):对账器与判重消费。活跃条目(完整字段)+ 全部终局记录。
|
|
212
366
|
* 消费方:Reconciler 判别在途集(runtime registry 侧)、resync 判重、reattach 收养判定。
|
|
213
|
-
* 访问入口 = handle.entriesFull()(MF-1-13
|
|
367
|
+
* 访问入口 = handle.entriesFull()(MF-1-13 拆名:与派生视图分开命名,误用编译期可查)。
|
|
368
|
+
*
|
|
369
|
+
* tombstones 字段口径(v3):本 handle 生命周期内终局条目的完整记录(含 restart 前
|
|
370
|
+
* 条目的判重不依赖它——重启前的已完结 id 由 store.loadTerminalIds 播种进判重索引,
|
|
371
|
+
* 判重正确性不依赖本视图的全量性)。字段名与结构保持 v2 形态(双视图消费方零改动)。
|
|
214
372
|
*/
|
|
215
373
|
interface DeliveryEntriesFull {
|
|
216
374
|
active: readonly DeliveryEntry[];
|
|
217
|
-
tombstones: readonly
|
|
375
|
+
tombstones: readonly DeliveryTerminalRecord[];
|
|
218
376
|
}
|
|
219
377
|
/**
|
|
220
|
-
*
|
|
378
|
+
* 派生视图(D9②/D5③):session.delivery 帧装配用。活跃条目(queued/in-flight/failed)
|
|
221
379
|
* 全量 + delivered 最近 50 条完整条目;稳态体积有界 ≤ 50 + 活跃条目数。
|
|
222
380
|
* 装配(帧 DTO 转换)归 runtime transport 侧(u3a),内核只产出本视图。
|
|
223
|
-
* 访问入口 = handle.projection()
|
|
381
|
+
* 访问入口 = handle.projection();派生视图窗口恒为默认常量 50,无参数位(与全量视图
|
|
224
382
|
* entriesFull() 拆名为两个具名方法——双视图误用编译期即红,不靠注释纪律维持)。
|
|
225
383
|
*/
|
|
226
384
|
interface DeliveryEntriesProjection {
|
|
@@ -230,22 +388,25 @@ interface DeliveryEntriesProjection {
|
|
|
230
388
|
/**
|
|
231
389
|
* 投递所有权内核实现:createDelivery(v2)。
|
|
232
390
|
*
|
|
233
|
-
* 在 v1
|
|
234
|
-
*
|
|
235
|
-
* §3 D9①②③⑤ / D5②③ / D3,单元 u1)。零 pi 依赖不变:确认/回收由适配器(runtime
|
|
391
|
+
* 在 v1 投递循环之上升级为条目制所有权内核(v2 设计批次 D9①②③⑤ / D5②③ / D3,单元 u1——
|
|
392
|
+
* 设计文档未入 git,裁决随本仓 commit 历史可溯)。零 pi 依赖不变:确认/回收由适配器(runtime
|
|
236
393
|
* registry)调 handle 方法驱动。
|
|
237
394
|
*
|
|
238
|
-
* 五态状态机(D9①,types.ts 迁移表)+
|
|
395
|
+
* 五态状态机(D9①,types.ts 迁移表)+ 本文件登记的实施扩展:
|
|
239
396
|
* - queued → in-flight:出站投递被受理(两阶段回执第一阶段,D2)
|
|
240
397
|
* - in-flight → delivered:confirmDelivered(送达回执,D2)
|
|
241
398
|
* - in-flight → queued:requeue(对账回收重投,D3)
|
|
242
399
|
* - queued/failed → cancelled:cancel(本地移除 + tombstone / 用户 × 移除)
|
|
243
400
|
* - in-flight → failed:断连事件驱动的未确认终局(failInFlight,ADR-0122)
|
|
401
|
+
* - queued → failed:出站投递受理失败留守(onFailure='failed',chat-flow-consistency
|
|
402
|
+
* 第 11 条——与断连未确认同出路,等用户处置)
|
|
244
403
|
* - failed → queued:requeue(用户重试/resync 单条重报)
|
|
245
|
-
* - [扩展①] queued → delivered:confirmDelivered 也接受 queued
|
|
246
|
-
* 场景 rebuild 直确认重建(D3②:适配器扫描 transcript
|
|
247
|
-
* + confirmDelivered
|
|
248
|
-
*
|
|
404
|
+
* - [扩展①] queued/failed → delivered:confirmDelivered 也接受 queued 与 failed
|
|
405
|
+
* 态——queued 服务 reattach 场景 rebuild 直确认重建(D3②:适配器扫描 transcript
|
|
406
|
+
* 判 delivered 后 send 重建 + confirmDelivered 一步落终态),以及出站批次在途时
|
|
407
|
+
* 送达回执先于受理回执到达的竞态形态(message_end 比 RPC resolve 快,事实优先);
|
|
408
|
+
* failed 服务第 11 条受理失败的歧义性终结——迟到的送达回执 / resync 的 transcript
|
|
409
|
+
* 预检命中即确定性送达事实,行随事实消失。
|
|
249
410
|
* - [扩展②] cancel 对 in-flight 两段式:首次调用只标记待收回(条目留守 in-flight,
|
|
250
411
|
* 返回 reclaim-requested,适配器驱动 clear_queue 收回);适配器收回确认后再次
|
|
251
412
|
* cancel(id) 终结为 cancelled + tombstone。收回失败不再调 → 条目留守原态
|
|
@@ -253,12 +414,17 @@ interface DeliveryEntriesProjection {
|
|
|
253
414
|
* 对已标记条目事实优先转 delivered(delivered 事实 > cancel 意图)。
|
|
254
415
|
* - [扩展③] confirmAccepted 受理登记(分段 port 实现用,dmg-r1-4):申报批内条目
|
|
255
416
|
* 已由底层通道受理——queued → in-flight + 出批 + checked waiter 受理口径 resolve。
|
|
256
|
-
* 合批分段中途失败时已登记段留守 in-flight
|
|
417
|
+
* 合批分段中途失败时已登记段留守 in-flight(等回执/对账),仅未受理段按失败处理。
|
|
257
418
|
*
|
|
258
419
|
* 失败语义(ADR-0122 首败即停,2026-10-05 用户裁决,backoff 自动重试链退役):
|
|
259
|
-
* port.send 失败(含 accepted:false
|
|
260
|
-
*
|
|
261
|
-
*
|
|
420
|
+
* port.send 失败(含 accepted:false)一次即收尾——处置按条目 onFailure 策略分流:
|
|
421
|
+
* forget 条目 checked reject + 从内核移除;非 checked forget 条目 onSettled('rejected')
|
|
422
|
+
* 逐条显式上报 + 从内核移除;failed 条目(chat-flow-consistency 第 11 条)转 failed
|
|
423
|
+
* 终态留守(不自动重投,等用户处置:resync 重试 / cancel 撤销),checked waiter
|
|
424
|
+
* reject 照常(受理口径通知与条目状态正交)。唯一例外:拒绝回执为 deferred 形态
|
|
425
|
+
* (设计内等待,DeliveryDeferReason——如 port 合批提交窗 hold)不计首败,条目留守
|
|
426
|
+
* 等投递端下一边沿重提(onSendDeferred)。重试决策归消费方(人看页面通知、agent
|
|
427
|
+
* 收失败回执),内核只做忠实投递与事实上报。
|
|
262
428
|
*
|
|
263
429
|
* onSettled 记账口径(D9⑤ 升级):
|
|
264
430
|
* - 'delivered' = 送达口径:仅 confirmDelivered 驱动回调(受理只转 in-flight,
|
|
@@ -276,22 +442,51 @@ interface DeliveryEntriesProjection {
|
|
|
276
442
|
* 同步了结(resolveWaitersOf)——契约「受理成功即 resolve」对命令条目成立,
|
|
277
443
|
* waiter 不因 settle 时序倒挂滞留。
|
|
278
444
|
*
|
|
279
|
-
* 双视图(D9②/D5③):entriesFull() = 全量视图(活跃条目 +
|
|
280
|
-
* 判重消费);projection() =
|
|
281
|
-
* cancelled
|
|
282
|
-
* =
|
|
445
|
+
* 双视图(D9②/D5③):entriesFull() = 全量视图(活跃条目 + 全量终局记录,对账/
|
|
446
|
+
* 判重消费);projection() = 派生视图(活跃全量 + delivered 最近 50 条完整条目,
|
|
447
|
+
* cancelled 不进派生视图;窗口恒为默认常量)。delivered 完整条目留存上限
|
|
448
|
+
* = 默认派生视图窗口 50(终局记录全量保留不受限——判重正确性不依赖展示窗口)。
|
|
449
|
+
*
|
|
450
|
+
* 判重(v3 统一:在途条目 → 内存已完结 id 索引 → 持久记录,设计 unified-delivery
|
|
451
|
+
* §3.5.5):resync 重报/重建以显式 id 查三级判重链去重(send/sendChecked 的 opts.id
|
|
452
|
+
* 命中即吞)。一级 = 活跃集(activeIndex,含 queued/in-flight/failed);二级 = 内存
|
|
453
|
+
* 已完结 id 索引(terminalIndex:store.loadTerminalIds 启动播种 + 终局即时登记);
|
|
454
|
+
* 三级 = 持久记录(store.hasTerminal,索引 miss 的持久兜底)。v2 的 tombstone 独立
|
|
455
|
+
* 判重机制与 DeliveryTombstone 类型退役;已撤销消息的防复活凭据 = 撤销时写持久
|
|
456
|
+
* 终局(store.ack)。条目 id = opts.id ?? 内部生成;未传 id 的消息不参与幂等判重
|
|
457
|
+
* (v1「无 dedupe 配置不去重」语义保持)。
|
|
283
458
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
459
|
+
* v3 增量(统一消息投递内核,设计 unified-delivery §3.5):
|
|
460
|
+
* - DeliveryStore 端口(config.store):send/sendChecked 受理前先 persist(耐久承诺
|
|
461
|
+
* 先于投递尝试;persist 抛错 = 承诺未落 → 拒收 rejected('persist-failed'),条目
|
|
462
|
+
* 回滚不入队不投递);终局 delivered / cancelled 后 ack(finalizeEntry / drain);
|
|
463
|
+
* forget 失败条目 discard(无终局语义的记录无痕移除,防重启复活)。ack / 判重
|
|
464
|
+
* 查询的 store 异常降级 warn 留痕、状态机照常(终局事实已发生 / 探测异常降级
|
|
465
|
+
* 保守放行);persist 异常 fail-fast 拒收(承诺语义)。
|
|
466
|
+
* - 送达凭据三档(receiptAnchor):'details-id' 与 'marker' / 'acceptance' 并列
|
|
467
|
+
* (ADR-0074 词条扩展)。details-id 条目受理后维持 in-flight 等回执(同 marker);
|
|
468
|
+
* 判定逻辑以可注入凭据检查函数(config.proofCheck)承载,调用点 =
|
|
469
|
+
* handle.confirmDeliveredByProof(id)——装配方在回执证据到达时调用,内核按条目
|
|
470
|
+
* 凭据档位分派(非 details-id 条目一律拒绝,防误确认)。
|
|
471
|
+
* - 失败保留策略(opts.onFailure:'forget' | 'retain' | 'failed',缺省 custom=retain /
|
|
472
|
+
* text=forget):retain 条目首败后内存态转回 queued 留守(下一 settled 边沿 /
|
|
473
|
+
* 外部 flush 重入队,事件驱动无定时器;失败轮不主动重投——防 idle 同步失败循环),
|
|
474
|
+
* store 记录保留(重启经 loadUndelivered 重入队);forget 条目维持现状首败即停;
|
|
475
|
+
* failed 条目转 failed 终态留守(无自动重投,等用户 resync 重试 / cancel 撤销——
|
|
476
|
+
* chat-flow-consistency 第 11 条)。sendChecked 挂起条目缺省/显式 retain 按 forget
|
|
477
|
+
* 处置(同步失败回执契约与 retain 重投矛盾);显式 failed 放行(reject 是受理口径
|
|
478
|
+
* 通知,failed 留守是条目状态,不自动重投故无矛盾观测)。
|
|
479
|
+
* - 受理即出站(v3,唯一出站形态):受理后立即 port.send(忙闲门禁旁路——承重
|
|
480
|
+
* 前提 = 底层唯一队列忙时不拒,适配层保证);无合批窗口(队列唯一化后无组批
|
|
481
|
+
* 时机,合批归 port 提交侧承载);in-flight 防重保留(内核自身并发保护);
|
|
482
|
+
* retain 失败留守条目经 settled 边沿 / 外部 flush 重投(事件驱动,无定时器)。
|
|
483
|
+
* v2 队列持有形态(busy gate 排队 + 合批窗口)已随队列持有分支退役删除。
|
|
288
484
|
*
|
|
289
|
-
* 其余 v1 机制逐条保持:
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* reject)。[ADR-0122 退役登记] backoff 自动重试链、port.send settle 挂死兜底
|
|
485
|
+
* 其余 v1/v2 机制逐条保持:settled 边沿驱动(send / flush / 边沿统一出站触发)、
|
|
486
|
+
* dedupe LRU、dispose 语义(丢弃不触发 onSettled、checked 挂起 reject)。
|
|
487
|
+
* [ADR-0122 退役登记] backoff 自动重试链、port.send settle 挂死兜底
|
|
293
488
|
* (60s)、watchdog 30s 定时复核、无订阅装配 busy 退避轮询——时间平抑/补偿类机制
|
|
294
|
-
*
|
|
489
|
+
* 全部删除。depth() 口径保持 v1 = 尚未受理的
|
|
295
490
|
* 消息数(受理转 in-flight 后不计;「在途未确认」数经 entriesFull() 全量视图消费)。
|
|
296
491
|
*/
|
|
297
492
|
|
|
@@ -307,32 +502,50 @@ interface DeliveryWarnSink {
|
|
|
307
502
|
}
|
|
308
503
|
/** createDelivery 配置(DeliveryConfig + warn 出口注入)。 */
|
|
309
504
|
type DeliveryConfigWithWarn = DeliveryConfig & DeliveryWarnSink;
|
|
310
|
-
/** send/sendChecked
|
|
505
|
+
/** send/sendChecked 提交选项。 */
|
|
311
506
|
interface DeliverySubmitOptions {
|
|
312
|
-
/** 合批窗口判定覆盖(v1 既有)。 */
|
|
313
|
-
merge?: boolean;
|
|
314
507
|
/**
|
|
315
508
|
* 客户端幂等 id(= 条目 id = 协议层 clientUuid,D2 裸标记身份源/D5② 判重锚)。
|
|
316
509
|
* 显式提供时参与幂等判重:命中活跃条目或 tombstone 即吞(resync 重报/重放去重)。
|
|
317
510
|
* 缺省内部生成、不参与判重。
|
|
318
511
|
*/
|
|
319
512
|
id?: string;
|
|
320
|
-
/**
|
|
513
|
+
/** 投递路径(D1):适配器判定后传入记录;缺省 'direct'(既有调用方语义)。 */
|
|
321
514
|
lane?: DeliveryLane;
|
|
322
515
|
/**
|
|
323
|
-
* 送达回执锚申报(msg-pipeline-debloat D1
|
|
516
|
+
* 送达回执锚申报(msg-pipeline-debloat D1 申报制;v3 三档,ADR-0074 词条扩展):
|
|
517
|
+
* 提交方声明本条目的送达判定锚。
|
|
324
518
|
* - 'marker'(缺省)= 出站文本尾附裸标记、送达以 message_end 回执命中为准
|
|
325
519
|
* (用户消息主通路语义);受理后维持 in-flight 等回执。
|
|
326
520
|
* - 'acceptance' = 无回执锚点条目(agent 通路:完成回流 / session_manager send /
|
|
327
521
|
* notifier 通知——出站文本原样不改),port.send 受理成功即落 delivered 终态
|
|
328
522
|
* (「受理 = 送达」是唯一可判事实,v1 记账口径 / D9⑤ 显式例外)。
|
|
523
|
+
* - 'details-id'(v3)= 通知条目凭据(deliveryId 由注入端写入 details,业务字段
|
|
524
|
+
* 保持原样):送达判定 = 对话记录中出现 details 携带本条目 id 的通知条目;受理
|
|
525
|
+
* 后维持 in-flight 等回执(同 'marker'),终局由装配方调 handle.confirmDeliveredByProof
|
|
526
|
+
* 驱动(判定逻辑经 config.proofCheck 注入,内核零 pi 知识)。
|
|
329
527
|
*
|
|
330
|
-
|
|
331
|
-
|
|
528
|
+
* 缺省 'marker' 是刻意的保守取向:无标记提交点漏申报的代价 = 复发已知死锁形态
|
|
529
|
+
* (in-flight 永挂——条目无人确认终态,有全链诊断可查,可见);误申报 'acceptance' 的代价
|
|
332
530
|
* = 静默丢送达确认语义(更隐蔽)。两害取可见者。新增无标记投递来源必须显式申报
|
|
333
|
-
* 'acceptance' 并登记来源清单(ADR-0074 词条)。
|
|
531
|
+
* 'acceptance' 或 'details-id' 并登记来源清单(ADR-0074 词条)。
|
|
532
|
+
*/
|
|
533
|
+
receiptAnchor?: DeliveryReceiptAnchor;
|
|
534
|
+
/**
|
|
535
|
+
* 失败保留策略(v3):port.send 首败时本条目的处置(ADR-0122 无内核自动重试不变
|
|
536
|
+
* ——retain 不是重试机制,是失败条目的留存策略)。
|
|
537
|
+
* - 'forget' = 首败即停 + 显式上报后移除;store 已 persist 的记录同步 discard。
|
|
538
|
+
* - 'retain' = 失败条目留守:内存态转回 queued 等下一 settled 边沿 / 外部 flush
|
|
539
|
+
* 重入队;store 记录保留(重启经 loadUndelivered 重入队)。
|
|
540
|
+
* - 'failed' = 受理失败转 failed 终态留守(chat-flow-consistency 第 11 条):不自动
|
|
541
|
+
* 重投、不 discard,等用户处置(resync 重试 / cancel 撤销),与断连 failInFlight
|
|
542
|
+
* 的 failed 同态同出路。
|
|
543
|
+
* 缺省按载荷类型:custom = 'retain' / text = 'forget'(§3.4 缺省值表:交互在席 vs
|
|
544
|
+
* 无人值守)。sendChecked 挂起条目缺省/显式 retain 按 forget 处置(同步失败回执
|
|
545
|
+
* 契约与 retain 重投矛盾;一次性 warn);显式 failed 放行——waiter reject 是受理
|
|
546
|
+
* 口径通知,failed 留守是条目状态,两者不矛盾(failed 不自动重投)。
|
|
334
547
|
*/
|
|
335
|
-
|
|
548
|
+
onFailure?: DeliveryFailurePolicy;
|
|
336
549
|
/**
|
|
337
550
|
* 无标记条目标志(pi1-disposition-chat-flow D14⑥ 起源;技能/芯片通路并入后为
|
|
338
551
|
* 「出站不注标条目」统一标志——命令 / 手打 skill 与 prompt 模板 / `<taiji-skill>`
|
|
@@ -348,13 +561,14 @@ interface DeliverySubmitOptions {
|
|
|
348
561
|
unmarked?: boolean;
|
|
349
562
|
}
|
|
350
563
|
/**
|
|
351
|
-
* send() 受理结果(b31-D6
|
|
564
|
+
* send() 受理结果(b31-D6:失败双表面窄口收尾)。调用方忽略返回值不受影响
|
|
352
565
|
* (v1 句柄面 send 仍声明 void,结构兼容)。三分口径:
|
|
353
566
|
* - accepted:已入队并进入投递流程(id = 条目 id,供调用方对账/判重锚定)
|
|
354
567
|
* - swallowed:幂等吞(already-seen = 显式 id 命中活跃集/tombstone;duplicate-content
|
|
355
568
|
* = dedupeKey 内容级去重命中)——设计内去重,非失败
|
|
356
569
|
* - rejected:拒收未入队(disposed = 句柄已销毁;unsupported-payload = 通路能力
|
|
357
|
-
* 不含该 payload kind
|
|
570
|
+
* 不含该 payload kind;persist-failed = store 受理前写入记录失败——耐久承诺未写入,
|
|
571
|
+
* v3 仅 config.store 装配时可达)——仅 warn 单表面 + 本返回值,无终态无 onSettled
|
|
358
572
|
*
|
|
359
573
|
* void 兼容口径(与 SendReceipt 的 U2 扩展位同构):接口返回类型含 void——
|
|
360
574
|
* v1 → v2 测试替身适配(runtime asHandleV2 直转发 v1 handle.send)返回 void =
|
|
@@ -368,7 +582,7 @@ type DeliverySendResult = {
|
|
|
368
582
|
reason: 'already-seen' | 'duplicate-content';
|
|
369
583
|
} | {
|
|
370
584
|
kind: 'rejected';
|
|
371
|
-
reason: 'disposed' | 'unsupported-payload';
|
|
585
|
+
reason: 'disposed' | 'unsupported-payload' | 'persist-failed';
|
|
372
586
|
};
|
|
373
587
|
/** v2 句柄:v1 DeliveryHandle 的超集(结构兼容,既有消费者零改动)。 */
|
|
374
588
|
interface DeliveryHandleV2 extends DeliveryHandle {
|
|
@@ -377,7 +591,7 @@ interface DeliveryHandleV2 extends DeliveryHandle {
|
|
|
377
591
|
sendChecked(msg: DeliveryMessage, opts?: DeliverySubmitOptions): Promise<void>;
|
|
378
592
|
/** 全量视图(D9②):活跃条目(完整字段)+ 全部 tombstone。快照(防御性拷贝)。 */
|
|
379
593
|
entriesFull(): DeliveryEntriesFull;
|
|
380
|
-
/**
|
|
594
|
+
/** 派生视图(D9②/D5③):活跃全量 + delivered 最近 50 条完整条目,cancelled 不进派生视图。 */
|
|
381
595
|
projection(): DeliveryEntriesProjection;
|
|
382
596
|
/**
|
|
383
597
|
* 变更订阅(state topic 装配用):任何条目创建/迁移/终态触发。返回退订函数。
|
|
@@ -390,16 +604,27 @@ interface DeliveryHandleV2 extends DeliveryHandle {
|
|
|
390
604
|
* @returns 是否发生了状态转移(诊断/测试用)。
|
|
391
605
|
*/
|
|
392
606
|
confirmDelivered(id: string): boolean;
|
|
607
|
+
/**
|
|
608
|
+
* 凭据检查驱动的送达确认(v3,'details-id' 凭据调用点):仅对申报 'details-id'
|
|
609
|
+
* 的条目生效——调 config.proofCheck(id)(对话记录中出现 details 携带本条目 id 的
|
|
610
|
+
* 通知条目的判定,由装配方注入实现),true → confirmDelivered(终局化 + onSettled
|
|
611
|
+
* 回调),false → 留守。非 details-id 条目(marker / acceptance 有各自判定通路)、
|
|
612
|
+
* 未注入 proofCheck、proofCheck 抛错(降级 warn 视为未送达,保守放行型降级)时
|
|
613
|
+
* 一律返回 false 不转移。装配方在回执证据到达(message_end 等)或重放扫描时对本
|
|
614
|
+
* 批候选条目调用,内核保证凭据档位分派(误确认在结构上不可达)。
|
|
615
|
+
* 幂等:未知/已终态 no-op。@returns 是否发生了状态转移。
|
|
616
|
+
*/
|
|
617
|
+
confirmDeliveredByProof(id: string): boolean;
|
|
393
618
|
/**
|
|
394
619
|
* 受理登记([扩展③],dmg-r1-4):分段 port 实现申报「本条目已由底层通道受理」。
|
|
395
620
|
* 只接受当前出站批内的 queued 条目(受理事实锚定于在途投递);生效即 queued →
|
|
396
621
|
* in-flight 转移 + 出批 + checked waiter 按受理口径 resolve(D9⑤)。幂等:非批内/
|
|
397
622
|
* 已转移/未知 id 返回 false。合批分段中途失败时已登记段留守 in-flight(等回执/对账),
|
|
398
|
-
*
|
|
623
|
+
* 仅未受理段按失败处理——已进底层通道的文本不再误报「投递失败」。
|
|
399
624
|
*/
|
|
400
625
|
confirmAccepted(id: string): boolean;
|
|
401
626
|
/**
|
|
402
|
-
* 断连事件驱动的显式失败终局(ADR-0122
|
|
627
|
+
* 断连事件驱动的显式失败终局(ADR-0122 命令终局事件化的断连路径):把全部 in-flight
|
|
403
628
|
* 条目批量转 failed 终态(留守活跃集等用户处置:resync 重试 / cancel 移除),
|
|
404
629
|
* 逐条 onSettled('rejected') 显式上报。queued 条目不触碰(未触达底层通道,随队列
|
|
405
630
|
* 存活,重连后照常投递)。幂等:无 in-flight 条目返回 0。@returns 终态化条数。
|
|
@@ -416,6 +641,17 @@ interface DeliveryHandleV2 extends DeliveryHandle {
|
|
|
416
641
|
* 待收回(扩展②两段式);已终态幂等返回既有 tombstone。
|
|
417
642
|
*/
|
|
418
643
|
cancel(id: string): DeliveryCancelResult;
|
|
644
|
+
/**
|
|
645
|
+
* 原位替换条目内容(chat-flow-consistency D6 edit 原语):语义 = 替换指定条目的 payload
|
|
646
|
+
* 且**条目 id 与排队位置均不变**——上层只见「替换内容,位置保持」,收回-全部-保序重投是
|
|
647
|
+
* registry 编排层组合 requeue 的内部细节,本原语不感知 pi。
|
|
648
|
+
* 接受 queued(未触达底层通道,替换后出站即新文本;持有期排队的常态)/ failed(留守行
|
|
649
|
+
* 编辑,替换后由调用方 requeue 重投)/ in-flight(registry 收回路径:clear_queue 已把
|
|
650
|
+
* 文本收回后、requeue 重投前的窗口内替换)。拒绝:终态/未知 id(false)、queued 但已在
|
|
651
|
+
* 出站批内(受理登记窗口——文本可能已进底层通道,替换会双投,调用方按悬空窗口处置)。
|
|
652
|
+
* msg.payload 同步替换引用(onSettled per-message 契约的消费方拿到替换后内容)。
|
|
653
|
+
*/
|
|
654
|
+
edit(id: string, payload: DeliveryPayload): boolean;
|
|
419
655
|
/** 全量取回文本(forceQuit,D9③):条目清空 + 全部记 cancelled tombstone。 */
|
|
420
656
|
drain(): DrainResult;
|
|
421
657
|
}
|
|
@@ -428,7 +664,7 @@ type DeliveryCancelResult = {
|
|
|
428
664
|
entry: DeliveryEntry;
|
|
429
665
|
} | {
|
|
430
666
|
kind: 'already-final';
|
|
431
|
-
tombstone:
|
|
667
|
+
tombstone: DeliveryTerminalRecord;
|
|
432
668
|
} | {
|
|
433
669
|
kind: 'not-found';
|
|
434
670
|
};
|
|
@@ -458,7 +694,7 @@ declare function createDelivery(port: DeliveryPort, config?: DeliveryConfigWithW
|
|
|
458
694
|
* 产生点(delivery.ts):cancel 终结条目(queued/failed 本地终结 + in-flight 收回确认)
|
|
459
695
|
* 与 drain 全量回收时的 waiter reject。语义:文本已回草稿(V9/V11),条目已被内核
|
|
460
696
|
* 终结——不是投递失败,消费方(registry dispose 错误分级)据 instanceof 判别后
|
|
461
|
-
*
|
|
697
|
+
* 只记日志、不进用户可见错误范围。
|
|
462
698
|
*
|
|
463
699
|
* 结构化字段(原文案契约的语义信息转字段,文案漂移不再击穿判别):
|
|
464
700
|
* - kind:回收方式。'cancelled' = cancel(id) 单条回收;'drained' = drain() 全量回收。
|
|
@@ -473,4 +709,59 @@ declare class DeliveryReclaimError extends Error {
|
|
|
473
709
|
constructor(kind: 'cancelled' | 'drained', entryId?: string);
|
|
474
710
|
}
|
|
475
711
|
|
|
476
|
-
|
|
712
|
+
/**
|
|
713
|
+
* 磁盘 outbox —— DeliveryStore 的 runtime 半区实现(统一消息投递设计 §3.5.1 / §3.8
|
|
714
|
+
* 文本行,波 4;关闭 ADR-0074「outbox 不落盘」代价)。
|
|
715
|
+
*
|
|
716
|
+
* 形态:JSONL 追加文件,每行一个 JSON 对象(`t` 字段判别行型),单调累积——
|
|
717
|
+
* - `{"t":"record","r":<DeliveryStoreRecord>}`:受理前写入记录(耐久承诺先于投递尝试);
|
|
718
|
+
* - `{"t":"ack","id":...}`:终局(delivered / cancelled 同形,判重只看 id)——终局行
|
|
719
|
+
* 保留是判重依据(已撤销消息的防复活凭据同样在此),**单调累积、不实现清理通道**;
|
|
720
|
+
* - `{"t":"discard","id":...}`:forget 失败条目的记录移除(无终局无判重语义,区别于
|
|
721
|
+
* ack——loadUndelivered 跳过、hasTerminal 为假,防重启后被当未投递复活);
|
|
722
|
+
* - `{"t":"meta","id":...,"segments":...}`:segments 快照补记(runtime 侧撤销编排的
|
|
723
|
+
* 回填通道——重启重放后撤销仍返回原始 segments)。
|
|
724
|
+
*
|
|
725
|
+
* 读取 = 全量行序重放(后行覆盖前行):record 建记录 / ack 进终局集 / discard 进弃置
|
|
726
|
+
* 集 / meta 并入记录 meta。同 id 先 discard 后 record = 记录复活(新承诺覆盖弃置);
|
|
727
|
+
* 同 id 先 ack 后 record = 判重保持(终局集不撤销——宁可误吞不误重投,与内核
|
|
728
|
+
* store.hasTerminal 注释口径一致)。
|
|
729
|
+
*
|
|
730
|
+
* 本文件零 node:fs 依赖:全部 fs 操作经 DiskStoreFs 窄接口注入(内核包零 fs 知识纪律,
|
|
731
|
+
* 设计 §3.5.1「两个实现在两半区适配层」);目录与文件路径由装配方(runtime 侧)动态
|
|
732
|
+
* 推导后以字符串传入。崩溃写入的中途半行由读取容错覆盖(JSON.parse 失败 warn 跳过)
|
|
733
|
+
* ——最坏丢一行 = 最坏多投一次,与「必达优先于去重」语义一致,不需要原子替换协议。
|
|
734
|
+
*/
|
|
735
|
+
|
|
736
|
+
/** fs 窄接口(注入点;生产 = node:fs 适配,测试 = 内存替身)。 */
|
|
737
|
+
interface DiskStoreFs {
|
|
738
|
+
existsSync(path: string): boolean;
|
|
739
|
+
mkdirSync(path: string, opts: {
|
|
740
|
+
recursive: true;
|
|
741
|
+
}): unknown;
|
|
742
|
+
readFileSync(path: string, encoding: 'utf8'): string;
|
|
743
|
+
appendFileSync(path: string, data: string): void;
|
|
744
|
+
}
|
|
745
|
+
interface DiskDeliveryStoreDeps {
|
|
746
|
+
/** fs 操作面(注入;内核零 node:fs import)。 */
|
|
747
|
+
fs: DiskStoreFs;
|
|
748
|
+
/** outbox 目录(绝对路径,装配方动态推导;首次写入前惰性创建)。 */
|
|
749
|
+
dir: string;
|
|
750
|
+
/** outbox 文件(`<dir>/<sessionId 编码>.jsonl`,装配方拼装)。 */
|
|
751
|
+
file: string;
|
|
752
|
+
/** 读取容错与写失败的留痕出口;缺省 console.warn。 */
|
|
753
|
+
log?: (message: string, err?: unknown) => void;
|
|
754
|
+
}
|
|
755
|
+
/** 磁盘 outbox store:DeliveryStore 契约 + segments 补记扩展(registry 撤销回填通道)。 */
|
|
756
|
+
interface DiskDeliveryStore extends DeliveryStore {
|
|
757
|
+
/**
|
|
758
|
+
* segments 快照补记(实现侧扩展,**非 DeliveryStore 端口成员**——内核不感知):
|
|
759
|
+
* 把提交侧 attachSegments 登记的原始快照追加为 meta 行,随记录跨重启存活;
|
|
760
|
+
* 重放侧经 loadUndelivered 的 record.meta.segments 取回。同 id 多次补记按行序
|
|
761
|
+
* 后写覆盖先写。
|
|
762
|
+
*/
|
|
763
|
+
attachSegments(id: string, segments: readonly unknown[]): void;
|
|
764
|
+
}
|
|
765
|
+
declare function createDiskDeliveryStore(deps: DiskDeliveryStoreDeps): DiskDeliveryStore;
|
|
766
|
+
|
|
767
|
+
export { type CustomPayload, type DeliveryCancelResult, type DeliveryConfig, type DeliveryConfigWithWarn, type DeliveryDeferReason, type DeliveryEntriesFull, type DeliveryEntriesProjection, type DeliveryEntry, type DeliveryEntryState, type DeliveryFailurePolicy, type DeliveryHandle, type DeliveryHandleV2, type DeliveryIntent, type DeliveryLane, type DeliveryMessage, type DeliveryMeta, type DeliveryPayload, type DeliveryPort, type DeliveryProofCheck, type DeliveryReceiptAnchor, DeliveryReclaimError, type DeliverySendResult, type DeliveryStore, type DeliveryStoreRecord, type DeliverySubmitOptions, type DeliveryTerminalRecord, type DeliveryWarnSink, type DiskDeliveryStore, type DiskDeliveryStoreDeps, type DiskStoreFs, type DrainResult, type SendReceipt, type TextPayload, createDelivery, createDiskDeliveryStore };
|
package/dist/index.mjs
CHANGED
|
@@ -43,8 +43,7 @@ var LruSet = class {
|
|
|
43
43
|
|
|
44
44
|
// src/delivery.ts
|
|
45
45
|
var DEFAULT_CONFIG = {
|
|
46
|
-
intent: "interrupt-at-turn-boundary"
|
|
47
|
-
mergeWindowMs: 0
|
|
46
|
+
intent: "interrupt-at-turn-boundary"
|
|
48
47
|
};
|
|
49
48
|
var DEFAULT_DELIVERED_WINDOW = 50;
|
|
50
49
|
function isThenable(v) {
|
|
@@ -103,14 +102,6 @@ function settleChecked(batch, err, checkedPending) {
|
|
|
103
102
|
checkedPending.push(...kept);
|
|
104
103
|
return rejected;
|
|
105
104
|
}
|
|
106
|
-
function safeIsIdle(port, onFault) {
|
|
107
|
-
try {
|
|
108
|
-
return port.isIdle();
|
|
109
|
-
} catch (err) {
|
|
110
|
-
onFault?.(err);
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
105
|
function resolveWarnSink(config) {
|
|
115
106
|
return config?.warn ?? ((msg, err) => {
|
|
116
107
|
console.warn(`[session-delivery] ${msg}`, err ?? "");
|
|
@@ -123,19 +114,53 @@ function createDelivery(port, config) {
|
|
|
123
114
|
};
|
|
124
115
|
const active = [];
|
|
125
116
|
const activeIndex = /* @__PURE__ */ new Map();
|
|
126
|
-
const
|
|
117
|
+
const terminalIndex = /* @__PURE__ */ new Set();
|
|
118
|
+
const terminalRecords = /* @__PURE__ */ new Map();
|
|
119
|
+
let checkedRetainWarned = false;
|
|
127
120
|
const deliveredLog = [];
|
|
128
121
|
const checkedPending = [];
|
|
129
122
|
let inflightBatch = [];
|
|
130
123
|
let inFlight = false;
|
|
131
|
-
let mergeTimer;
|
|
132
124
|
let disposed = false;
|
|
133
125
|
let settledUnsub;
|
|
134
126
|
let missingKeyWarned = false;
|
|
135
|
-
let probeFaultWarned = false;
|
|
136
127
|
let idSeq = 0;
|
|
137
128
|
const dedupSet = config?.dedupe ? new LruSet(config.dedupe.maxKeys) : null;
|
|
138
129
|
const changeSubs = /* @__PURE__ */ new Set();
|
|
130
|
+
const store = config?.store;
|
|
131
|
+
if (store) {
|
|
132
|
+
for (const id of store.loadTerminalIds()) terminalIndex.add(id);
|
|
133
|
+
}
|
|
134
|
+
function persistRecord(e) {
|
|
135
|
+
if (!store) return;
|
|
136
|
+
store.persist({
|
|
137
|
+
id: e.id,
|
|
138
|
+
lane: e.lane,
|
|
139
|
+
payload: e.payload,
|
|
140
|
+
intent: e.msg.intent ?? cfg.intent,
|
|
141
|
+
recordedAt: e.createdAt,
|
|
142
|
+
receiptAnchor: e.receiptAnchor,
|
|
143
|
+
unmarked: e.unmarked,
|
|
144
|
+
meta: e.msg.meta
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function ackStore(id) {
|
|
148
|
+
if (!store) return;
|
|
149
|
+
try {
|
|
150
|
+
store.ack(id);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
warn(`store.ack failed (terminal record not persisted; id=${id})`, err);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function safeHasTerminal(id) {
|
|
156
|
+
if (!store) return false;
|
|
157
|
+
try {
|
|
158
|
+
return store.hasTerminal(id);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
warn(`store.hasTerminal threw; treating as not-terminal (id=${id})`, err);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
139
164
|
function now() {
|
|
140
165
|
return Date.now();
|
|
141
166
|
}
|
|
@@ -170,19 +195,6 @@ function createDelivery(port, config) {
|
|
|
170
195
|
warn(`onSettled callback threw (outcome=${outcome}); isolated from delivery state machine`, err);
|
|
171
196
|
}
|
|
172
197
|
}
|
|
173
|
-
function warnProbeFault(err) {
|
|
174
|
-
if (probeFaultWarned) return;
|
|
175
|
-
probeFaultWarned = true;
|
|
176
|
-
warn("port busy probe (isIdle) threw; conservatively treating as busy", err);
|
|
177
|
-
}
|
|
178
|
-
function isBusySafe() {
|
|
179
|
-
if (!safeIsIdle(port, warnProbeFault)) return true;
|
|
180
|
-
return hasInFlightEntries();
|
|
181
|
-
}
|
|
182
|
-
function hasInFlightEntries() {
|
|
183
|
-
for (const e of active) if (e.state === "in-flight") return true;
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
198
|
function inRegistry(e) {
|
|
187
199
|
return activeIndex.get(e.id) === e;
|
|
188
200
|
}
|
|
@@ -200,7 +212,9 @@ function createDelivery(port, config) {
|
|
|
200
212
|
const ts = now();
|
|
201
213
|
e.updatedAt = ts;
|
|
202
214
|
e.settledAt = ts;
|
|
203
|
-
|
|
215
|
+
terminalIndex.add(e.id);
|
|
216
|
+
terminalRecords.set(e.id, { id: e.id, state, lane: e.lane, settledAt: ts });
|
|
217
|
+
ackStore(e.id);
|
|
204
218
|
if (state === "delivered") {
|
|
205
219
|
resolveWaitersOf(e);
|
|
206
220
|
deliveredLog.push(e);
|
|
@@ -229,30 +243,11 @@ function createDelivery(port, config) {
|
|
|
229
243
|
}
|
|
230
244
|
}
|
|
231
245
|
}
|
|
232
|
-
function clearMergeTimer() {
|
|
233
|
-
if (mergeTimer !== void 0) {
|
|
234
|
-
clearTimeout(mergeTimer);
|
|
235
|
-
mergeTimer = void 0;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
function armMergeTimer() {
|
|
239
|
-
if (cfg.mergeWindowMs <= 0) return;
|
|
240
|
-
mergeTimer = setTimeout(() => {
|
|
241
|
-
mergeTimer = void 0;
|
|
242
|
-
flush();
|
|
243
|
-
}, cfg.mergeWindowMs);
|
|
244
|
-
}
|
|
245
|
-
function resetMergeTimer() {
|
|
246
|
-
clearMergeTimer();
|
|
247
|
-
armMergeTimer();
|
|
248
|
-
}
|
|
249
246
|
function ensureSettledSub() {
|
|
250
247
|
if (settledUnsub || !port.subscribeSettled) return;
|
|
251
248
|
settledUnsub = port.subscribeSettled(() => {
|
|
252
249
|
if (disposed) return;
|
|
253
|
-
|
|
254
|
-
flush();
|
|
255
|
-
}
|
|
250
|
+
flush();
|
|
256
251
|
});
|
|
257
252
|
}
|
|
258
253
|
function teardownSettledSub() {
|
|
@@ -289,6 +284,10 @@ function createDelivery(port, config) {
|
|
|
289
284
|
}
|
|
290
285
|
function onSendReceipt(receipt) {
|
|
291
286
|
if (receipt !== void 0 && receipt.accepted === false) {
|
|
287
|
+
if ("deferred" in receipt) {
|
|
288
|
+
onSendDeferred(receipt.deferred);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
292
291
|
onSendFail(new Error(receipt.reason ?? "port.send rejected (accepted:false)"));
|
|
293
292
|
return;
|
|
294
293
|
}
|
|
@@ -324,18 +323,47 @@ function createDelivery(port, config) {
|
|
|
324
323
|
warn("port.send failed (first-failure stop, no kernel retry)", err);
|
|
325
324
|
const rejected = settleChecked(batch, err, checkedPending);
|
|
326
325
|
let changed = false;
|
|
326
|
+
let retained = false;
|
|
327
327
|
for (const e of batch) {
|
|
328
328
|
if (!inRegistry(e)) continue;
|
|
329
329
|
if (rejected.has(e)) continue;
|
|
330
|
+
if (e.onFailure === "retain") continue;
|
|
330
331
|
callOnSettled(e.msg, "rejected");
|
|
331
332
|
}
|
|
333
|
+
const ts = now();
|
|
332
334
|
for (const e of batch) {
|
|
333
335
|
if (!inRegistry(e)) continue;
|
|
336
|
+
if (e.onFailure === "retain") {
|
|
337
|
+
e.state = "queued";
|
|
338
|
+
e.updatedAt = now();
|
|
339
|
+
changed = true;
|
|
340
|
+
retained = true;
|
|
341
|
+
warn(`entry retained after failure (id=${e.id}); re-dispatch waits for next settled edge or external flush`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (e.onFailure === "failed") {
|
|
345
|
+
e.state = "failed";
|
|
346
|
+
e.updatedAt = ts;
|
|
347
|
+
e.settledAt = ts;
|
|
348
|
+
changed = true;
|
|
349
|
+
warn(`entry kept as failed after rejection (id=${e.id}); awaiting user retry (resync) or cancel`);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
store?.discard(e.id);
|
|
334
353
|
removeActive(e);
|
|
335
354
|
changed = true;
|
|
336
355
|
}
|
|
337
356
|
if (changed) notifyChange();
|
|
338
|
-
pump();
|
|
357
|
+
if (!retained) pump();
|
|
358
|
+
}
|
|
359
|
+
function onSendDeferred(reason) {
|
|
360
|
+
if (disposed) return;
|
|
361
|
+
const batch = inflightBatch;
|
|
362
|
+
inFlight = false;
|
|
363
|
+
inflightBatch = [];
|
|
364
|
+
warn(
|
|
365
|
+
`port.send deferred (designed-in wait, reason=${reason}); ${batch.length} entry(ies) stay queued for next dispatch edge`
|
|
366
|
+
);
|
|
339
367
|
}
|
|
340
368
|
function pump() {
|
|
341
369
|
if (disposed || inFlight) return;
|
|
@@ -367,7 +395,6 @@ function createDelivery(port, config) {
|
|
|
367
395
|
function scheduleFlush() {
|
|
368
396
|
if (disposed || !hasQueuedEntries()) return;
|
|
369
397
|
if (inFlight) return;
|
|
370
|
-
if (isBusySafe()) return;
|
|
371
398
|
doSend();
|
|
372
399
|
}
|
|
373
400
|
const warnSink = resolveWarnSink(config);
|
|
@@ -389,7 +416,22 @@ function createDelivery(port, config) {
|
|
|
389
416
|
}
|
|
390
417
|
function seenId(id) {
|
|
391
418
|
if (id === void 0) return false;
|
|
392
|
-
|
|
419
|
+
if (activeIndex.has(id)) return true;
|
|
420
|
+
if (terminalIndex.has(id)) return true;
|
|
421
|
+
return safeHasTerminal(id);
|
|
422
|
+
}
|
|
423
|
+
function resolveOnFailure(msg, opts) {
|
|
424
|
+
return opts?.onFailure ?? (msg.payload.kind === "custom" ? "retain" : "forget");
|
|
425
|
+
}
|
|
426
|
+
function persistBeforeAccept(e) {
|
|
427
|
+
try {
|
|
428
|
+
persistRecord(e);
|
|
429
|
+
return true;
|
|
430
|
+
} catch (err) {
|
|
431
|
+
warn(`store.persist failed before accept; entry rejected (id=${e.id})`, err);
|
|
432
|
+
removeActive(e);
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
393
435
|
}
|
|
394
436
|
function createEntry(msg, opts) {
|
|
395
437
|
const ts = now();
|
|
@@ -403,7 +445,8 @@ function createDelivery(port, config) {
|
|
|
403
445
|
msg,
|
|
404
446
|
cancelRequested: false,
|
|
405
447
|
receiptAnchor: opts?.receiptAnchor ?? "marker",
|
|
406
|
-
unmarked: opts?.unmarked ?? false
|
|
448
|
+
unmarked: opts?.unmarked ?? false,
|
|
449
|
+
onFailure: resolveOnFailure(msg, opts)
|
|
407
450
|
};
|
|
408
451
|
active.push(entry);
|
|
409
452
|
activeIndex.set(entry.id, entry);
|
|
@@ -425,13 +468,9 @@ function createDelivery(port, config) {
|
|
|
425
468
|
}
|
|
426
469
|
if (!passDedupe(msg)) return { kind: "swallowed", reason: "duplicate-content" };
|
|
427
470
|
const entry = createEntry(msg, opts);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
resetMergeTimer();
|
|
431
|
-
ensureSettledSub();
|
|
432
|
-
return { kind: "accepted", id: entry.id };
|
|
471
|
+
if (!persistBeforeAccept(entry)) {
|
|
472
|
+
return { kind: "rejected", reason: "persist-failed" };
|
|
433
473
|
}
|
|
434
|
-
clearMergeTimer();
|
|
435
474
|
ensureSettledSub();
|
|
436
475
|
scheduleFlush();
|
|
437
476
|
return { kind: "accepted", id: entry.id };
|
|
@@ -446,7 +485,17 @@ function createDelivery(port, config) {
|
|
|
446
485
|
return;
|
|
447
486
|
}
|
|
448
487
|
if (!passDedupe(msg)) return;
|
|
488
|
+
if (opts?.onFailure === "retain") {
|
|
489
|
+
if (!checkedRetainWarned) {
|
|
490
|
+
checkedRetainWarned = true;
|
|
491
|
+
warn("sendChecked ignores onFailure=retain (checked entries default to forget)");
|
|
492
|
+
}
|
|
493
|
+
opts = { ...opts, onFailure: "forget" };
|
|
494
|
+
}
|
|
449
495
|
const entry = createEntry(msg, opts);
|
|
496
|
+
if (!persistBeforeAccept(entry)) {
|
|
497
|
+
throw new Error(`store.persist failed before accept (id=${entry.id})`);
|
|
498
|
+
}
|
|
450
499
|
ensureSettledSub();
|
|
451
500
|
return new Promise((resolve, reject) => {
|
|
452
501
|
checkedPending.push({ entry, resolve, reject });
|
|
@@ -459,7 +508,6 @@ function createDelivery(port, config) {
|
|
|
459
508
|
}
|
|
460
509
|
function flush() {
|
|
461
510
|
if (disposed) return;
|
|
462
|
-
clearMergeTimer();
|
|
463
511
|
scheduleFlush();
|
|
464
512
|
}
|
|
465
513
|
function depth() {
|
|
@@ -470,7 +518,7 @@ function createDelivery(port, config) {
|
|
|
470
518
|
function entriesFull() {
|
|
471
519
|
return {
|
|
472
520
|
active: active.map(snapshot),
|
|
473
|
-
tombstones: [...
|
|
521
|
+
tombstones: [...terminalRecords.values()].map((t) => ({ ...t }))
|
|
474
522
|
};
|
|
475
523
|
}
|
|
476
524
|
function projection() {
|
|
@@ -489,11 +537,33 @@ function createDelivery(port, config) {
|
|
|
489
537
|
}
|
|
490
538
|
const e = activeIndex.get(id);
|
|
491
539
|
if (!e) return false;
|
|
492
|
-
if (e.state !== "queued" && e.state !== "in-flight") return false;
|
|
540
|
+
if (e.state !== "queued" && e.state !== "in-flight" && e.state !== "failed") return false;
|
|
493
541
|
finalizeEntry(e, "delivered");
|
|
494
542
|
if (!disposed) callOnSettled(e.msg, "delivered");
|
|
495
543
|
return true;
|
|
496
544
|
}
|
|
545
|
+
function confirmDeliveredByProof(id) {
|
|
546
|
+
if (disposed) {
|
|
547
|
+
warn(`confirmDeliveredByProof ignored: delivery handle disposed (id=${id})`);
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
const e = activeIndex.get(id);
|
|
551
|
+
if (!e || e.receiptAnchor !== "details-id") return false;
|
|
552
|
+
if (e.state !== "queued" && e.state !== "in-flight") return false;
|
|
553
|
+
if (!cfg.proofCheck) {
|
|
554
|
+
warn(`details-id entry has no proofCheck injected; delivery unconfirmed (id=${id})`);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
let proven;
|
|
558
|
+
try {
|
|
559
|
+
proven = cfg.proofCheck(id);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
warn(`proofCheck threw; treating as not-delivered (id=${id})`, err);
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
if (!proven) return false;
|
|
565
|
+
return confirmDelivered(id);
|
|
566
|
+
}
|
|
497
567
|
function confirmAccepted(id) {
|
|
498
568
|
if (disposed) return false;
|
|
499
569
|
const e = activeIndex.get(id);
|
|
@@ -543,7 +613,7 @@ function createDelivery(port, config) {
|
|
|
543
613
|
}
|
|
544
614
|
const e = activeIndex.get(id);
|
|
545
615
|
if (!e) {
|
|
546
|
-
const tb =
|
|
616
|
+
const tb = terminalRecords.get(id);
|
|
547
617
|
if (tb) return { kind: "already-final", tombstone: { ...tb } };
|
|
548
618
|
return { kind: "not-found" };
|
|
549
619
|
}
|
|
@@ -562,6 +632,18 @@ function createDelivery(port, config) {
|
|
|
562
632
|
finalizeEntry(e, "cancelled");
|
|
563
633
|
return { kind: "cancelled", entry: snapshot(e) };
|
|
564
634
|
}
|
|
635
|
+
function edit(id, payload) {
|
|
636
|
+
if (disposed) return false;
|
|
637
|
+
const e = activeIndex.get(id);
|
|
638
|
+
if (!e) return false;
|
|
639
|
+
if (e.state !== "queued" && e.state !== "in-flight" && e.state !== "failed") return false;
|
|
640
|
+
if (e.state === "queued" && inflightBatch.includes(e)) return false;
|
|
641
|
+
e.payload = payload;
|
|
642
|
+
e.msg.payload = payload;
|
|
643
|
+
e.updatedAt = now();
|
|
644
|
+
notifyChange();
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
565
647
|
function failInFlight(reason) {
|
|
566
648
|
if (disposed) return 0;
|
|
567
649
|
const ts = now();
|
|
@@ -584,7 +666,6 @@ function createDelivery(port, config) {
|
|
|
584
666
|
}
|
|
585
667
|
function drain() {
|
|
586
668
|
if (disposed) return [];
|
|
587
|
-
clearMergeTimer();
|
|
588
669
|
const drained = active.slice();
|
|
589
670
|
active.length = 0;
|
|
590
671
|
activeIndex.clear();
|
|
@@ -592,7 +673,9 @@ function createDelivery(port, config) {
|
|
|
592
673
|
inFlight = false;
|
|
593
674
|
const ts = now();
|
|
594
675
|
const result = drained.map((e) => {
|
|
595
|
-
|
|
676
|
+
terminalIndex.add(e.id);
|
|
677
|
+
terminalRecords.set(e.id, { id: e.id, state: "cancelled", lane: e.lane, settledAt: ts });
|
|
678
|
+
ackStore(e.id);
|
|
596
679
|
return { id: e.id, payload: e.payload };
|
|
597
680
|
});
|
|
598
681
|
for (const w of checkedPending) {
|
|
@@ -604,11 +687,11 @@ function createDelivery(port, config) {
|
|
|
604
687
|
}
|
|
605
688
|
function dispose() {
|
|
606
689
|
disposed = true;
|
|
607
|
-
clearMergeTimer();
|
|
608
690
|
teardownSettledSub();
|
|
609
691
|
active.length = 0;
|
|
610
692
|
activeIndex.clear();
|
|
611
|
-
|
|
693
|
+
terminalIndex.clear();
|
|
694
|
+
terminalRecords.clear();
|
|
612
695
|
deliveredLog.length = 0;
|
|
613
696
|
inflightBatch = [];
|
|
614
697
|
inFlight = false;
|
|
@@ -619,9 +702,123 @@ function createDelivery(port, config) {
|
|
|
619
702
|
checkedPending.length = 0;
|
|
620
703
|
dedupSet?.clear();
|
|
621
704
|
}
|
|
622
|
-
return { send, sendChecked, flush, depth, entriesFull, projection, onChange, confirmDelivered, confirmAccepted, requeue, cancel, failInFlight, drain, dispose };
|
|
705
|
+
return { send, sendChecked, flush, depth, entriesFull, projection, onChange, confirmDelivered, confirmDeliveredByProof, confirmAccepted, requeue, cancel, edit, failInFlight, drain, dispose };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/disk-store.ts
|
|
709
|
+
function serializeLine(line) {
|
|
710
|
+
return `${JSON.stringify(line)}
|
|
711
|
+
`;
|
|
712
|
+
}
|
|
713
|
+
function createDiskDeliveryStore(deps) {
|
|
714
|
+
const { fs, dir, file } = deps;
|
|
715
|
+
const log = deps.log ?? ((message, err) => {
|
|
716
|
+
console.warn(message, err ?? "");
|
|
717
|
+
});
|
|
718
|
+
let loaded = false;
|
|
719
|
+
const records = /* @__PURE__ */ new Map();
|
|
720
|
+
const terminalIds = /* @__PURE__ */ new Set();
|
|
721
|
+
const discardedIds = /* @__PURE__ */ new Set();
|
|
722
|
+
let dirCreated = false;
|
|
723
|
+
function applyLine(line) {
|
|
724
|
+
if (line.t === "record") {
|
|
725
|
+
records.set(line.r.id, line.r);
|
|
726
|
+
discardedIds.delete(line.r.id);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (line.t === "ack") {
|
|
730
|
+
terminalIds.add(line.id);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (line.t === "discard") {
|
|
734
|
+
discardedIds.add(line.id);
|
|
735
|
+
records.delete(line.id);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const record = records.get(line.id);
|
|
739
|
+
if (record) {
|
|
740
|
+
record.meta = { ...record.meta, segments: line.segments };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function ensureLoaded() {
|
|
744
|
+
if (loaded) return;
|
|
745
|
+
loaded = true;
|
|
746
|
+
if (!fs.existsSync(file)) return;
|
|
747
|
+
let raw;
|
|
748
|
+
try {
|
|
749
|
+
raw = fs.readFileSync(file, "utf8");
|
|
750
|
+
} catch (err) {
|
|
751
|
+
log(`[disk-delivery-store] read failed (treated as empty), file=${file}`, err);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
let lineNumber = 0;
|
|
755
|
+
for (const lineText of raw.split("\n")) {
|
|
756
|
+
lineNumber += 1;
|
|
757
|
+
const trimmed = lineText.trim();
|
|
758
|
+
if (trimmed === "") continue;
|
|
759
|
+
let line;
|
|
760
|
+
try {
|
|
761
|
+
line = JSON.parse(trimmed);
|
|
762
|
+
} catch {
|
|
763
|
+
log(`[disk-delivery-store] corrupted line skipped, file=${file}, line=${lineNumber}`);
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
if (!line || typeof line !== "object" || typeof line.t !== "string") {
|
|
767
|
+
log(`[disk-delivery-store] malformed line skipped, file=${file}, line=${lineNumber}`);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
applyLine(line);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function appendLine(line) {
|
|
774
|
+
if (!dirCreated) {
|
|
775
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
776
|
+
dirCreated = true;
|
|
777
|
+
}
|
|
778
|
+
fs.appendFileSync(file, serializeLine(line));
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
persist(record) {
|
|
782
|
+
ensureLoaded();
|
|
783
|
+
appendLine({ t: "record", r: record });
|
|
784
|
+
applyLine({ t: "record", r: record });
|
|
785
|
+
},
|
|
786
|
+
ack(id) {
|
|
787
|
+
ensureLoaded();
|
|
788
|
+
appendLine({ t: "ack", id, at: Date.now() });
|
|
789
|
+
applyLine({ t: "ack", id, at: 0 });
|
|
790
|
+
},
|
|
791
|
+
discard(id) {
|
|
792
|
+
ensureLoaded();
|
|
793
|
+
appendLine({ t: "discard", id, at: Date.now() });
|
|
794
|
+
applyLine({ t: "discard", id, at: 0 });
|
|
795
|
+
},
|
|
796
|
+
loadUndelivered() {
|
|
797
|
+
ensureLoaded();
|
|
798
|
+
const out = [];
|
|
799
|
+
for (const record of records.values()) {
|
|
800
|
+
if (terminalIds.has(record.id) || discardedIds.has(record.id)) continue;
|
|
801
|
+
out.push(record);
|
|
802
|
+
}
|
|
803
|
+
return out;
|
|
804
|
+
},
|
|
805
|
+
loadTerminalIds() {
|
|
806
|
+
ensureLoaded();
|
|
807
|
+
return [...terminalIds];
|
|
808
|
+
},
|
|
809
|
+
hasTerminal(id) {
|
|
810
|
+
ensureLoaded();
|
|
811
|
+
return terminalIds.has(id);
|
|
812
|
+
},
|
|
813
|
+
attachSegments(id, segments) {
|
|
814
|
+
ensureLoaded();
|
|
815
|
+
appendLine({ t: "meta", id, segments: [...segments] });
|
|
816
|
+
applyLine({ t: "meta", id, segments: [...segments] });
|
|
817
|
+
}
|
|
818
|
+
};
|
|
623
819
|
}
|
|
624
820
|
export {
|
|
625
821
|
DeliveryReclaimError,
|
|
626
|
-
createDelivery
|
|
822
|
+
createDelivery,
|
|
823
|
+
createDiskDeliveryStore
|
|
627
824
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/session-delivery",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Session message delivery kernel: zero-pi-dependency strategy layer for
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Session message delivery kernel: zero-pi-dependency strategy layer for idempotent, accept-time dispatch of messages into pi sessions",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.mts",
|