@rei-standard/amsg-server 2.6.0-next.29 → 2.6.0-next.30
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/README.md +66 -3
- package/dist/adapters/d1.d.ts +36 -0
- package/dist/adapters/interface.d.ts +13 -0
- package/dist/{chunk-HDAKZXL5.cjs → chunk-E4657Y5X.cjs} +313 -66
- package/dist/{chunk-IDQPG2GZ.mjs → chunk-VBDBORLR.mjs} +310 -63
- package/dist/cloudflare.cjs +4 -2
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.mjs +3 -1
- package/dist/index.cjs +30 -25
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.mjs +6 -1
- package/dist/lib/agentic-fire.d.ts +27 -0
- package/dist/lib/errors.d.ts +59 -1
- package/dist/lib/llm.d.ts +12 -1
- package/dist/lib/message-processor.d.ts +14 -11
- package/dist/lib/outbox-store.d.ts +62 -0
- package/dist/lib/run-tick.d.ts +2 -0
- package/dist/single-user.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -389,8 +389,8 @@ hook 在 `pushPayloads` 里自己写了这几个字段的话会被库覆盖:
|
|
|
389
389
|
|
|
390
390
|
| hook | 什么时候调 | 载荷 |
|
|
391
391
|
|---|---|---|
|
|
392
|
-
| `onAfterSend` | fire 的 pushPayloads 逐段发完,或中途发挂 | `{ task, sentCount, pushedCount, total, error, scratch, readState, writeState, emitResult }` |
|
|
393
|
-
| `onFireSettled` | 一次 fire 收尾——只要 `onBeforeFire` 被调用过,什么结局都调一次 | `{ task, status, skipReason, sentCount, pushedCount, total, iterations, error, scratch, readState, writeState, emitResult }` |
|
|
392
|
+
| `onAfterSend` | fire 的 pushPayloads 逐段发完,或中途发挂 | `{ task, sentCount, pushedCount, total, error, usage, usageTotal, llmCalls, outboxed, scratch, readState, writeState, emitResult }` |
|
|
393
|
+
| `onFireSettled` | 一次 fire 收尾——只要 `onBeforeFire` 被调用过,什么结局都调一次 | `{ task, status, skipReason, sentCount, pushedCount, total, iterations, error, metadata, usage, usageTotal, llmCalls, outboxed, scratch, readState, writeState, emitResult }` |
|
|
394
394
|
| `onStaleSkip` | 任务错过触发时刻超过 60 分钟、这一次(或这几次)不再补发 | `{ reason, action, metadata, recurrenceType, occurrenceMs, skippedCount, skippedOccurrences, skippedTruncated, nextSendAt, readState, writeState, emitResult }` |
|
|
395
395
|
|
|
396
396
|
三个 hook 都自带 `readState` / `writeState` / `emitResult`,作用于当前用户,语义与 fire 级那套一致。`onStaleSkip` 尤其需要:服务停摆恢复后的第一跳里可能一次 fire 都没跑过,而那正是它要留痕迹的时候。
|
|
@@ -408,6 +408,20 @@ hook 在 `pushPayloads` 里自己写了这几个字段的话会被库覆盖:
|
|
|
408
408
|
| `failed` | 链路抛错,`error` 带原始错误。发到第 k 段挂了也是这个:`sentCount = k`、`total` 是原本要发的段数 |
|
|
409
409
|
| `not-handled` | `onBeforeFire` 返回 `null`,这条任务交还给排程时冻结的 prompt 老链路。那条链路不归 fire hook 管,它后面发没发出去不体现在这里 |
|
|
410
410
|
|
|
411
|
+
**用量记账**看这三个字段,两个 hook 都带,`onFireSettled` 的每种结局(发完、跳过、失败)都带——失败也花了钱:
|
|
412
|
+
|
|
413
|
+
| 字段 | 是什么 |
|
|
414
|
+
|---|---|
|
|
415
|
+
| `usage` | 最后一轮 LLM 响应的 `usage` 原样(没跑到 LLM → `null`) |
|
|
416
|
+
| `usageTotal` | 本次 fire 所有 LLM 轮次加起来,形状固定 `{ prompt_tokens, completion_tokens, total_tokens }`。各家 usage 形状不齐时尽量相加:没有 `prompt_tokens` / `completion_tokens` 就认 `input_tokens` / `output_tokens`,某一轮没报 `total_tokens` 就拿那一轮的 prompt + completion 补上;一项所有轮次都没报过就是 `null`,所有轮次都没报 usage(或没跑到 LLM)时整个是 `null` |
|
|
417
|
+
| `llmCalls` | 本次 fire 实际发出的 LLM 请求数,失败的那一次也算;没跑到 LLM → `0` |
|
|
418
|
+
|
|
419
|
+
`status: 'not-handled'`(交还给冻结 prompt 老链路)时 `llmCalls` 是 `0`:老链路那一次生成发生在收尾之后,不归 fire hook 管。
|
|
420
|
+
|
|
421
|
+
`outboxed` 说的是 finish 的这一批有没有整批落进收件箱(没走到 finish → `false`)。`true` 时客户端补收一定拿得到全部 `total` 段;推送没发完的那部分,库在任务重试时**只补推送、不重新生成**,补推那一跳也不再调任何 hook(包括这两个)。所以 `status: 'failed'` 而 `outboxed: true` 的意思是「内容已经生成并落定,只是推送没发完」,而不是「这条消息没了」。详见[投递失败怎么重试](#投递失败怎么重试)。
|
|
422
|
+
|
|
423
|
+
`error.permanent === true` 表示这次失败一跳终审、不会再重试(hook 抛的 `NonRetryableError`、LLM 上游明确拒了这次请求、没落进收件箱的批次推到一半失败)。反过来不成立:推送服务回 404 / 410 / 413、订阅没登记这几种也是一跳终审,但判定在投递侧,错误对象上不带 `permanent`。
|
|
424
|
+
|
|
411
425
|
跟 `onAfterSend` 的分工:`onAfterSend` 只走「有 push 要发」这条路,所以 hook 判断这次不用说话、或者链路中途抛错时它不会被调到——「开始时占点什么、结束时放掉」的写法要挂 `onFireSettled`(fire 里已经用 `ctx.scheduleTask` 建出来的任务,不记账就成了只活在数据库里的幽灵任务;fire 开头拿的锁,没有可靠释放点就只能等 TTL)。正常发完时两个都会调,`onAfterSend` 在前。`scratch` 是同一个引用。没配 hooks 的部署、以及不需要 LLM 的固定文本任务不走 fire 这条路径,两个都不会调。
|
|
412
426
|
|
|
413
427
|
`onStaleSkip` 的 `action` 分两种:
|
|
@@ -641,6 +655,54 @@ await client.scheduleMessage({
|
|
|
641
655
|
|
|
642
656
|
投递失败的退避记在 `retry_after` 上,租约同时放掉。两件事分两列记:`lease_until` 只表示「这条正在跑」,`retry_after` 表示「这条没在跑,在等重试」。挤在一列的话,下面的分组串行会把一条正在退避、其实闲着的任务当成「这一组忙着」,同组别的任务白等一轮退避(最长 6 分钟)。
|
|
643
657
|
|
|
658
|
+
## 投递失败怎么重试
|
|
659
|
+
|
|
660
|
+
定时任务投递失败后按 2 / 4 / 6 分钟退避,默认再试 **3 次**。用完之后一次性任务标 `failed`,循环任务只作废本次(排期推进到下一次、重试计数归零)。次数可以调低:
|
|
661
|
+
|
|
662
|
+
```js
|
|
663
|
+
createSingleUserCloudflareWorker((env) => ({ …, maxDeliveryRetries: 1 })); // 0 = 第一次失败就终审
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
`createSingleUserServer` / `createReiServer` 的 config、直接调 `runScheduledTick` 的 ctx 也认同名字段,默认值导出为 `DEFAULT_MAX_DELIVERY_RETRIES`。它只管定时任务这条退避阶梯;`messageType: 'instant'` 在请求里当场投递的那条路有自己的三轮重试,不受它影响。宿主要是自己按「重试次数到 3」判断终态的,调低之后记得跟着改。
|
|
667
|
+
|
|
668
|
+
### 一跳终审、不再重试的失败
|
|
669
|
+
|
|
670
|
+
每重试一次都可能把整条生成(`onBeforeFire` 里宿主的计费调用、每一轮 LLM)重跑一遍,所以重试也好不了的失败不进退避阶梯:
|
|
671
|
+
|
|
672
|
+
| 失败 | 怎么认出来 |
|
|
673
|
+
|---|---|
|
|
674
|
+
| hook 契约违约、hook 抛 `NonRetryableError` | 错误带 `permanent: true`,见[hook 契约违约算确定性失败](#hook-契约违约算确定性失败) |
|
|
675
|
+
| LLM 上游明确拒了这次请求:400 / 401 / 402 / 403 / 404 / 405 / 413 / 422 | 错误带 `code: 'LLM_CALL_FAILED'`、`llmStatus`,并且 `permanent: true` |
|
|
676
|
+
| 推送订阅没登记、推送服务回 404 / 410(订阅没了)/ 413(内容太大) | 失败记录里的 `errorCode` / `pushStatus` |
|
|
677
|
+
| 没落进收件箱的一批推到一半失败 | 见下面「没有收件箱时」 |
|
|
678
|
+
|
|
679
|
+
LLM 这一条只认上游**答复了、并且拒了**的那几种状态码:Key 错了、余额不足、模型名写错、请求体不合法,隔两分钟再发一遍还是同一个结果。下面这些照旧重试:408 / 409 / 425 / 429、所有 5xx、网络直接炸了或超时(没有 `llmStatus`)、中转站回 200 却在响应体里装着报错(真实原因说不准),以及上面没列出来的其余 4xx。
|
|
680
|
+
|
|
681
|
+
`permanent: true` 是在 LLM 调用抛错的那一刻就标好的,所以 `onFireSettled` / `onAfterSend` 拿到的 `error.permanent` 与投递侧的处置一致,宿主可以据此判断「这是终态,该告诉用户了」。
|
|
682
|
+
|
|
683
|
+
### 生成成功之后推送失败:只补推送
|
|
684
|
+
|
|
685
|
+
一次触发的整批 push 在发出去之前会先落进收件箱。落进去了,这次触发的内容就定了——之后推送再失败(推送服务 5xx 之类),任务照常进退避阶梯,但重试那一跳**不再调 LLM,也不调任何 fire-time hook**,只把这一批里还没送到的几条原样再推一遍:
|
|
686
|
+
|
|
687
|
+
- 已经推出去的、客户端已经 ack 的不再推;到了客户端不弹通知的那些照旧不推。
|
|
688
|
+
- 推的就是收件箱里那一份:`messageId`、时间戳、正文都跟第一次一模一样。客户端补收拿到的、重推收到的、第一次推到一半已经收到的,是同一份内容。
|
|
689
|
+
- 客户端在两次重试之间已经把整批补收并 ack 了的话,这一跳什么都不用推,直接算成功。
|
|
690
|
+
- 补推再失败就继续退避,下一跳还是只补推;重试用完照常终审,内容仍在收件箱里等客户端补收。
|
|
691
|
+
- 落收件箱排在查 VAPID / 推送订阅之前:生成之后读订阅那一下超时、VAPID 暂时没配齐,同样只补推送。订阅压根没登记的照旧一跳终审,但这次的内容已经在收件箱里,客户端上线补收得到。
|
|
692
|
+
|
|
693
|
+
冻结 prompt 老链路和 fire-time hook 链路都是这样。tick 汇总的 `details.redeliveredTasks`(`{ taskId, pushedCount }`)列出这一跳里只补推、没有重新生成的任务。
|
|
694
|
+
|
|
695
|
+
重试那一跳靠适配器的 `listOutboxForTask` 找这次触发落定的那一批(内置 D1 有;没实现的自定义适配器退回翻页扫描未 ack 的行,读不到已 ack 的行,客户端恰好在两次重试之间 ack 了整批的话会重新生成一份)。
|
|
696
|
+
|
|
697
|
+
### 没有收件箱时
|
|
698
|
+
|
|
699
|
+
这一批没落进收件箱(多租户线的 pg / neon 适配器没有收件箱,或者这一次落行失败)时,推送是它唯一的腿,重试只能把整条内容重新生成一遍。库按设备上已经有没有东西来分:
|
|
700
|
+
|
|
701
|
+
- **一条都没推出去**:照旧重试,重试时重新生成。会多花一次生成,但客户端只见过重新生成的那一份——没有收件箱兜着,这是唯一能把消息送到的办法。
|
|
702
|
+
- **已经推出去了几条**:一跳终审(错误标 `permanent: true`),没推出去的那几段就此放弃。重新生成的话,设备上会是前半截来自第一次、后半截来自第二次的两份内容。
|
|
703
|
+
|
|
704
|
+
`GET /capabilities` 的 features 里对应 `llm-permanent-errors`、`redeliver-committed-batch`、`hook-usage-total`、`max-delivery-retries`。
|
|
705
|
+
|
|
644
706
|
## 同一分组的任务不并发(`serializeBy`)
|
|
645
707
|
|
|
646
708
|
同一个角色可能有好几条定时任务。撞在一起并发跑的话,用户一口气收到两条互不知情的消息;宿主在 hook 里维护的「我刚才说过什么」台账通常是读进内存 → 改 → 整份写回,两条各改各的再写回,后写的必然盖掉前面那条。
|
|
@@ -786,6 +848,7 @@ export default createSingleUserCloudflareWorker(buildConfig, {
|
|
|
786
848
|
- `createAdapter` / `createD1Adapter` — pg·neon / Cloudflare D1 数据库适配器
|
|
787
849
|
- `runScheduledTick` — 手动触发一轮到期任务投递(自定义 cron 宿主、要调 `claimLeaseMs` 时用)
|
|
788
850
|
- `runTask` — 只跑指定那一条任务(与 cron 同一条投递链)
|
|
851
|
+
- `DEFAULT_MAX_DELIVERY_RETRIES` — 投递失败的默认重试次数(3),config 的 `maxDeliveryRetries` 不配时用它
|
|
789
852
|
- `getSchemaVersion` / `ensureSchema` / `SCHEMA_VERSION` — 表结构自查与补齐
|
|
790
853
|
- `summarizeErrorCause` — 把异常压成响应体里 `error.cause` 那个形状(自己包一层路由、想回同样形状时用同一份)
|
|
791
854
|
- `NonRetryableError` / `isNonRetryableError` — hook 侧标注「重试也好不了」的失败
|
|
@@ -800,7 +863,7 @@ export default createSingleUserCloudflareWorker(buildConfig, {
|
|
|
800
863
|
|
|
801
864
|
`@rei-standard/amsg-server/cloudflare` 子路径 —— 只含「单用户 + D1 + Web Crypto 推送」这条子图,不引用多租户装配线和 pg / neon / `web-push`,所以 D1-only 安装(不装可选数据库 peer)也能干净打包,Worker 不需要 `nodejs_compat` 兼容 flag:
|
|
802
865
|
|
|
803
|
-
- `createSingleUserCloudflareWorker` / `createSingleUserServer` / `createD1Adapter` / `runScheduledTick` / `runTask`
|
|
866
|
+
- `createSingleUserCloudflareWorker` / `createSingleUserServer` / `createD1Adapter` / `runScheduledTick` / `runTask` / `DEFAULT_MAX_DELIVERY_RETRIES`
|
|
804
867
|
- `getSchemaVersion` / `ensureSchema` / `SCHEMA_VERSION`
|
|
805
868
|
- `summarizeErrorCause` / `NonRetryableError` / `isNonRetryableError`
|
|
806
869
|
- `createWebCryptoWebPush` / `measurePushPayload` / `MAX_PUSH_PAYLOAD_BYTES` / `WEB_PUSH_MAX_BODY_BYTES` / `WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES`
|
package/dist/adapters/d1.d.ts
CHANGED
|
@@ -482,6 +482,42 @@ export class D1Adapter {
|
|
|
482
482
|
* @returns {Promise<number>} 删掉的行数
|
|
483
483
|
*/
|
|
484
484
|
discardUndeliveredOutboxForTask(userId: string, taskUuid: string): Promise<number>;
|
|
485
|
+
/**
|
|
486
|
+
* 某条任务名下、某个时刻之后落的行,**不论投递 / ack 状态**(payload 仍是密文)。
|
|
487
|
+
*
|
|
488
|
+
* 给「生成成功之后推送失败、重试只补推送」那条路用(见 lib/outbox-store.js 的
|
|
489
|
+
* findCommittedBatch):重试那一跳先来这里看这次触发的整批是不是已经落定了。
|
|
490
|
+
* 已 ack 的行也要读——客户端在两次重试之间把整批补收并 ack 了,同样说明这次
|
|
491
|
+
* 触发的内容已经生成过,不该再生成一份。
|
|
492
|
+
*
|
|
493
|
+
* `+user_id` 的一元加号是故意的:它让这一项不参与选索引,查询改走
|
|
494
|
+
* idx_outbox_created 按 created_at 收窄。否则 SQLite 会挑 (user_id, message_id)
|
|
495
|
+
* 的唯一约束索引,单用户部署下 user_id 对每一行都成立,等于把整个收件箱扫一遍
|
|
496
|
+
* (D1 按扫过的行数计费)。idx_outbox_created 不在时照样查得出来,只是退回扫表。
|
|
497
|
+
*
|
|
498
|
+
* @param {string} userId
|
|
499
|
+
* @param {string} taskUuid
|
|
500
|
+
* @param {{ sinceMs?: number, limit?: number }} [options] - sinceMs:只要
|
|
501
|
+
* created_at ≥ 它的行(epoch 毫秒);limit:最多读几行(默认 500)
|
|
502
|
+
* @returns {Promise<Array<{ id: number, message_id: string, task_uuid: string|null,
|
|
503
|
+
* session_id: string|null, message_index: number|null, total_messages: number|null,
|
|
504
|
+
* payload: string, created_at: number, delivered_at: number|null, acked_at: number|null }>>}
|
|
505
|
+
*/
|
|
506
|
+
listOutboxForTask(userId: string, taskUuid: string, { sinceMs, limit }?: {
|
|
507
|
+
sinceMs?: number;
|
|
508
|
+
limit?: number;
|
|
509
|
+
}): Promise<Array<{
|
|
510
|
+
id: number;
|
|
511
|
+
message_id: string;
|
|
512
|
+
task_uuid: string | null;
|
|
513
|
+
session_id: string | null;
|
|
514
|
+
message_index: number | null;
|
|
515
|
+
total_messages: number | null;
|
|
516
|
+
payload: string;
|
|
517
|
+
created_at: number;
|
|
518
|
+
delivered_at: number | null;
|
|
519
|
+
acked_at: number | null;
|
|
520
|
+
}>>;
|
|
485
521
|
/**
|
|
486
522
|
* 未 ack 的行(id 升序,游标翻页)。payload 仍是密文,解密在 handler。
|
|
487
523
|
*
|
|
@@ -339,6 +339,19 @@ export type DbAdapter = {
|
|
|
339
339
|
* 跳过。
|
|
340
340
|
*/
|
|
341
341
|
listUnackedOutbox?: (userId: string, sinceId: number, limit: number) => Promise<Array<any>>;
|
|
342
|
+
/**
|
|
343
|
+
* (可选;单用户/D1)某条任务名下 `created_at >= sinceMs` 的行,**不论投递 /
|
|
344
|
+
* ack 状态**,行上要带 `message_index`、`total_messages`、`payload`、
|
|
345
|
+
* `created_at`、`delivered_at`、`acked_at`。生成成功之后推送失败、任务走重试
|
|
346
|
+
* 时,重试那一跳靠它找到这次触发已经落定的那一批,只补推送、不重新生成(见
|
|
347
|
+
* lib/outbox-store.js 的 findCommittedBatch)。不实现 → 退回
|
|
348
|
+
* listUnackedOutbox 的翻页扫描,读不到已 ack 的行:客户端恰好在两次重试之间
|
|
349
|
+
* 把整批 ack 了的话,那一跳会重新生成一份。
|
|
350
|
+
*/
|
|
351
|
+
listOutboxForTask?: (userId: string, taskUuid: string, options?: {
|
|
352
|
+
sinceMs?: number;
|
|
353
|
+
limit?: number;
|
|
354
|
+
}) => Promise<Array<any>>;
|
|
342
355
|
/**
|
|
343
356
|
* (可选;单用户/D1)客户端确认收到(POST /outbox/ack,幂等)。
|
|
344
357
|
*/
|