@rei-standard/amsg-server 2.6.0-next.8 → 2.6.0-next.9

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 CHANGED
@@ -121,6 +121,51 @@ const { bytes, remainingBytes, withinLimit } = measurePushPayload(JSON.stringify
121
121
 
122
122
  装不下的内容(长文、附件详情)建议走旁路:正文存进 `client_state`,push 里只带一个引用键,客户端上线后用 `GET /client-state` 取回。单用户 Worker 的 fire-time hook 用 `ctx.writeState()` 写,见 [`examples/cloudflare-single-user/README.md`](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/server/examples/cloudflare-single-user/README.md)。
123
123
 
124
+ ## Fire 时刻 hooks
125
+
126
+ 配上 `hooks: { onBeforeFire, onLLMOutput, executeToolCalls }` 之后,AI 类任务的 prompt 不再是排程那一刻冻结的文本,而是 cron 触发时现场组装,工具也在服务端就地跑完,全程不需要客户端在线。完整用法见 [`examples/cloudflare-single-user/README.md`](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/server/examples/cloudflare-single-user/README.md) 的「Fire 时刻 hooks」。
127
+
128
+ 三个 hook 拿到的 ctx 上都有这几个口子:
129
+
130
+ | ctx 上的口子 | 干什么 |
131
+ |---|---|
132
+ | `readState(ns)` / `writeState(ns, entries)` | 读写 `client_state`,和客户端 `GET/PUT /client-state` 是同一份数据 |
133
+ | `scheduleTask(options)` | 给同一个用户再建一条定时任务 |
134
+ | `scratch` | 本次 fire 的便签对象,三个 hook 共享同一个引用,fire 结束即丢弃 |
135
+
136
+ ### `ctx.scheduleTask(options)`
137
+
138
+ 角色在这次 fire 里给自己排一条后续任务:「这条发完,一个半小时后我再接着说一句」。建出来的是一条正常的任务行,到点由 cron 触发,用户全程离线也不影响。
139
+
140
+ ```js
141
+ const result = await ctx.scheduleTask({
142
+ firstSendTime: new Date(Date.now() + 90 * 60_000).toISOString(), // 必填,ISO 字符串
143
+ messageType: 'auto', // 可选,默认继承当前任务
144
+ recurrenceType: 'none', // 可选,默认 none
145
+ metadata: { beat: 'followup' }, // 可选,整体替换当前任务的 metadata(不深合并)
146
+ uuid: `fire-${ctx.task.id}-${ctx.task.nextSendAt}`, // 可选,默认随机
147
+ });
148
+ // → { created: true, id, uuid, nextSendAt }
149
+ // 或 { created: false, reason: 'duplicate', uuid }
150
+ ```
151
+
152
+ 凭据和投递配置(`pushSubscription` / `apiUrl` / `apiKey` / `primaryModel` / `maxTokens` / `temperature` / `splitPattern`)以及 `contactName` / `avatarUrl` / `messageSubtype` / `userMessage` 从当前任务继承,宿主只说「什么时候、说什么方向」——hook 全程看不到凭据。`completePrompt` / `messages` 不继承(都置 `null`):hook 每次现场重组 prompt,把排程时冻结的旧 prompt 带过去,新任务万一走回冻结 prompt 老链路就会静默发出一条谁也没打算发的文案。
153
+
154
+ 护栏:
155
+
156
+ | 护栏 | 阈值 / 规则 | 不满足时 | 为什么 |
157
+ |---|---|---|---|
158
+ | `firstSendTime` | 必填、能解析成合法时间、至少比现在晚 **60 秒** | `RangeError` | cron 一分钟一跳,排在 60 秒内等于让下一跳立刻捡走,容易变成自己触发自己的紧密循环 |
159
+ | `messageType` | 只收 `auto` / `prompted` / `fixed` | `TypeError` | `instant` 的语义是「建行的那一刻就投递」,那条路径归 `POST /schedule-message` 管;从 fire 里造这么一行,投递时机反而说不清 |
160
+ | `messageType: 'fixed'` | 必须有 `userMessage`(自己传或继承到) | `TypeError` | 固定文本任务没有正文,就是一条永远发空的任务 |
161
+ | 单次 fire 的建任务条数 | 默认 **2 条**,factory 配置 `maxScheduledTasksPerFire` 可调(`0` = 不许自排) | `RangeError` | 模型自排后续本质上是条能无限延伸的链,没有上限就没人按停止键 |
162
+ | `uuid` 撞车 | 不当错误处理 | 返回 `{ created: false, reason: 'duplicate', uuid }` | fire 失败会整条重跑,宿主传一个由「任务 id + 触发时刻」推出来的确定性 uuid 就天然幂等 |
163
+ | 数据库适配器没有 `createTask` | — | 抛 `AGENTIC_SCHEDULE_UNSUPPORTED` | 静默成功会让宿主以为后续那条排上了,其实谁也不会触发它 |
164
+
165
+ `recurrenceType` 沿用排程接口那套 `none` / `daily` / `weekly`,别的值抛 `TypeError`。参数不合法的调用不占建任务额度;uuid 撞车占(那条任务其实已经建出来了)。
166
+
167
+ `GET /capabilities` 的 features 里有 `agentic-schedule-task`,前端可以据此判断部署的 worker 认不认这条链路。
168
+
124
169
  ## 导出(新增)
125
170
 
126
171
  - `validateLlmMessagesArray(messages)` — 同步预校验 messages 数组,返回 `string | null`(错误信息 / 通过)。和 `@rei-standard/amsg-instant` 的校验规则字节级一致。
@@ -641,6 +641,10 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
641
641
  // src/server/lib/agentic-fire.js
642
642
  var DEFAULT_MAX_TOOL_ITERATIONS = 5;
643
643
  var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
644
+ var DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
645
+ var MIN_SCHEDULE_LEAD_MS = 6e4;
646
+ var SCHEDULABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set(["auto", "prompted", "fixed"]);
647
+ var SCHEDULABLE_RECURRENCE_TYPES = /* @__PURE__ */ new Set(["none", "daily", "weekly"]);
644
648
  var SLEEP_BETWEEN_MESSAGES_MS = 1500;
645
649
  var CREDENTIAL_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["apiKey", "pushSubscription"]);
646
650
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -674,6 +678,9 @@ function normalizeBeforeFireResult(result) {
674
678
  messages: result.messages,
675
679
  maxToolIterations: result.maxToolIterations,
676
680
  totalTimeoutMs: result.totalTimeoutMs,
681
+ // 归一成「有就是非空数组,否则 undefined」,让下面每轮的透传判断能直接
682
+ // 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
683
+ // buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
677
684
  tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : void 0,
678
685
  toolChoice: result.toolChoice
679
686
  };
@@ -765,12 +772,123 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
765
772
  });
766
773
  return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized2 });
767
774
  };
775
+ const maxScheduledTasksPerFire = Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0 ? ctx.maxScheduledTasksPerFire : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
776
+ let scheduledTaskCount = 0;
777
+ const scheduleTask = async (options) => {
778
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
779
+ throw new TypeError("scheduleTask(options) \u9700\u8981\u4E00\u4E2A\u5BF9\u8C61\uFF0C\u81F3\u5C11\u5305\u542B { firstSendTime }");
780
+ }
781
+ if (typeof options.firstSendTime !== "string" || !options.firstSendTime.trim()) {
782
+ throw new RangeError("scheduleTask: firstSendTime \u5FC5\u586B\uFF0C\u4E14\u5FC5\u987B\u662F ISO 8601 \u5B57\u7B26\u4E32");
783
+ }
784
+ const firstSendAt = new Date(options.firstSendTime);
785
+ if (Number.isNaN(firstSendAt.getTime())) {
786
+ throw new RangeError(`scheduleTask: firstSendTime \u89E3\u6790\u4E0D\u51FA\u5408\u6CD5\u65F6\u95F4\uFF1A${options.firstSendTime}`);
787
+ }
788
+ const earliest = nowFn() + MIN_SCHEDULE_LEAD_MS;
789
+ if (firstSendAt.getTime() < earliest) {
790
+ throw new RangeError(
791
+ `scheduleTask: firstSendTime \u81F3\u5C11\u8981\u6BD4\u73B0\u5728\u665A ${MIN_SCHEDULE_LEAD_MS / 1e3} \u79D2\uFF08\u6700\u65E9\u53EF\u6392 ${new Date(earliest).toISOString()}\uFF0C\u6536\u5230 ${firstSendAt.toISOString()}\uFF09\u2014\u2014cron \u4E00\u5206\u949F\u4E00\u8DF3\uFF0C\u6392\u5F97\u66F4\u8FD1\u7B49\u4E8E\u8BA9\u4E0B\u4E00\u8DF3\u7ACB\u523B\u6361\u8D70`
792
+ );
793
+ }
794
+ const nextSendAt = firstSendAt.toISOString();
795
+ const inheritedType = options.messageType == null;
796
+ const messageType = inheritedType ? decryptedPayload.messageType : options.messageType;
797
+ if (messageType === "instant") {
798
+ throw new TypeError(
799
+ "scheduleTask: messageType \u4E0D\u80FD\u662F 'instant'\u2014\u2014instant \u7684\u8BED\u4E49\u662F\u300C\u5EFA\u884C\u7684\u90A3\u4E00\u523B\u5C31\u6295\u9012\u300D\uFF0C\u90A3\u6761\u8DEF\u5F84\u5F52 POST /schedule-message \u7BA1\uFF1B\u4ECE fire \u91CC\u5EFA\u4E00\u6761 instant\uFF0C\u6295\u9012\u65F6\u673A\u53CD\u800C\u8BF4\u4E0D\u6E05\u3002" + (inheritedType ? "\uFF08\u8FD9\u4E2A instant \u662F\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u6765\u7684\uFF0C\u663E\u5F0F\u4F20 auto / prompted / fixed \u8986\u76D6\u5B83\u3002\uFF09" : "")
800
+ );
801
+ }
802
+ if (!SCHEDULABLE_MESSAGE_TYPES.has(messageType)) {
803
+ throw new TypeError(
804
+ `scheduleTask: messageType \u53EA\u80FD\u662F auto / prompted / fixed\uFF0C\u6536\u5230 ${JSON.stringify(messageType)}`
805
+ );
806
+ }
807
+ const recurrenceType = options.recurrenceType == null ? "none" : options.recurrenceType;
808
+ if (!SCHEDULABLE_RECURRENCE_TYPES.has(recurrenceType)) {
809
+ throw new TypeError(
810
+ `scheduleTask: recurrenceType \u53EA\u80FD\u662F none / daily / weekly\uFF0C\u6536\u5230 ${JSON.stringify(recurrenceType)}`
811
+ );
812
+ }
813
+ const contactName = options.contactName === void 0 ? decryptedPayload.contactName : options.contactName;
814
+ if (typeof contactName !== "string" || !contactName.trim()) {
815
+ throw new TypeError("scheduleTask: contactName \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u9ED8\u8BA4\u7EE7\u627F\u5F53\u524D\u4EFB\u52A1\uFF09");
816
+ }
817
+ const userMessage = options.userMessage === void 0 ? _nullishCoalesce(decryptedPayload.userMessage, () => ( null)) : options.userMessage;
818
+ if (messageType === "fixed" && (typeof userMessage !== "string" || !userMessage.trim())) {
819
+ throw new TypeError(
820
+ "scheduleTask: messageType 'fixed' \u5FC5\u987B\u6709 userMessage\uFF08\u81EA\u5DF1\u4F20\uFF0C\u6216\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u5230\uFF09\u2014\u2014\u56FA\u5B9A\u6587\u672C\u4EFB\u52A1\u6CA1\u6709\u6B63\u6587\uFF0C\u5C31\u662F\u4E00\u6761\u6C38\u8FDC\u53D1\u7A7A\u7684\u4EFB\u52A1"
821
+ );
822
+ }
823
+ const metadata = options.metadata === void 0 ? decryptedPayload.metadata || {} : options.metadata;
824
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
825
+ throw new TypeError("scheduleTask: metadata \u5FC5\u987B\u662F\u666E\u901A\u5BF9\u8C61\uFF08\u6574\u4F53\u66FF\u6362\u5F53\u524D\u4EFB\u52A1\u7684 metadata\uFF0C\u4E0D\u505A\u6DF1\u5408\u5E76\uFF09");
826
+ }
827
+ const uuid = options.uuid == null ? randomUUID() : options.uuid;
828
+ if (typeof uuid !== "string" || !uuid.trim()) {
829
+ throw new TypeError("scheduleTask: uuid \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
830
+ }
831
+ if (scheduledTaskCount >= maxScheduledTasksPerFire) {
832
+ throw new RangeError(
833
+ `scheduleTask: \u5355\u6B21 fire \u6700\u591A\u5EFA ${maxScheduledTasksPerFire} \u6761\u4EFB\u52A1\uFF08factory \u914D\u7F6E maxScheduledTasksPerFire \u53EF\u8C03\uFF09\uFF0C\u8FD9\u662F\u7B2C ${scheduledTaskCount + 1} \u6761`
834
+ );
835
+ }
836
+ if (!ctx.db || typeof ctx.db.createTask !== "function") {
837
+ throw new Error("AGENTIC_SCHEDULE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5EFA\u4EFB\u52A1\uFF08\u7F3A createTask\uFF09");
838
+ }
839
+ scheduledTaskCount++;
840
+ const fullTaskData = {
841
+ contactName,
842
+ avatarUrl: options.avatarUrl === void 0 ? decryptedPayload.avatarUrl || null : options.avatarUrl || null,
843
+ messageType,
844
+ messageSubtype: (options.messageSubtype === void 0 ? decryptedPayload.messageSubtype : options.messageSubtype) || "chat",
845
+ userMessage: userMessage || null,
846
+ firstSendTime: nextSendAt,
847
+ recurrenceType,
848
+ apiUrl: decryptedPayload.apiUrl || null,
849
+ apiKey: decryptedPayload.apiKey || null,
850
+ primaryModel: decryptedPayload.primaryModel || null,
851
+ // 见文件头:fire-time hook 每次现场重组 prompt,旧 prompt 带过去只会在新
852
+ // 任务走回老链路时顶替宿主的意图。
853
+ completePrompt: null,
854
+ messages: null,
855
+ maxTokens: _nullishCoalesce(decryptedPayload.maxTokens, () => ( null)),
856
+ temperature: _nullishCoalesce(decryptedPayload.temperature, () => ( null)),
857
+ splitPattern: _nullishCoalesce(decryptedPayload.splitPattern, () => ( null)),
858
+ pushSubscription: decryptedPayload.pushSubscription,
859
+ metadata
860
+ };
861
+ const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
862
+ let created;
863
+ try {
864
+ created = await ctx.db.createTask({
865
+ user_id: task.user_id,
866
+ uuid,
867
+ encrypted_payload: encryptedPayload,
868
+ next_send_at: nextSendAt,
869
+ message_type: messageType
870
+ });
871
+ } catch (error) {
872
+ if (isUniqueViolation(error)) return { created: false, reason: "duplicate", uuid };
873
+ throw error;
874
+ }
875
+ if (!created) {
876
+ throw new Error("AGENTIC_SCHEDULE_FAILED: createTask \u6CA1\u6709\u8FD4\u56DE\u65B0\u5EFA\u7684\u4EFB\u52A1\u884C");
877
+ }
878
+ return {
879
+ created: true,
880
+ id: _nullishCoalesce(created.id, () => ( null)),
881
+ uuid: _nullishCoalesce(created.uuid, () => ( uuid)),
882
+ nextSendAt: _nullishCoalesce(created.next_send_at, () => ( nextSendAt))
883
+ };
884
+ };
768
885
  const scratch = {};
769
886
  const fireCtx = Object.freeze({
770
887
  task: buildHookTask(task, decryptedPayload),
771
888
  userId: task.user_id,
772
889
  readState,
773
890
  writeState,
891
+ scheduleTask,
774
892
  now: new Date(nowFn()),
775
893
  scratch
776
894
  });
@@ -819,7 +937,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
819
937
  scratch
820
938
  }),
821
939
  readState,
822
- writeState
940
+ writeState,
941
+ scheduleTask
823
942
  });
824
943
  const decision = await hooks.onLLMOutput(sessionCtx);
825
944
  _amsgshared.assertValidDecision.call(void 0, decision, { inlineToolCalls: true });
@@ -2277,7 +2396,7 @@ function createClientStateHandler(ctx) {
2277
2396
  }
2278
2397
 
2279
2398
  // src/server/lib/version.js
2280
- var SERVER_VERSION = true ? "2.6.0-next.8" : "0.0.0-dev";
2399
+ var SERVER_VERSION = true ? "2.6.0-next.9" : "0.0.0-dev";
2281
2400
 
2282
2401
  // src/server/handlers/capabilities.js
2283
2402
  var SERVER_FEATURES = Object.freeze([
@@ -2288,6 +2407,7 @@ var SERVER_FEATURES = Object.freeze([
2288
2407
  "agentic-scratch",
2289
2408
  "agentic-write-state",
2290
2409
  "agentic-fire-tools",
2410
+ "agentic-schedule-task",
2291
2411
  "vapid-public-key"
2292
2412
  ]);
2293
2413
  function createCapabilitiesHandler(ctx) {
@@ -2329,7 +2449,9 @@ function createSingleUserServer(config) {
2329
2449
  hooks: config.hooks || null,
2330
2450
  maxToolIterations: config.maxToolIterations,
2331
2451
  totalTimeoutMs: config.totalTimeoutMs,
2332
- maxStateValueBytes: config.maxStateValueBytes
2452
+ maxStateValueBytes: config.maxStateValueBytes,
2453
+ // hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
2454
+ maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
2333
2455
  };
2334
2456
  return {
2335
2457
  ctx,
@@ -2668,6 +2790,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
2668
2790
  totalTimeoutMs: cfg.totalTimeoutMs,
2669
2791
  // hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
2670
2792
  maxStateValueBytes: cfg.maxStateValueBytes,
2793
+ // hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
2794
+ maxScheduledTasksPerFire: cfg.maxScheduledTasksPerFire,
2671
2795
  // 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
2672
2796
  claimLeaseMs: cfg.claimLeaseMs
2673
2797
  });
@@ -641,6 +641,10 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
641
641
  // src/server/lib/agentic-fire.js
642
642
  var DEFAULT_MAX_TOOL_ITERATIONS = 5;
643
643
  var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
644
+ var DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
645
+ var MIN_SCHEDULE_LEAD_MS = 6e4;
646
+ var SCHEDULABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set(["auto", "prompted", "fixed"]);
647
+ var SCHEDULABLE_RECURRENCE_TYPES = /* @__PURE__ */ new Set(["none", "daily", "weekly"]);
644
648
  var SLEEP_BETWEEN_MESSAGES_MS = 1500;
645
649
  var CREDENTIAL_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["apiKey", "pushSubscription"]);
646
650
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -674,6 +678,9 @@ function normalizeBeforeFireResult(result) {
674
678
  messages: result.messages,
675
679
  maxToolIterations: result.maxToolIterations,
676
680
  totalTimeoutMs: result.totalTimeoutMs,
681
+ // 归一成「有就是非空数组,否则 undefined」,让下面每轮的透传判断能直接
682
+ // 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
683
+ // buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
677
684
  tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : void 0,
678
685
  toolChoice: result.toolChoice
679
686
  };
@@ -765,12 +772,123 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
765
772
  });
766
773
  return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized2 });
767
774
  };
775
+ const maxScheduledTasksPerFire = Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0 ? ctx.maxScheduledTasksPerFire : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
776
+ let scheduledTaskCount = 0;
777
+ const scheduleTask = async (options) => {
778
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
779
+ throw new TypeError("scheduleTask(options) \u9700\u8981\u4E00\u4E2A\u5BF9\u8C61\uFF0C\u81F3\u5C11\u5305\u542B { firstSendTime }");
780
+ }
781
+ if (typeof options.firstSendTime !== "string" || !options.firstSendTime.trim()) {
782
+ throw new RangeError("scheduleTask: firstSendTime \u5FC5\u586B\uFF0C\u4E14\u5FC5\u987B\u662F ISO 8601 \u5B57\u7B26\u4E32");
783
+ }
784
+ const firstSendAt = new Date(options.firstSendTime);
785
+ if (Number.isNaN(firstSendAt.getTime())) {
786
+ throw new RangeError(`scheduleTask: firstSendTime \u89E3\u6790\u4E0D\u51FA\u5408\u6CD5\u65F6\u95F4\uFF1A${options.firstSendTime}`);
787
+ }
788
+ const earliest = nowFn() + MIN_SCHEDULE_LEAD_MS;
789
+ if (firstSendAt.getTime() < earliest) {
790
+ throw new RangeError(
791
+ `scheduleTask: firstSendTime \u81F3\u5C11\u8981\u6BD4\u73B0\u5728\u665A ${MIN_SCHEDULE_LEAD_MS / 1e3} \u79D2\uFF08\u6700\u65E9\u53EF\u6392 ${new Date(earliest).toISOString()}\uFF0C\u6536\u5230 ${firstSendAt.toISOString()}\uFF09\u2014\u2014cron \u4E00\u5206\u949F\u4E00\u8DF3\uFF0C\u6392\u5F97\u66F4\u8FD1\u7B49\u4E8E\u8BA9\u4E0B\u4E00\u8DF3\u7ACB\u523B\u6361\u8D70`
792
+ );
793
+ }
794
+ const nextSendAt = firstSendAt.toISOString();
795
+ const inheritedType = options.messageType == null;
796
+ const messageType = inheritedType ? decryptedPayload.messageType : options.messageType;
797
+ if (messageType === "instant") {
798
+ throw new TypeError(
799
+ "scheduleTask: messageType \u4E0D\u80FD\u662F 'instant'\u2014\u2014instant \u7684\u8BED\u4E49\u662F\u300C\u5EFA\u884C\u7684\u90A3\u4E00\u523B\u5C31\u6295\u9012\u300D\uFF0C\u90A3\u6761\u8DEF\u5F84\u5F52 POST /schedule-message \u7BA1\uFF1B\u4ECE fire \u91CC\u5EFA\u4E00\u6761 instant\uFF0C\u6295\u9012\u65F6\u673A\u53CD\u800C\u8BF4\u4E0D\u6E05\u3002" + (inheritedType ? "\uFF08\u8FD9\u4E2A instant \u662F\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u6765\u7684\uFF0C\u663E\u5F0F\u4F20 auto / prompted / fixed \u8986\u76D6\u5B83\u3002\uFF09" : "")
800
+ );
801
+ }
802
+ if (!SCHEDULABLE_MESSAGE_TYPES.has(messageType)) {
803
+ throw new TypeError(
804
+ `scheduleTask: messageType \u53EA\u80FD\u662F auto / prompted / fixed\uFF0C\u6536\u5230 ${JSON.stringify(messageType)}`
805
+ );
806
+ }
807
+ const recurrenceType = options.recurrenceType == null ? "none" : options.recurrenceType;
808
+ if (!SCHEDULABLE_RECURRENCE_TYPES.has(recurrenceType)) {
809
+ throw new TypeError(
810
+ `scheduleTask: recurrenceType \u53EA\u80FD\u662F none / daily / weekly\uFF0C\u6536\u5230 ${JSON.stringify(recurrenceType)}`
811
+ );
812
+ }
813
+ const contactName = options.contactName === void 0 ? decryptedPayload.contactName : options.contactName;
814
+ if (typeof contactName !== "string" || !contactName.trim()) {
815
+ throw new TypeError("scheduleTask: contactName \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u9ED8\u8BA4\u7EE7\u627F\u5F53\u524D\u4EFB\u52A1\uFF09");
816
+ }
817
+ const userMessage = options.userMessage === void 0 ? decryptedPayload.userMessage ?? null : options.userMessage;
818
+ if (messageType === "fixed" && (typeof userMessage !== "string" || !userMessage.trim())) {
819
+ throw new TypeError(
820
+ "scheduleTask: messageType 'fixed' \u5FC5\u987B\u6709 userMessage\uFF08\u81EA\u5DF1\u4F20\uFF0C\u6216\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u5230\uFF09\u2014\u2014\u56FA\u5B9A\u6587\u672C\u4EFB\u52A1\u6CA1\u6709\u6B63\u6587\uFF0C\u5C31\u662F\u4E00\u6761\u6C38\u8FDC\u53D1\u7A7A\u7684\u4EFB\u52A1"
821
+ );
822
+ }
823
+ const metadata = options.metadata === void 0 ? decryptedPayload.metadata || {} : options.metadata;
824
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
825
+ throw new TypeError("scheduleTask: metadata \u5FC5\u987B\u662F\u666E\u901A\u5BF9\u8C61\uFF08\u6574\u4F53\u66FF\u6362\u5F53\u524D\u4EFB\u52A1\u7684 metadata\uFF0C\u4E0D\u505A\u6DF1\u5408\u5E76\uFF09");
826
+ }
827
+ const uuid = options.uuid == null ? randomUUID() : options.uuid;
828
+ if (typeof uuid !== "string" || !uuid.trim()) {
829
+ throw new TypeError("scheduleTask: uuid \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
830
+ }
831
+ if (scheduledTaskCount >= maxScheduledTasksPerFire) {
832
+ throw new RangeError(
833
+ `scheduleTask: \u5355\u6B21 fire \u6700\u591A\u5EFA ${maxScheduledTasksPerFire} \u6761\u4EFB\u52A1\uFF08factory \u914D\u7F6E maxScheduledTasksPerFire \u53EF\u8C03\uFF09\uFF0C\u8FD9\u662F\u7B2C ${scheduledTaskCount + 1} \u6761`
834
+ );
835
+ }
836
+ if (!ctx.db || typeof ctx.db.createTask !== "function") {
837
+ throw new Error("AGENTIC_SCHEDULE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5EFA\u4EFB\u52A1\uFF08\u7F3A createTask\uFF09");
838
+ }
839
+ scheduledTaskCount++;
840
+ const fullTaskData = {
841
+ contactName,
842
+ avatarUrl: options.avatarUrl === void 0 ? decryptedPayload.avatarUrl || null : options.avatarUrl || null,
843
+ messageType,
844
+ messageSubtype: (options.messageSubtype === void 0 ? decryptedPayload.messageSubtype : options.messageSubtype) || "chat",
845
+ userMessage: userMessage || null,
846
+ firstSendTime: nextSendAt,
847
+ recurrenceType,
848
+ apiUrl: decryptedPayload.apiUrl || null,
849
+ apiKey: decryptedPayload.apiKey || null,
850
+ primaryModel: decryptedPayload.primaryModel || null,
851
+ // 见文件头:fire-time hook 每次现场重组 prompt,旧 prompt 带过去只会在新
852
+ // 任务走回老链路时顶替宿主的意图。
853
+ completePrompt: null,
854
+ messages: null,
855
+ maxTokens: decryptedPayload.maxTokens ?? null,
856
+ temperature: decryptedPayload.temperature ?? null,
857
+ splitPattern: decryptedPayload.splitPattern ?? null,
858
+ pushSubscription: decryptedPayload.pushSubscription,
859
+ metadata
860
+ };
861
+ const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
862
+ let created;
863
+ try {
864
+ created = await ctx.db.createTask({
865
+ user_id: task.user_id,
866
+ uuid,
867
+ encrypted_payload: encryptedPayload,
868
+ next_send_at: nextSendAt,
869
+ message_type: messageType
870
+ });
871
+ } catch (error) {
872
+ if (isUniqueViolation(error)) return { created: false, reason: "duplicate", uuid };
873
+ throw error;
874
+ }
875
+ if (!created) {
876
+ throw new Error("AGENTIC_SCHEDULE_FAILED: createTask \u6CA1\u6709\u8FD4\u56DE\u65B0\u5EFA\u7684\u4EFB\u52A1\u884C");
877
+ }
878
+ return {
879
+ created: true,
880
+ id: created.id ?? null,
881
+ uuid: created.uuid ?? uuid,
882
+ nextSendAt: created.next_send_at ?? nextSendAt
883
+ };
884
+ };
768
885
  const scratch = {};
769
886
  const fireCtx = Object.freeze({
770
887
  task: buildHookTask(task, decryptedPayload),
771
888
  userId: task.user_id,
772
889
  readState,
773
890
  writeState,
891
+ scheduleTask,
774
892
  now: new Date(nowFn()),
775
893
  scratch
776
894
  });
@@ -819,7 +937,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
819
937
  scratch
820
938
  }),
821
939
  readState,
822
- writeState
940
+ writeState,
941
+ scheduleTask
823
942
  });
824
943
  const decision = await hooks.onLLMOutput(sessionCtx);
825
944
  assertValidDecision(decision, { inlineToolCalls: true });
@@ -2277,7 +2396,7 @@ function createClientStateHandler(ctx) {
2277
2396
  }
2278
2397
 
2279
2398
  // src/server/lib/version.js
2280
- var SERVER_VERSION = true ? "2.6.0-next.8" : "0.0.0-dev";
2399
+ var SERVER_VERSION = true ? "2.6.0-next.9" : "0.0.0-dev";
2281
2400
 
2282
2401
  // src/server/handlers/capabilities.js
2283
2402
  var SERVER_FEATURES = Object.freeze([
@@ -2288,6 +2407,7 @@ var SERVER_FEATURES = Object.freeze([
2288
2407
  "agentic-scratch",
2289
2408
  "agentic-write-state",
2290
2409
  "agentic-fire-tools",
2410
+ "agentic-schedule-task",
2291
2411
  "vapid-public-key"
2292
2412
  ]);
2293
2413
  function createCapabilitiesHandler(ctx) {
@@ -2329,7 +2449,9 @@ function createSingleUserServer(config) {
2329
2449
  hooks: config.hooks || null,
2330
2450
  maxToolIterations: config.maxToolIterations,
2331
2451
  totalTimeoutMs: config.totalTimeoutMs,
2332
- maxStateValueBytes: config.maxStateValueBytes
2452
+ maxStateValueBytes: config.maxStateValueBytes,
2453
+ // hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
2454
+ maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
2333
2455
  };
2334
2456
  return {
2335
2457
  ctx,
@@ -2668,6 +2790,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
2668
2790
  totalTimeoutMs: cfg.totalTimeoutMs,
2669
2791
  // hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
2670
2792
  maxStateValueBytes: cfg.maxStateValueBytes,
2793
+ // hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
2794
+ maxScheduledTasksPerFire: cfg.maxScheduledTasksPerFire,
2671
2795
  // 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
2672
2796
  claimLeaseMs: cfg.claimLeaseMs
2673
2797
  });
@@ -1183,6 +1183,32 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
1183
1183
  * 补完那轮模型仍可能再发起调用。库不看 tools 的内容,执行照旧是
1184
1184
  * executeToolCalls 的事。
1185
1185
  *
1186
+ * 自排后续任务:fireCtx 和每轮的 sessionCtx 上都挂着 scheduleTask,宿主用它
1187
+ * 在这次 fire 里给同一个用户再建一条定时任务(「这条发完,一个半小时后我再
1188
+ * 接着说一句」)。写口在 sessionCtx 上也给,是因为要不要接着说往往是看完
1189
+ * LLM 输出才定的,那时 onBeforeFire 早已返回。凭据(pushSubscription /
1190
+ * apiKey)和投递配置从当前任务继承、宿主全程看不到(与 buildHookTask 屏蔽
1191
+ * 凭据同一个原则),宿主只提供「什么时候、说什么方向」。
1192
+ *
1193
+ * 新任务不继承 completePrompt / messages,两者都置 null:fire-time hook 每次
1194
+ * 现场重组 prompt,把排程时冻结的旧 prompt 带过去,会在新任务万一走回老链路
1195
+ * (onBeforeFire 返回 null、或那次部署没配 hooks)时静默顶替宿主的意图——
1196
+ * 与其发一条来路不明的旧文案,不如让它当场失败、走既有的重试/标记逻辑。
1197
+ *
1198
+ * 护栏(几条都是这个能力能不能上线的关键):
1199
+ * - firstSendTime 至少比现在晚 MIN_SCHEDULE_LEAD_MS(60 秒)。cron 每分钟
1200
+ * 一跳,排在 60 秒内等于让下一跳立刻捡走,容易变成自己触发自己的紧密循环。
1201
+ * - messageType 只收 auto / prompted / fixed,不收 instant。instant 的语义是
1202
+ * 「建行的那一刻就投递」,那条路径归 POST /schedule-message 管;从 fire 里
1203
+ * 造这么一行,投递时机反而说不清(这里不投,要等 cron 捞到才发)。
1204
+ * - 单次 fire 的建任务上限默认 DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE 条,
1205
+ * factory 配置 ctx.maxScheduledTasksPerFire 可覆盖(0 表示不许自排)。模型
1206
+ * 自排后续本质上是个可以无限延伸的链:没有上限的话,一次 fire 里连排十条、
1207
+ * 或者每条任务都排下一条,就成了谁也没按下停止键的循环。
1208
+ * - uuid 冲突不抛错,返回 { created: false, reason: 'duplicate' }。fire 失败
1209
+ * 会整条重跑(run-tick 的重试语义),宿主传一个由「任务 id + 触发时刻」推
1210
+ * 出来的确定性 uuid 就天然幂等,不会每重试一次多排一条。
1211
+ *
1186
1212
  * Budget guards, both factory-level (ctx.maxToolIterations /
1187
1213
  * ctx.totalTimeoutMs) and per-fire (onBeforeFire may return
1188
1214
  * { messages, maxToolIterations?, totalTimeoutMs? } to override for one
@@ -1197,6 +1223,17 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
1197
1223
  const DEFAULT_MAX_TOOL_ITERATIONS = 5;
1198
1224
  const DEFAULT_TOTAL_TIMEOUT_MS = 240_000;
1199
1225
 
1226
+ // 一次 fire 里最多能给自己排几条后续任务(ctx.maxScheduledTasksPerFire 可覆盖)。
1227
+ const DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
1228
+ // 自排任务的最小提前量:cron 一分钟一跳,比这更近等于让下一跳立刻捡走。
1229
+ const MIN_SCHEDULE_LEAD_MS = 60_000;
1230
+
1231
+ // 自排任务允许的类型。instant 归 POST /schedule-message 那条同步路径管。
1232
+ const SCHEDULABLE_MESSAGE_TYPES = new Set(['auto', 'prompted', 'fixed']);
1233
+ // 与 validateScheduleMessagePayload 同一套;run-tick 只认得这三种,别的值会让
1234
+ // 任务发完之后推进不到下一次。
1235
+ const SCHEDULABLE_RECURRENCE_TYPES = new Set(['none', 'daily', 'weekly']);
1236
+
1200
1237
  // Same pacing as the legacy path / amsg-instant.
1201
1238
  const SLEEP_BETWEEN_MESSAGES_MS$1 = 1500;
1202
1239
 
@@ -1251,6 +1288,9 @@ function normalizeBeforeFireResult(result) {
1251
1288
  messages: result.messages,
1252
1289
  maxToolIterations: result.maxToolIterations,
1253
1290
  totalTimeoutMs: result.totalTimeoutMs,
1291
+ // 归一成「有就是非空数组,否则 undefined」,让下面每轮的透传判断能直接
1292
+ // 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
1293
+ // buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
1254
1294
  tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : undefined,
1255
1295
  toolChoice: result.toolChoice,
1256
1296
  };
@@ -1281,7 +1321,7 @@ function firstPositiveNumber(values, fallback) {
1281
1321
  * @param {import('../adapters/interface.js').TaskRow} args.task
1282
1322
  * @param {Object} args.decryptedPayload - decrypted task payload (has credentials; they stop here)
1283
1323
  * @param {string} args.userKey - per-user storage key (for readState decryption)
1284
- * @param {Object} args.ctx - processor ctx ({ db, webpush, vapid, hooks, maxToolIterations, totalTimeoutMs })
1324
+ * @param {Object} args.ctx - processor ctx ({ db, webpush, vapid, hooks, maxToolIterations, totalTimeoutMs, maxScheduledTasksPerFire })
1285
1325
  * @returns {Promise<{ handled: false } | { handled: true, result: { success: true, messagesSent: number, status: 'finished'|'skipped', iterations: number } }>}
1286
1326
  * `handled: false` → caller falls back to the legacy frozen-prompt path
1287
1327
  * (onBeforeFire returned null).
@@ -1400,6 +1440,174 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1400
1440
  return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized });
1401
1441
  };
1402
1442
 
1443
+ const maxScheduledTasksPerFire =
1444
+ Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0
1445
+ ? ctx.maxScheduledTasksPerFire
1446
+ : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
1447
+ // 本次 fire 已经用掉的建任务额度。校验通过、真的要写库时才 +1,参数不合法的
1448
+ // 调用不占额度;uuid 撞车(那条任务其实已经建出来了)照样占。
1449
+ let scheduledTaskCount = 0;
1450
+
1451
+ /**
1452
+ * scheduleTask(options) —— 在这次 fire 里给**同一个用户**再建一条定时任务。
1453
+ *
1454
+ * 典型用法:角色发完这条,想过一个半小时再接着说一句。以前只能写进
1455
+ * client_state 等客户端上线重放,用户一直不上线这条链就断了;建成任务行之后,
1456
+ * 到点由 cron 直接触发,跟别的定时消息一样。
1457
+ *
1458
+ * 凭据与投递配置(pushSubscription / apiUrl / apiKey / primaryModel /
1459
+ * maxTokens / temperature / splitPattern)以及 contactName / avatarUrl /
1460
+ * messageSubtype / userMessage 从当前任务继承,宿主只说「什么时候、说什么
1461
+ * 方向」——hook 全程看不到凭据。completePrompt / messages 不继承(都置 null),
1462
+ * 见文件头。
1463
+ *
1464
+ * @param {Object} options
1465
+ * @param {string} options.firstSendTime - 必填,ISO 8601;至少比现在晚 60 秒
1466
+ * @param {'none'|'daily'|'weekly'} [options.recurrenceType='none']
1467
+ * @param {'auto'|'prompted'|'fixed'} [options.messageType] - 默认继承当前任务
1468
+ * @param {Object} [options.metadata] - 整体替换(不是深合并)当前任务的 metadata
1469
+ * @param {string} [options.contactName] - 默认继承
1470
+ * @param {string|null} [options.avatarUrl] - 默认继承
1471
+ * @param {string} [options.messageSubtype] - 默认继承
1472
+ * @param {string|null} [options.userMessage] - 默认继承;messageType 'fixed' 时必须有
1473
+ * @param {string} [options.uuid] - 默认 randomUUID();传确定性 uuid 可做重试幂等
1474
+ * @returns {Promise<{ created: true, id: number|null, uuid: string, nextSendAt: string }
1475
+ * | { created: false, reason: 'duplicate', uuid: string }>}
1476
+ */
1477
+ const scheduleTask = async (options) => {
1478
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
1479
+ throw new TypeError('scheduleTask(options) 需要一个对象,至少包含 { firstSendTime }');
1480
+ }
1481
+
1482
+ // ---- 触发时刻 ----
1483
+ if (typeof options.firstSendTime !== 'string' || !options.firstSendTime.trim()) {
1484
+ throw new RangeError('scheduleTask: firstSendTime 必填,且必须是 ISO 8601 字符串');
1485
+ }
1486
+ const firstSendAt = new Date(options.firstSendTime);
1487
+ if (Number.isNaN(firstSendAt.getTime())) {
1488
+ throw new RangeError(`scheduleTask: firstSendTime 解析不出合法时间:${options.firstSendTime}`);
1489
+ }
1490
+ const earliest = nowFn() + MIN_SCHEDULE_LEAD_MS;
1491
+ if (firstSendAt.getTime() < earliest) {
1492
+ throw new RangeError(
1493
+ `scheduleTask: firstSendTime 至少要比现在晚 ${MIN_SCHEDULE_LEAD_MS / 1000} 秒` +
1494
+ `(最早可排 ${new Date(earliest).toISOString()},收到 ${firstSendAt.toISOString()})` +
1495
+ '——cron 一分钟一跳,排得更近等于让下一跳立刻捡走'
1496
+ );
1497
+ }
1498
+ const nextSendAt = firstSendAt.toISOString();
1499
+
1500
+ // ---- 类型 ----
1501
+ const inheritedType = options.messageType == null;
1502
+ const messageType = inheritedType ? decryptedPayload.messageType : options.messageType;
1503
+ if (messageType === 'instant') {
1504
+ throw new TypeError(
1505
+ "scheduleTask: messageType 不能是 'instant'——instant 的语义是「建行的那一刻就投递」," +
1506
+ '那条路径归 POST /schedule-message 管;从 fire 里建一条 instant,投递时机反而说不清。' +
1507
+ (inheritedType ? '(这个 instant 是从当前任务继承来的,显式传 auto / prompted / fixed 覆盖它。)' : '')
1508
+ );
1509
+ }
1510
+ if (!SCHEDULABLE_MESSAGE_TYPES.has(messageType)) {
1511
+ throw new TypeError(
1512
+ `scheduleTask: messageType 只能是 auto / prompted / fixed,收到 ${JSON.stringify(messageType)}`
1513
+ );
1514
+ }
1515
+
1516
+ const recurrenceType = options.recurrenceType == null ? 'none' : options.recurrenceType;
1517
+ if (!SCHEDULABLE_RECURRENCE_TYPES.has(recurrenceType)) {
1518
+ throw new TypeError(
1519
+ `scheduleTask: recurrenceType 只能是 none / daily / weekly,收到 ${JSON.stringify(recurrenceType)}`
1520
+ );
1521
+ }
1522
+
1523
+ // ---- 继承 + 覆盖 ----
1524
+ const contactName = options.contactName === undefined ? decryptedPayload.contactName : options.contactName;
1525
+ if (typeof contactName !== 'string' || !contactName.trim()) {
1526
+ throw new TypeError('scheduleTask: contactName 必须是非空字符串(默认继承当前任务)');
1527
+ }
1528
+ const userMessage = options.userMessage === undefined
1529
+ ? (decryptedPayload.userMessage ?? null)
1530
+ : options.userMessage;
1531
+ if (messageType === 'fixed' && (typeof userMessage !== 'string' || !userMessage.trim())) {
1532
+ throw new TypeError(
1533
+ "scheduleTask: messageType 'fixed' 必须有 userMessage(自己传,或从当前任务继承到)" +
1534
+ '——固定文本任务没有正文,就是一条永远发空的任务'
1535
+ );
1536
+ }
1537
+ const metadata = options.metadata === undefined ? (decryptedPayload.metadata || {}) : options.metadata;
1538
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
1539
+ throw new TypeError('scheduleTask: metadata 必须是普通对象(整体替换当前任务的 metadata,不做深合并)');
1540
+ }
1541
+ const uuid = options.uuid == null ? randomUUID() : options.uuid;
1542
+ if (typeof uuid !== 'string' || !uuid.trim()) {
1543
+ throw new TypeError('scheduleTask: uuid 必须是非空字符串');
1544
+ }
1545
+
1546
+ // ---- 额度 ----
1547
+ if (scheduledTaskCount >= maxScheduledTasksPerFire) {
1548
+ throw new RangeError(
1549
+ `scheduleTask: 单次 fire 最多建 ${maxScheduledTasksPerFire} 条任务` +
1550
+ `(factory 配置 maxScheduledTasksPerFire 可调),这是第 ${scheduledTaskCount + 1} 条`
1551
+ );
1552
+ }
1553
+ // readState 在适配器不支持时降级成空数组;建任务不一样,静默成功会让宿主
1554
+ // 以为后续那条已经排上了,其实谁也不会触发它。
1555
+ if (!ctx.db || typeof ctx.db.createTask !== 'function') {
1556
+ throw new Error('AGENTIC_SCHEDULE_UNSUPPORTED: 当前数据库适配器不支持建任务(缺 createTask)');
1557
+ }
1558
+ scheduledTaskCount++;
1559
+
1560
+ // 字段构成与 POST /schedule-message 落库的那份保持一致,只是走库内部、不经 HTTP。
1561
+ const fullTaskData = {
1562
+ contactName,
1563
+ avatarUrl: options.avatarUrl === undefined ? (decryptedPayload.avatarUrl || null) : (options.avatarUrl || null),
1564
+ messageType,
1565
+ messageSubtype: (options.messageSubtype === undefined ? decryptedPayload.messageSubtype : options.messageSubtype) || 'chat',
1566
+ userMessage: userMessage || null,
1567
+ firstSendTime: nextSendAt,
1568
+ recurrenceType,
1569
+ apiUrl: decryptedPayload.apiUrl || null,
1570
+ apiKey: decryptedPayload.apiKey || null,
1571
+ primaryModel: decryptedPayload.primaryModel || null,
1572
+ // 见文件头:fire-time hook 每次现场重组 prompt,旧 prompt 带过去只会在新
1573
+ // 任务走回老链路时顶替宿主的意图。
1574
+ completePrompt: null,
1575
+ messages: null,
1576
+ maxTokens: decryptedPayload.maxTokens ?? null,
1577
+ temperature: decryptedPayload.temperature ?? null,
1578
+ splitPattern: decryptedPayload.splitPattern ?? null,
1579
+ pushSubscription: decryptedPayload.pushSubscription,
1580
+ metadata,
1581
+ };
1582
+
1583
+ const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
1584
+
1585
+ let created;
1586
+ try {
1587
+ created = await ctx.db.createTask({
1588
+ user_id: task.user_id,
1589
+ uuid,
1590
+ encrypted_payload: encryptedPayload,
1591
+ next_send_at: nextSendAt,
1592
+ message_type: messageType,
1593
+ });
1594
+ } catch (error) {
1595
+ // 撞 uuid 不算错:fire 失败会整条重跑,宿主用确定性 uuid(任务 id + 触发
1596
+ // 时刻推出来的那种)就天然幂等,重试不会多排一条。
1597
+ if (isUniqueViolation(error)) return { created: false, reason: 'duplicate', uuid };
1598
+ throw error;
1599
+ }
1600
+ if (!created) {
1601
+ throw new Error('AGENTIC_SCHEDULE_FAILED: createTask 没有返回新建的任务行');
1602
+ }
1603
+ return {
1604
+ created: true,
1605
+ id: created.id ?? null,
1606
+ uuid: created.uuid ?? uuid,
1607
+ nextSendAt: created.next_send_at ?? nextSendAt,
1608
+ };
1609
+ };
1610
+
1403
1611
  // 单次 fire 的宿主便签:onBeforeFire 的 fireCtx 和同一次 fire 每轮的
1404
1612
  // sessionCtx(onLLMOutput / executeToolCalls)拿到同一个对象引用,fire 结束
1405
1613
  // (finish / skip-push / 抛错 / 轮数超限)随调用栈丢弃。库自己不读不写、
@@ -1411,6 +1619,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1411
1619
  userId: task.user_id,
1412
1620
  readState,
1413
1621
  writeState,
1622
+ scheduleTask,
1414
1623
  now: new Date(nowFn()),
1415
1624
  scratch,
1416
1625
  });
@@ -1463,9 +1672,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1463
1672
  const assistantMessage = extractAssistantMessage(llmResponse);
1464
1673
  messages = [...messages, assistantMessage];
1465
1674
 
1466
- // 共享的 SessionContext(与 amsg-instant 同形状)之上,再挂两个状态访问器:
1467
- // 大内容要不要旁路存,往往到工具跑完、组 push 时才知道,而那正是
1468
- // onLLMOutput / executeToolCalls 的位置,onBeforeFire 早就返回了。
1675
+ // 共享的 SessionContext(与 amsg-instant 同形状)之上,再挂两个状态访问器
1676
+ // 和 scheduleTask:大内容要不要旁路存、要不要给自己排条后续,往往到工具跑完、
1677
+ // 组 push 时才知道,而那正是 onLLMOutput / executeToolCalls 的位置,
1678
+ // onBeforeFire 早就返回了。
1469
1679
  const sessionCtx = Object.freeze({
1470
1680
  ...buildSessionContext({
1471
1681
  sessionId,
@@ -1480,6 +1690,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1480
1690
  }),
1481
1691
  readState,
1482
1692
  writeState,
1693
+ scheduleTask,
1483
1694
  });
1484
1695
 
1485
1696
  const decision = await hooks.onLLMOutput(sessionCtx);
@@ -1536,6 +1747,13 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1536
1747
  // same round, both need their id on the assistant turn, or the half
1537
1748
  // that's missing leaves an orphan role:'tool' and a strict relay
1538
1749
  // rejects the next round.
1750
+ //
1751
+ // Contract for onLLMOutput: the toolCalls it returns must cover every
1752
+ // native tool_call the model declared. A native call left out still
1753
+ // gets declared on the assistant turn (it came from the model) but has
1754
+ // no matching role:'tool' result, and a strict relay rejects the next
1755
+ // round over the unanswered id. Drop a call on purpose → return a
1756
+ // result for it saying so, don't just omit it.
1539
1757
  const nativeCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
1540
1758
  const nativeIds = new Set(nativeCalls.map((tc) => tc && tc.id));
1541
1759
  const synthesized = toolCalls.filter((tc) => !nativeIds.has(tc && tc.id));
@@ -3573,6 +3791,7 @@ const SERVER_FEATURES = Object.freeze([
3573
3791
  'agentic-scratch',
3574
3792
  'agentic-write-state',
3575
3793
  'agentic-fire-tools',
3794
+ 'agentic-schedule-task',
3576
3795
  'vapid-public-key',
3577
3796
  ]);
3578
3797
 
@@ -3612,6 +3831,7 @@ function createCapabilitiesHandler(ctx) {
3612
3831
  * @param {number} [config.maxToolIterations] - factory default LLM-round cap for the agentic loop (default 5).
3613
3832
  * @param {number} [config.totalTimeoutMs] - factory default wall-time ceiling for the agentic loop (default 240000).
3614
3833
  * @param {number} [config.maxStateValueBytes] - client_state 单条 value 的总上限(默认 5MB)。超过 200KB 的值由服务端透明分块存储(见 lib/state-chunks.js)。
3834
+ * @param {number} [config.maxScheduledTasksPerFire] - 一次 fire 里 hook 用 ctx.scheduleTask() 最多能建几条后续任务(默认 2,0 表示不许自排)。
3615
3835
  * @returns {{ handlers: Object, ctx: Object }}
3616
3836
  */
3617
3837
 
@@ -3641,7 +3861,9 @@ function createSingleUserServer(config) {
3641
3861
  hooks: config.hooks || null,
3642
3862
  maxToolIterations: config.maxToolIterations,
3643
3863
  totalTimeoutMs: config.totalTimeoutMs,
3644
- maxStateValueBytes: config.maxStateValueBytes
3864
+ maxStateValueBytes: config.maxStateValueBytes,
3865
+ // hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
3866
+ maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
3645
3867
  };
3646
3868
 
3647
3869
  return {
@@ -4078,10 +4300,11 @@ function createWebCryptoWebPush(vapid = {}, { ttl = SCHEDULED_DEFAULT_TTL } = {}
4078
4300
  * Access-Control-* on responses. With no `cors` the Worker stays same-origin.
4079
4301
  *
4080
4302
  * Fire-time hooks are opt-in too: pass `hooks: { onBeforeFire, onLLMOutput,
4081
- * executeToolCalls }` (+ optional `maxToolIterations` / `totalTimeoutMs`) in the
4082
- * config to let scheduled AI tasks assemble their prompt and run a server-side
4083
- * tool loop at fire time. Omit them and AI tasks replay the schedule-time frozen
4084
- * prompt exactly as before. See lib/agentic-fire.js.
4303
+ * executeToolCalls }` (+ optional `maxToolIterations` / `totalTimeoutMs` /
4304
+ * `maxScheduledTasksPerFire`) in the config to let scheduled AI tasks assemble
4305
+ * their prompt and run a server-side tool loop at fire time. Omit them and AI
4306
+ * tasks replay the schedule-time frozen prompt exactly as before. See
4307
+ * lib/agentic-fire.js.
4085
4308
  *
4086
4309
  * scheduled() 每次触发都会先给任务占位(在行的 lease_until 上写租约),同一
4087
4310
  * 条任务不会被相邻两跳重复触发(见 lib/run-tick.js)。租期默认 10 分钟,可以
@@ -4221,6 +4444,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
4221
4444
  totalTimeoutMs: cfg.totalTimeoutMs,
4222
4445
  // hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
4223
4446
  maxStateValueBytes: cfg.maxStateValueBytes,
4447
+ // hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
4448
+ maxScheduledTasksPerFire: cfg.maxScheduledTasksPerFire,
4224
4449
  // 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
4225
4450
  claimLeaseMs: cfg.claimLeaseMs
4226
4451
  });
@@ -1183,6 +1183,32 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
1183
1183
  * 补完那轮模型仍可能再发起调用。库不看 tools 的内容,执行照旧是
1184
1184
  * executeToolCalls 的事。
1185
1185
  *
1186
+ * 自排后续任务:fireCtx 和每轮的 sessionCtx 上都挂着 scheduleTask,宿主用它
1187
+ * 在这次 fire 里给同一个用户再建一条定时任务(「这条发完,一个半小时后我再
1188
+ * 接着说一句」)。写口在 sessionCtx 上也给,是因为要不要接着说往往是看完
1189
+ * LLM 输出才定的,那时 onBeforeFire 早已返回。凭据(pushSubscription /
1190
+ * apiKey)和投递配置从当前任务继承、宿主全程看不到(与 buildHookTask 屏蔽
1191
+ * 凭据同一个原则),宿主只提供「什么时候、说什么方向」。
1192
+ *
1193
+ * 新任务不继承 completePrompt / messages,两者都置 null:fire-time hook 每次
1194
+ * 现场重组 prompt,把排程时冻结的旧 prompt 带过去,会在新任务万一走回老链路
1195
+ * (onBeforeFire 返回 null、或那次部署没配 hooks)时静默顶替宿主的意图——
1196
+ * 与其发一条来路不明的旧文案,不如让它当场失败、走既有的重试/标记逻辑。
1197
+ *
1198
+ * 护栏(几条都是这个能力能不能上线的关键):
1199
+ * - firstSendTime 至少比现在晚 MIN_SCHEDULE_LEAD_MS(60 秒)。cron 每分钟
1200
+ * 一跳,排在 60 秒内等于让下一跳立刻捡走,容易变成自己触发自己的紧密循环。
1201
+ * - messageType 只收 auto / prompted / fixed,不收 instant。instant 的语义是
1202
+ * 「建行的那一刻就投递」,那条路径归 POST /schedule-message 管;从 fire 里
1203
+ * 造这么一行,投递时机反而说不清(这里不投,要等 cron 捞到才发)。
1204
+ * - 单次 fire 的建任务上限默认 DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE 条,
1205
+ * factory 配置 ctx.maxScheduledTasksPerFire 可覆盖(0 表示不许自排)。模型
1206
+ * 自排后续本质上是个可以无限延伸的链:没有上限的话,一次 fire 里连排十条、
1207
+ * 或者每条任务都排下一条,就成了谁也没按下停止键的循环。
1208
+ * - uuid 冲突不抛错,返回 { created: false, reason: 'duplicate' }。fire 失败
1209
+ * 会整条重跑(run-tick 的重试语义),宿主传一个由「任务 id + 触发时刻」推
1210
+ * 出来的确定性 uuid 就天然幂等,不会每重试一次多排一条。
1211
+ *
1186
1212
  * Budget guards, both factory-level (ctx.maxToolIterations /
1187
1213
  * ctx.totalTimeoutMs) and per-fire (onBeforeFire may return
1188
1214
  * { messages, maxToolIterations?, totalTimeoutMs? } to override for one
@@ -1197,6 +1223,17 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
1197
1223
  const DEFAULT_MAX_TOOL_ITERATIONS = 5;
1198
1224
  const DEFAULT_TOTAL_TIMEOUT_MS = 240_000;
1199
1225
 
1226
+ // 一次 fire 里最多能给自己排几条后续任务(ctx.maxScheduledTasksPerFire 可覆盖)。
1227
+ const DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
1228
+ // 自排任务的最小提前量:cron 一分钟一跳,比这更近等于让下一跳立刻捡走。
1229
+ const MIN_SCHEDULE_LEAD_MS = 60_000;
1230
+
1231
+ // 自排任务允许的类型。instant 归 POST /schedule-message 那条同步路径管。
1232
+ const SCHEDULABLE_MESSAGE_TYPES = new Set(['auto', 'prompted', 'fixed']);
1233
+ // 与 validateScheduleMessagePayload 同一套;run-tick 只认得这三种,别的值会让
1234
+ // 任务发完之后推进不到下一次。
1235
+ const SCHEDULABLE_RECURRENCE_TYPES = new Set(['none', 'daily', 'weekly']);
1236
+
1200
1237
  // Same pacing as the legacy path / amsg-instant.
1201
1238
  const SLEEP_BETWEEN_MESSAGES_MS$1 = 1500;
1202
1239
 
@@ -1251,6 +1288,9 @@ function normalizeBeforeFireResult(result) {
1251
1288
  messages: result.messages,
1252
1289
  maxToolIterations: result.maxToolIterations,
1253
1290
  totalTimeoutMs: result.totalTimeoutMs,
1291
+ // 归一成「有就是非空数组,否则 undefined」,让下面每轮的透传判断能直接
1292
+ // 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
1293
+ // buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
1254
1294
  tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : undefined,
1255
1295
  toolChoice: result.toolChoice,
1256
1296
  };
@@ -1281,7 +1321,7 @@ function firstPositiveNumber(values, fallback) {
1281
1321
  * @param {import('../adapters/interface.js').TaskRow} args.task
1282
1322
  * @param {Object} args.decryptedPayload - decrypted task payload (has credentials; they stop here)
1283
1323
  * @param {string} args.userKey - per-user storage key (for readState decryption)
1284
- * @param {Object} args.ctx - processor ctx ({ db, webpush, vapid, hooks, maxToolIterations, totalTimeoutMs })
1324
+ * @param {Object} args.ctx - processor ctx ({ db, webpush, vapid, hooks, maxToolIterations, totalTimeoutMs, maxScheduledTasksPerFire })
1285
1325
  * @returns {Promise<{ handled: false } | { handled: true, result: { success: true, messagesSent: number, status: 'finished'|'skipped', iterations: number } }>}
1286
1326
  * `handled: false` → caller falls back to the legacy frozen-prompt path
1287
1327
  * (onBeforeFire returned null).
@@ -1400,6 +1440,174 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1400
1440
  return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized });
1401
1441
  };
1402
1442
 
1443
+ const maxScheduledTasksPerFire =
1444
+ Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0
1445
+ ? ctx.maxScheduledTasksPerFire
1446
+ : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
1447
+ // 本次 fire 已经用掉的建任务额度。校验通过、真的要写库时才 +1,参数不合法的
1448
+ // 调用不占额度;uuid 撞车(那条任务其实已经建出来了)照样占。
1449
+ let scheduledTaskCount = 0;
1450
+
1451
+ /**
1452
+ * scheduleTask(options) —— 在这次 fire 里给**同一个用户**再建一条定时任务。
1453
+ *
1454
+ * 典型用法:角色发完这条,想过一个半小时再接着说一句。以前只能写进
1455
+ * client_state 等客户端上线重放,用户一直不上线这条链就断了;建成任务行之后,
1456
+ * 到点由 cron 直接触发,跟别的定时消息一样。
1457
+ *
1458
+ * 凭据与投递配置(pushSubscription / apiUrl / apiKey / primaryModel /
1459
+ * maxTokens / temperature / splitPattern)以及 contactName / avatarUrl /
1460
+ * messageSubtype / userMessage 从当前任务继承,宿主只说「什么时候、说什么
1461
+ * 方向」——hook 全程看不到凭据。completePrompt / messages 不继承(都置 null),
1462
+ * 见文件头。
1463
+ *
1464
+ * @param {Object} options
1465
+ * @param {string} options.firstSendTime - 必填,ISO 8601;至少比现在晚 60 秒
1466
+ * @param {'none'|'daily'|'weekly'} [options.recurrenceType='none']
1467
+ * @param {'auto'|'prompted'|'fixed'} [options.messageType] - 默认继承当前任务
1468
+ * @param {Object} [options.metadata] - 整体替换(不是深合并)当前任务的 metadata
1469
+ * @param {string} [options.contactName] - 默认继承
1470
+ * @param {string|null} [options.avatarUrl] - 默认继承
1471
+ * @param {string} [options.messageSubtype] - 默认继承
1472
+ * @param {string|null} [options.userMessage] - 默认继承;messageType 'fixed' 时必须有
1473
+ * @param {string} [options.uuid] - 默认 randomUUID();传确定性 uuid 可做重试幂等
1474
+ * @returns {Promise<{ created: true, id: number|null, uuid: string, nextSendAt: string }
1475
+ * | { created: false, reason: 'duplicate', uuid: string }>}
1476
+ */
1477
+ const scheduleTask = async (options) => {
1478
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
1479
+ throw new TypeError('scheduleTask(options) 需要一个对象,至少包含 { firstSendTime }');
1480
+ }
1481
+
1482
+ // ---- 触发时刻 ----
1483
+ if (typeof options.firstSendTime !== 'string' || !options.firstSendTime.trim()) {
1484
+ throw new RangeError('scheduleTask: firstSendTime 必填,且必须是 ISO 8601 字符串');
1485
+ }
1486
+ const firstSendAt = new Date(options.firstSendTime);
1487
+ if (Number.isNaN(firstSendAt.getTime())) {
1488
+ throw new RangeError(`scheduleTask: firstSendTime 解析不出合法时间:${options.firstSendTime}`);
1489
+ }
1490
+ const earliest = nowFn() + MIN_SCHEDULE_LEAD_MS;
1491
+ if (firstSendAt.getTime() < earliest) {
1492
+ throw new RangeError(
1493
+ `scheduleTask: firstSendTime 至少要比现在晚 ${MIN_SCHEDULE_LEAD_MS / 1000} 秒` +
1494
+ `(最早可排 ${new Date(earliest).toISOString()},收到 ${firstSendAt.toISOString()})` +
1495
+ '——cron 一分钟一跳,排得更近等于让下一跳立刻捡走'
1496
+ );
1497
+ }
1498
+ const nextSendAt = firstSendAt.toISOString();
1499
+
1500
+ // ---- 类型 ----
1501
+ const inheritedType = options.messageType == null;
1502
+ const messageType = inheritedType ? decryptedPayload.messageType : options.messageType;
1503
+ if (messageType === 'instant') {
1504
+ throw new TypeError(
1505
+ "scheduleTask: messageType 不能是 'instant'——instant 的语义是「建行的那一刻就投递」," +
1506
+ '那条路径归 POST /schedule-message 管;从 fire 里建一条 instant,投递时机反而说不清。' +
1507
+ (inheritedType ? '(这个 instant 是从当前任务继承来的,显式传 auto / prompted / fixed 覆盖它。)' : '')
1508
+ );
1509
+ }
1510
+ if (!SCHEDULABLE_MESSAGE_TYPES.has(messageType)) {
1511
+ throw new TypeError(
1512
+ `scheduleTask: messageType 只能是 auto / prompted / fixed,收到 ${JSON.stringify(messageType)}`
1513
+ );
1514
+ }
1515
+
1516
+ const recurrenceType = options.recurrenceType == null ? 'none' : options.recurrenceType;
1517
+ if (!SCHEDULABLE_RECURRENCE_TYPES.has(recurrenceType)) {
1518
+ throw new TypeError(
1519
+ `scheduleTask: recurrenceType 只能是 none / daily / weekly,收到 ${JSON.stringify(recurrenceType)}`
1520
+ );
1521
+ }
1522
+
1523
+ // ---- 继承 + 覆盖 ----
1524
+ const contactName = options.contactName === undefined ? decryptedPayload.contactName : options.contactName;
1525
+ if (typeof contactName !== 'string' || !contactName.trim()) {
1526
+ throw new TypeError('scheduleTask: contactName 必须是非空字符串(默认继承当前任务)');
1527
+ }
1528
+ const userMessage = options.userMessage === undefined
1529
+ ? (decryptedPayload.userMessage ?? null)
1530
+ : options.userMessage;
1531
+ if (messageType === 'fixed' && (typeof userMessage !== 'string' || !userMessage.trim())) {
1532
+ throw new TypeError(
1533
+ "scheduleTask: messageType 'fixed' 必须有 userMessage(自己传,或从当前任务继承到)" +
1534
+ '——固定文本任务没有正文,就是一条永远发空的任务'
1535
+ );
1536
+ }
1537
+ const metadata = options.metadata === undefined ? (decryptedPayload.metadata || {}) : options.metadata;
1538
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
1539
+ throw new TypeError('scheduleTask: metadata 必须是普通对象(整体替换当前任务的 metadata,不做深合并)');
1540
+ }
1541
+ const uuid = options.uuid == null ? randomUUID() : options.uuid;
1542
+ if (typeof uuid !== 'string' || !uuid.trim()) {
1543
+ throw new TypeError('scheduleTask: uuid 必须是非空字符串');
1544
+ }
1545
+
1546
+ // ---- 额度 ----
1547
+ if (scheduledTaskCount >= maxScheduledTasksPerFire) {
1548
+ throw new RangeError(
1549
+ `scheduleTask: 单次 fire 最多建 ${maxScheduledTasksPerFire} 条任务` +
1550
+ `(factory 配置 maxScheduledTasksPerFire 可调),这是第 ${scheduledTaskCount + 1} 条`
1551
+ );
1552
+ }
1553
+ // readState 在适配器不支持时降级成空数组;建任务不一样,静默成功会让宿主
1554
+ // 以为后续那条已经排上了,其实谁也不会触发它。
1555
+ if (!ctx.db || typeof ctx.db.createTask !== 'function') {
1556
+ throw new Error('AGENTIC_SCHEDULE_UNSUPPORTED: 当前数据库适配器不支持建任务(缺 createTask)');
1557
+ }
1558
+ scheduledTaskCount++;
1559
+
1560
+ // 字段构成与 POST /schedule-message 落库的那份保持一致,只是走库内部、不经 HTTP。
1561
+ const fullTaskData = {
1562
+ contactName,
1563
+ avatarUrl: options.avatarUrl === undefined ? (decryptedPayload.avatarUrl || null) : (options.avatarUrl || null),
1564
+ messageType,
1565
+ messageSubtype: (options.messageSubtype === undefined ? decryptedPayload.messageSubtype : options.messageSubtype) || 'chat',
1566
+ userMessage: userMessage || null,
1567
+ firstSendTime: nextSendAt,
1568
+ recurrenceType,
1569
+ apiUrl: decryptedPayload.apiUrl || null,
1570
+ apiKey: decryptedPayload.apiKey || null,
1571
+ primaryModel: decryptedPayload.primaryModel || null,
1572
+ // 见文件头:fire-time hook 每次现场重组 prompt,旧 prompt 带过去只会在新
1573
+ // 任务走回老链路时顶替宿主的意图。
1574
+ completePrompt: null,
1575
+ messages: null,
1576
+ maxTokens: decryptedPayload.maxTokens ?? null,
1577
+ temperature: decryptedPayload.temperature ?? null,
1578
+ splitPattern: decryptedPayload.splitPattern ?? null,
1579
+ pushSubscription: decryptedPayload.pushSubscription,
1580
+ metadata,
1581
+ };
1582
+
1583
+ const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
1584
+
1585
+ let created;
1586
+ try {
1587
+ created = await ctx.db.createTask({
1588
+ user_id: task.user_id,
1589
+ uuid,
1590
+ encrypted_payload: encryptedPayload,
1591
+ next_send_at: nextSendAt,
1592
+ message_type: messageType,
1593
+ });
1594
+ } catch (error) {
1595
+ // 撞 uuid 不算错:fire 失败会整条重跑,宿主用确定性 uuid(任务 id + 触发
1596
+ // 时刻推出来的那种)就天然幂等,重试不会多排一条。
1597
+ if (isUniqueViolation(error)) return { created: false, reason: 'duplicate', uuid };
1598
+ throw error;
1599
+ }
1600
+ if (!created) {
1601
+ throw new Error('AGENTIC_SCHEDULE_FAILED: createTask 没有返回新建的任务行');
1602
+ }
1603
+ return {
1604
+ created: true,
1605
+ id: created.id ?? null,
1606
+ uuid: created.uuid ?? uuid,
1607
+ nextSendAt: created.next_send_at ?? nextSendAt,
1608
+ };
1609
+ };
1610
+
1403
1611
  // 单次 fire 的宿主便签:onBeforeFire 的 fireCtx 和同一次 fire 每轮的
1404
1612
  // sessionCtx(onLLMOutput / executeToolCalls)拿到同一个对象引用,fire 结束
1405
1613
  // (finish / skip-push / 抛错 / 轮数超限)随调用栈丢弃。库自己不读不写、
@@ -1411,6 +1619,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1411
1619
  userId: task.user_id,
1412
1620
  readState,
1413
1621
  writeState,
1622
+ scheduleTask,
1414
1623
  now: new Date(nowFn()),
1415
1624
  scratch,
1416
1625
  });
@@ -1463,9 +1672,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1463
1672
  const assistantMessage = extractAssistantMessage(llmResponse);
1464
1673
  messages = [...messages, assistantMessage];
1465
1674
 
1466
- // 共享的 SessionContext(与 amsg-instant 同形状)之上,再挂两个状态访问器:
1467
- // 大内容要不要旁路存,往往到工具跑完、组 push 时才知道,而那正是
1468
- // onLLMOutput / executeToolCalls 的位置,onBeforeFire 早就返回了。
1675
+ // 共享的 SessionContext(与 amsg-instant 同形状)之上,再挂两个状态访问器
1676
+ // 和 scheduleTask:大内容要不要旁路存、要不要给自己排条后续,往往到工具跑完、
1677
+ // 组 push 时才知道,而那正是 onLLMOutput / executeToolCalls 的位置,
1678
+ // onBeforeFire 早就返回了。
1469
1679
  const sessionCtx = Object.freeze({
1470
1680
  ...buildSessionContext({
1471
1681
  sessionId,
@@ -1480,6 +1690,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1480
1690
  }),
1481
1691
  readState,
1482
1692
  writeState,
1693
+ scheduleTask,
1483
1694
  });
1484
1695
 
1485
1696
  const decision = await hooks.onLLMOutput(sessionCtx);
@@ -1536,6 +1747,13 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1536
1747
  // same round, both need their id on the assistant turn, or the half
1537
1748
  // that's missing leaves an orphan role:'tool' and a strict relay
1538
1749
  // rejects the next round.
1750
+ //
1751
+ // Contract for onLLMOutput: the toolCalls it returns must cover every
1752
+ // native tool_call the model declared. A native call left out still
1753
+ // gets declared on the assistant turn (it came from the model) but has
1754
+ // no matching role:'tool' result, and a strict relay rejects the next
1755
+ // round over the unanswered id. Drop a call on purpose → return a
1756
+ // result for it saying so, don't just omit it.
1539
1757
  const nativeCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
1540
1758
  const nativeIds = new Set(nativeCalls.map((tc) => tc && tc.id));
1541
1759
  const synthesized = toolCalls.filter((tc) => !nativeIds.has(tc && tc.id));
@@ -3573,6 +3791,7 @@ const SERVER_FEATURES = Object.freeze([
3573
3791
  'agentic-scratch',
3574
3792
  'agentic-write-state',
3575
3793
  'agentic-fire-tools',
3794
+ 'agentic-schedule-task',
3576
3795
  'vapid-public-key',
3577
3796
  ]);
3578
3797
 
@@ -3612,6 +3831,7 @@ function createCapabilitiesHandler(ctx) {
3612
3831
  * @param {number} [config.maxToolIterations] - factory default LLM-round cap for the agentic loop (default 5).
3613
3832
  * @param {number} [config.totalTimeoutMs] - factory default wall-time ceiling for the agentic loop (default 240000).
3614
3833
  * @param {number} [config.maxStateValueBytes] - client_state 单条 value 的总上限(默认 5MB)。超过 200KB 的值由服务端透明分块存储(见 lib/state-chunks.js)。
3834
+ * @param {number} [config.maxScheduledTasksPerFire] - 一次 fire 里 hook 用 ctx.scheduleTask() 最多能建几条后续任务(默认 2,0 表示不许自排)。
3615
3835
  * @returns {{ handlers: Object, ctx: Object }}
3616
3836
  */
3617
3837
 
@@ -3641,7 +3861,9 @@ function createSingleUserServer(config) {
3641
3861
  hooks: config.hooks || null,
3642
3862
  maxToolIterations: config.maxToolIterations,
3643
3863
  totalTimeoutMs: config.totalTimeoutMs,
3644
- maxStateValueBytes: config.maxStateValueBytes
3864
+ maxStateValueBytes: config.maxStateValueBytes,
3865
+ // hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
3866
+ maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
3645
3867
  };
3646
3868
 
3647
3869
  return {
@@ -4078,10 +4300,11 @@ function createWebCryptoWebPush(vapid = {}, { ttl = SCHEDULED_DEFAULT_TTL } = {}
4078
4300
  * Access-Control-* on responses. With no `cors` the Worker stays same-origin.
4079
4301
  *
4080
4302
  * Fire-time hooks are opt-in too: pass `hooks: { onBeforeFire, onLLMOutput,
4081
- * executeToolCalls }` (+ optional `maxToolIterations` / `totalTimeoutMs`) in the
4082
- * config to let scheduled AI tasks assemble their prompt and run a server-side
4083
- * tool loop at fire time. Omit them and AI tasks replay the schedule-time frozen
4084
- * prompt exactly as before. See lib/agentic-fire.js.
4303
+ * executeToolCalls }` (+ optional `maxToolIterations` / `totalTimeoutMs` /
4304
+ * `maxScheduledTasksPerFire`) in the config to let scheduled AI tasks assemble
4305
+ * their prompt and run a server-side tool loop at fire time. Omit them and AI
4306
+ * tasks replay the schedule-time frozen prompt exactly as before. See
4307
+ * lib/agentic-fire.js.
4085
4308
  *
4086
4309
  * scheduled() 每次触发都会先给任务占位(在行的 lease_until 上写租约),同一
4087
4310
  * 条任务不会被相邻两跳重复触发(见 lib/run-tick.js)。租期默认 10 分钟,可以
@@ -4221,6 +4444,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
4221
4444
  totalTimeoutMs: cfg.totalTimeoutMs,
4222
4445
  // hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
4223
4446
  maxStateValueBytes: cfg.maxStateValueBytes,
4447
+ // hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
4448
+ maxScheduledTasksPerFire: cfg.maxScheduledTasksPerFire,
4224
4449
  // 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
4225
4450
  claimLeaseMs: cfg.claimLeaseMs
4226
4451
  });
@@ -12,7 +12,7 @@
12
12
 
13
13
 
14
14
 
15
- var _chunkHQHRXSQAcjs = require('./chunk-HQHRXSQA.cjs');
15
+ var _chunk3QRVP4JEcjs = require('./chunk-3QRVP4JE.cjs');
16
16
 
17
17
 
18
18
 
@@ -27,4 +27,4 @@ var _chunkHQHRXSQAcjs = require('./chunk-HQHRXSQA.cjs');
27
27
 
28
28
 
29
29
 
30
- exports.MAX_PUSH_PAYLOAD_BYTES = _chunkHQHRXSQAcjs.MAX_PUSH_PAYLOAD_BYTES; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkHQHRXSQAcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkHQHRXSQAcjs.WEB_PUSH_MAX_BODY_BYTES; exports.createD1Adapter = _chunkHQHRXSQAcjs.createD1Adapter; exports.createSingleUserCloudflareWorker = _chunkHQHRXSQAcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkHQHRXSQAcjs.createSingleUserServer; exports.createWebCryptoWebPush = _chunkHQHRXSQAcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkHQHRXSQAcjs.decryptFromStorage; exports.decryptPayload = _chunkHQHRXSQAcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkHQHRXSQAcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkHQHRXSQAcjs.encryptForStorage; exports.measurePushPayload = _chunkHQHRXSQAcjs.measurePushPayload; exports.runScheduledTick = _chunkHQHRXSQAcjs.runScheduledTick;
30
+ exports.MAX_PUSH_PAYLOAD_BYTES = _chunk3QRVP4JEcjs.MAX_PUSH_PAYLOAD_BYTES; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunk3QRVP4JEcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunk3QRVP4JEcjs.WEB_PUSH_MAX_BODY_BYTES; exports.createD1Adapter = _chunk3QRVP4JEcjs.createD1Adapter; exports.createSingleUserCloudflareWorker = _chunk3QRVP4JEcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunk3QRVP4JEcjs.createSingleUserServer; exports.createWebCryptoWebPush = _chunk3QRVP4JEcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunk3QRVP4JEcjs.decryptFromStorage; exports.decryptPayload = _chunk3QRVP4JEcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunk3QRVP4JEcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunk3QRVP4JEcjs.encryptForStorage; exports.measurePushPayload = _chunk3QRVP4JEcjs.measurePushPayload; exports.runScheduledTick = _chunk3QRVP4JEcjs.runScheduledTick;
@@ -1,2 +1,2 @@
1
- export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, v as measurePushPayload, r as runScheduledTick } from './cloudflare-CH3dBl5b.cjs';
1
+ export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, v as measurePushPayload, r as runScheduledTick } from './cloudflare-kUY6z-k5.cjs';
2
2
  import '@rei-standard/amsg-shared';
@@ -1,2 +1,2 @@
1
- export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, v as measurePushPayload, r as runScheduledTick } from './cloudflare-CH3dBl5b.js';
1
+ export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, v as measurePushPayload, r as runScheduledTick } from './cloudflare-kUY6z-k5.js';
2
2
  import '@rei-standard/amsg-shared';
@@ -12,7 +12,7 @@ import {
12
12
  encryptForStorage,
13
13
  measurePushPayload,
14
14
  runScheduledTick
15
- } from "./chunk-LEE5KQ7Y.mjs";
15
+ } from "./chunk-S6O3MXQO.mjs";
16
16
  export {
17
17
  MAX_PUSH_PAYLOAD_BYTES,
18
18
  WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
package/dist/index.cjs CHANGED
@@ -27,7 +27,7 @@
27
27
 
28
28
 
29
29
 
30
- var _chunkHQHRXSQAcjs = require('./chunk-HQHRXSQA.cjs');
30
+ var _chunk3QRVP4JEcjs = require('./chunk-3QRVP4JE.cjs');
31
31
 
32
32
  // src/server/adapters/factory.js
33
33
  async function createAdapter(config) {
@@ -60,7 +60,7 @@ async function createAdapter(config) {
60
60
  // src/server/handlers/init-tenant.js
61
61
  function createInitTenantHandler(ctx) {
62
62
  async function POST(headers, body) {
63
- const initSecret = _chunkHQHRXSQAcjs.getHeader.call(void 0, headers, "x-init-secret");
63
+ const initSecret = _chunk3QRVP4JEcjs.getHeader.call(void 0, headers, "x-init-secret");
64
64
  if (ctx.tenant.initSecret && initSecret !== ctx.tenant.initSecret) {
65
65
  return {
66
66
  status: 401,
@@ -73,7 +73,7 @@ function createInitTenantHandler(ctx) {
73
73
  }
74
74
  };
75
75
  }
76
- const parsed = _chunkHQHRXSQAcjs.parseJsonBody.call(void 0, body);
76
+ const parsed = _chunk3QRVP4JEcjs.parseJsonBody.call(void 0, body);
77
77
  if (!parsed.ok) {
78
78
  return {
79
79
  status: 400,
@@ -87,7 +87,7 @@ function createInitTenantHandler(ctx) {
87
87
  const tenantId = payload.tenantId || void 0;
88
88
  const driver = payload.driver;
89
89
  const databaseUrl = payload.databaseUrl || payload.connectionString;
90
- if (tenantId && !_chunkHQHRXSQAcjs.isValidUUIDv4.call(void 0, tenantId)) {
90
+ if (tenantId && !_chunk3QRVP4JEcjs.isValidUUIDv4.call(void 0, tenantId)) {
91
91
  return {
92
92
  status: 400,
93
93
  body: {
@@ -207,7 +207,7 @@ function createSendNotificationsHandler(ctx) {
207
207
  }
208
208
  };
209
209
  }
210
- const data = await _chunkHQHRXSQAcjs.runScheduledTick.call(void 0, { ...ctx, db, masterKey });
210
+ const data = await _chunk3QRVP4JEcjs.runScheduledTick.call(void 0, { ...ctx, db, masterKey });
211
211
  return { status: 200, body: { success: true, data } };
212
212
  }
213
213
  return { POST };
@@ -579,12 +579,12 @@ async function createReiServer(config) {
579
579
  return {
580
580
  handlers: {
581
581
  initTenant: createInitTenantHandler(ctx),
582
- getUserKey: _chunkHQHRXSQAcjs.createGetUserKeyHandler.call(void 0, ctx),
583
- scheduleMessage: _chunkHQHRXSQAcjs.createScheduleMessageHandler.call(void 0, ctx),
582
+ getUserKey: _chunk3QRVP4JEcjs.createGetUserKeyHandler.call(void 0, ctx),
583
+ scheduleMessage: _chunk3QRVP4JEcjs.createScheduleMessageHandler.call(void 0, ctx),
584
584
  sendNotifications: createSendNotificationsHandler(ctx),
585
- updateMessage: _chunkHQHRXSQAcjs.createUpdateMessageHandler.call(void 0, ctx),
586
- cancelMessage: _chunkHQHRXSQAcjs.createCancelMessageHandler.call(void 0, ctx),
587
- messages: _chunkHQHRXSQAcjs.createMessagesHandler.call(void 0, ctx)
585
+ updateMessage: _chunk3QRVP4JEcjs.createUpdateMessageHandler.call(void 0, ctx),
586
+ cancelMessage: _chunk3QRVP4JEcjs.createCancelMessageHandler.call(void 0, ctx),
587
+ messages: _chunk3QRVP4JEcjs.createMessagesHandler.call(void 0, ctx)
588
588
  }
589
589
  };
590
590
  }
@@ -614,4 +614,4 @@ async function createReiServer(config) {
614
614
 
615
615
 
616
616
 
617
- exports.MAX_PUSH_PAYLOAD_BYTES = _chunkHQHRXSQAcjs.MAX_PUSH_PAYLOAD_BYTES; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkHQHRXSQAcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkHQHRXSQAcjs.WEB_PUSH_MAX_BODY_BYTES; exports.createAdapter = createAdapter; exports.createD1Adapter = _chunkHQHRXSQAcjs.createD1Adapter; exports.createReiServer = createReiServer; exports.createSingleUserCloudflareWorker = _chunkHQHRXSQAcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkHQHRXSQAcjs.createSingleUserServer; exports.createTenantToken = createTenantToken; exports.createWebCryptoWebPush = _chunkHQHRXSQAcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkHQHRXSQAcjs.decryptFromStorage; exports.decryptPayload = _chunkHQHRXSQAcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkHQHRXSQAcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkHQHRXSQAcjs.encryptForStorage; exports.isValidISO8601 = _chunkHQHRXSQAcjs.isValidISO8601; exports.isValidUUID = _chunkHQHRXSQAcjs.isValidUUID; exports.isValidUUIDv4 = _chunkHQHRXSQAcjs.isValidUUIDv4; exports.isValidUrl = _chunkHQHRXSQAcjs.isValidUrl; exports.measurePushPayload = _chunkHQHRXSQAcjs.measurePushPayload; exports.runScheduledTick = _chunkHQHRXSQAcjs.runScheduledTick; exports.validateAvatarUrl = _chunkHQHRXSQAcjs.validateAvatarUrl; exports.validateLlmMessagesArray = _chunkHQHRXSQAcjs.validateLlmMessagesArray; exports.validateScheduleMessagePayload = _chunkHQHRXSQAcjs.validateScheduleMessagePayload; exports.validateSplitPattern = _chunkHQHRXSQAcjs.validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
617
+ exports.MAX_PUSH_PAYLOAD_BYTES = _chunk3QRVP4JEcjs.MAX_PUSH_PAYLOAD_BYTES; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunk3QRVP4JEcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunk3QRVP4JEcjs.WEB_PUSH_MAX_BODY_BYTES; exports.createAdapter = createAdapter; exports.createD1Adapter = _chunk3QRVP4JEcjs.createD1Adapter; exports.createReiServer = createReiServer; exports.createSingleUserCloudflareWorker = _chunk3QRVP4JEcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunk3QRVP4JEcjs.createSingleUserServer; exports.createTenantToken = createTenantToken; exports.createWebCryptoWebPush = _chunk3QRVP4JEcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunk3QRVP4JEcjs.decryptFromStorage; exports.decryptPayload = _chunk3QRVP4JEcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunk3QRVP4JEcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunk3QRVP4JEcjs.encryptForStorage; exports.isValidISO8601 = _chunk3QRVP4JEcjs.isValidISO8601; exports.isValidUUID = _chunk3QRVP4JEcjs.isValidUUID; exports.isValidUUIDv4 = _chunk3QRVP4JEcjs.isValidUUIDv4; exports.isValidUrl = _chunk3QRVP4JEcjs.isValidUrl; exports.measurePushPayload = _chunk3QRVP4JEcjs.measurePushPayload; exports.runScheduledTick = _chunk3QRVP4JEcjs.runScheduledTick; exports.validateAvatarUrl = _chunk3QRVP4JEcjs.validateAvatarUrl; exports.validateLlmMessagesArray = _chunk3QRVP4JEcjs.validateLlmMessagesArray; exports.validateScheduleMessagePayload = _chunk3QRVP4JEcjs.validateScheduleMessagePayload; exports.validateSplitPattern = _chunk3QRVP4JEcjs.validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { g as getHeader, p as parseJsonBody, i as isValidUUIDv4, r as runScheduledTick, c as createMessagesHandler, a as createCancelMessageHandler, b as createUpdateMessageHandler, d as createScheduleMessageHandler, e as createGetUserKeyHandler } from './cloudflare-CH3dBl5b.cjs';
2
- export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, s as isValidISO8601, t as isValidUUID, u as isValidUrl, v as measurePushPayload, w as validateLlmMessagesArray, x as validateScheduleMessagePayload, y as validateSplitPattern } from './cloudflare-CH3dBl5b.cjs';
1
+ import { g as getHeader, p as parseJsonBody, i as isValidUUIDv4, r as runScheduledTick, c as createMessagesHandler, a as createCancelMessageHandler, b as createUpdateMessageHandler, d as createScheduleMessageHandler, e as createGetUserKeyHandler } from './cloudflare-kUY6z-k5.cjs';
2
+ export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, s as isValidISO8601, t as isValidUUID, u as isValidUrl, v as measurePushPayload, w as validateLlmMessagesArray, x as validateScheduleMessagePayload, y as validateSplitPattern } from './cloudflare-kUY6z-k5.cjs';
3
3
  import { createHash, randomBytes, createCipheriv, createDecipheriv, timingSafeEqual, createHmac, randomUUID } from 'crypto';
4
4
  import { normalizeVapidSubject } from '@rei-standard/amsg-shared';
5
5
  export { validateAvatarUrl } from '@rei-standard/amsg-shared';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { g as getHeader, p as parseJsonBody, i as isValidUUIDv4, r as runScheduledTick, c as createMessagesHandler, a as createCancelMessageHandler, b as createUpdateMessageHandler, d as createScheduleMessageHandler, e as createGetUserKeyHandler } from './cloudflare-CH3dBl5b.js';
2
- export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, s as isValidISO8601, t as isValidUUID, u as isValidUrl, v as measurePushPayload, w as validateLlmMessagesArray, x as validateScheduleMessagePayload, y as validateSplitPattern } from './cloudflare-CH3dBl5b.js';
1
+ import { g as getHeader, p as parseJsonBody, i as isValidUUIDv4, r as runScheduledTick, c as createMessagesHandler, a as createCancelMessageHandler, b as createUpdateMessageHandler, d as createScheduleMessageHandler, e as createGetUserKeyHandler } from './cloudflare-kUY6z-k5.js';
2
+ export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, s as isValidISO8601, t as isValidUUID, u as isValidUrl, v as measurePushPayload, w as validateLlmMessagesArray, x as validateScheduleMessagePayload, y as validateSplitPattern } from './cloudflare-kUY6z-k5.js';
3
3
  import { createHash, randomBytes, createCipheriv, createDecipheriv, timingSafeEqual, createHmac, randomUUID } from 'crypto';
4
4
  import { normalizeVapidSubject } from '@rei-standard/amsg-shared';
5
5
  export { validateAvatarUrl } from '@rei-standard/amsg-shared';
package/dist/index.mjs CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  validateLlmMessagesArray,
28
28
  validateScheduleMessagePayload,
29
29
  validateSplitPattern
30
- } from "./chunk-LEE5KQ7Y.mjs";
30
+ } from "./chunk-S6O3MXQO.mjs";
31
31
 
32
32
  // src/server/adapters/factory.js
33
33
  async function createAdapter(config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rei-standard/amsg-server",
3
- "version": "2.6.0-next.8",
3
+ "version": "2.6.0-next.9",
4
4
  "description": "ReiStandard Active Messaging server SDK with pluggable database adapters. Three-axis push schema (messageKind / messageType / messageSubtype) from @rei-standard/amsg-shared. Auto-emits ReasoningPush when the LLM response carries reasoning_content.",
5
5
  "repository": {
6
6
  "type": "git",