@rei-standard/amsg-server 2.6.0-next.20 → 2.6.0-next.21

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.
@@ -1,6 +1,8 @@
1
1
  import {
2
+ TASK_DELIVERY_COLUMNS,
3
+ TASK_DETAIL_COLUMNS,
2
4
  UPDATABLE_COLUMNS
3
- } from "./chunk-5FXVSC5O.mjs";
5
+ } from "./chunk-GN44PST5.mjs";
4
6
 
5
7
  // src/server/lib/recurrence.js
6
8
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -405,6 +407,26 @@ function validateLlmMessagesArray(messages) {
405
407
  return `messages[${i}].content must be a non-empty string or a non-empty array`;
406
408
  }
407
409
  }
410
+ var D1_MAX_ROW_BYTES = 2e6;
411
+ var STORAGE_HEX_OVERHEAD_BYTES = 66;
412
+ var TASK_ROW_OTHER_COLUMNS_BYTES = 4096;
413
+ var TASK_LAST_ERROR_RESERVED_BYTES = 2048;
414
+ var MAX_TASK_PAYLOAD_BYTES = Math.floor(
415
+ (D1_MAX_ROW_BYTES - TASK_ROW_OTHER_COLUMNS_BYTES - STORAGE_HEX_OVERHEAD_BYTES) / 2
416
+ ) - TASK_LAST_ERROR_RESERVED_BYTES;
417
+ var taskPayloadEncoder = new TextEncoder();
418
+ function validateTaskPayloadSize(serializedPayload) {
419
+ const bytes = taskPayloadByteLength(serializedPayload);
420
+ if (bytes <= MAX_TASK_PAYLOAD_BYTES) return null;
421
+ return {
422
+ code: "TASK_PAYLOAD_TOO_LARGE",
423
+ message: `\u4EFB\u52A1\u5185\u5BB9 ${bytes} \u5B57\u8282\uFF0C\u8D85\u8FC7 ${MAX_TASK_PAYLOAD_BYTES} \u5B57\u8282\u4E0A\u9650`,
424
+ details: { bytes, maxBytes: MAX_TASK_PAYLOAD_BYTES }
425
+ };
426
+ }
427
+ function taskPayloadByteLength(serializedPayload) {
428
+ return taskPayloadEncoder.encode(serializedPayload).length;
429
+ }
408
430
  function validateScheduleMessagePayload(payload) {
409
431
  if (!payload.contactName || typeof payload.contactName !== "string") {
410
432
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["contactName"] } };
@@ -456,6 +478,9 @@ function validateScheduleMessagePayload(payload) {
456
478
  if (payload.maxTokens !== void 0 && payload.maxTokens !== null && (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0)) {
457
479
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } };
458
480
  }
481
+ if (payload.userMessage !== void 0 && payload.userMessage !== null && typeof payload.userMessage !== "string") {
482
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["userMessage (must be a string)"] } };
483
+ }
459
484
  if (payload.messageType === "fixed") {
460
485
  if (!payload.userMessage) {
461
486
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["userMessage (required for fixed type)"] } };
@@ -554,6 +579,89 @@ var REQUEST_ERRORS = {
554
579
  INVALID_REQUEST_BODY: { code: "INVALID_REQUEST_BODY", message: "\u8BF7\u6C42\u4F53\u683C\u5F0F\u65E0\u6548" },
555
580
  INVALID_ENCRYPTED_PAYLOAD: { code: "INVALID_ENCRYPTED_PAYLOAD", message: "\u52A0\u5BC6\u6570\u636E\u683C\u5F0F\u9519\u8BEF" }
556
581
  };
582
+ var DEFAULT_MAX_REQUEST_BODY_BYTES = 32 * 1024 * 1024;
583
+ var GZIP_MAGIC_0 = 31;
584
+ var GZIP_MAGIC_1 = 139;
585
+ function normalizeContentEncoding(raw) {
586
+ const value = String(raw || "").trim().toLowerCase();
587
+ if (!value) return "";
588
+ const tokens = value.split(",").map((t) => t.trim()).filter((t) => t && t !== "identity");
589
+ if (tokens.length === 0) return "";
590
+ if (tokens.length === 1) return tokens[0];
591
+ return value;
592
+ }
593
+ function looksGzipped(bytes) {
594
+ return bytes.length >= 2 && bytes[0] === GZIP_MAGIC_0 && bytes[1] === GZIP_MAGIC_1;
595
+ }
596
+ async function inflateGzipToText(bytes, maxBytes) {
597
+ const reader = new Response(bytes).body.pipeThrough(new DecompressionStream("gzip")).getReader();
598
+ const decoder = new TextDecoder("utf-8");
599
+ let text = "";
600
+ let total = 0;
601
+ for (; ; ) {
602
+ const { done, value } = await reader.read();
603
+ if (done) break;
604
+ total += value.byteLength;
605
+ if (total > maxBytes) {
606
+ await reader.cancel();
607
+ const error = new Error(`\u89E3\u538B\u540E\u7684\u8BF7\u6C42\u4F53\u8D85\u8FC7 ${maxBytes} \u5B57\u8282\u4E0A\u9650`);
608
+ error.code = "REQUEST_BODY_TOO_LARGE";
609
+ throw error;
610
+ }
611
+ text += decoder.decode(value, { stream: true });
612
+ }
613
+ return text + decoder.decode();
614
+ }
615
+ async function readRequestBody(request, options = {}) {
616
+ const encoding = normalizeContentEncoding(
617
+ request.headers && typeof request.headers.get === "function" ? request.headers.get("content-encoding") : ""
618
+ );
619
+ if (!encoding) return { ok: true, body: await request.text() };
620
+ if (encoding !== "gzip" && encoding !== "x-gzip") {
621
+ return {
622
+ ok: false,
623
+ error: errorResponse(
624
+ 415,
625
+ "UNSUPPORTED_CONTENT_ENCODING",
626
+ `\u8BF7\u6C42\u4F53\u7684 Content-Encoding\u300C${encoding}\u300D\u4E0D\u652F\u6301\uFF0C\u8FD9\u91CC\u53EA\u8BA4 gzip`,
627
+ { contentEncoding: encoding, supported: ["gzip"] }
628
+ )
629
+ };
630
+ }
631
+ const bytes = new Uint8Array(await request.arrayBuffer());
632
+ if (!looksGzipped(bytes)) return { ok: true, body: new TextDecoder("utf-8").decode(bytes) };
633
+ if (typeof DecompressionStream !== "function") {
634
+ return {
635
+ ok: false,
636
+ error: errorResponse(
637
+ 415,
638
+ "UNSUPPORTED_CONTENT_ENCODING",
639
+ "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u6CA1\u6709 DecompressionStream\uFF0C\u6536\u4E0D\u4E86 gzip \u8BF7\u6C42\u4F53",
640
+ { contentEncoding: encoding }
641
+ )
642
+ };
643
+ }
644
+ const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0 ? options.maxBytes : DEFAULT_MAX_REQUEST_BODY_BYTES;
645
+ try {
646
+ return { ok: true, body: await inflateGzipToText(bytes, maxBytes) };
647
+ } catch (error) {
648
+ if (error && error.code === "REQUEST_BODY_TOO_LARGE") {
649
+ return {
650
+ ok: false,
651
+ error: errorResponse(413, "REQUEST_BODY_TOO_LARGE", error.message, { maxBytes })
652
+ };
653
+ }
654
+ return {
655
+ ok: false,
656
+ error: errorResponse(
657
+ 400,
658
+ "INVALID_CONTENT_ENCODING",
659
+ "\u8BF7\u6C42\u4F53\u58F0\u660E\u662F gzip\uFF0C\u4F46\u89E3\u538B\u5931\u8D25",
660
+ { contentEncoding: encoding }
661
+ )
662
+ };
663
+ }
664
+ }
557
665
  function parseBodyAsObject(body, options = {}) {
558
666
  const invalidJson = options.invalidJson || REQUEST_ERRORS.INVALID_JSON;
559
667
  const invalidType = options.invalidType || REQUEST_ERRORS.INVALID_REQUEST_BODY;
@@ -646,6 +754,184 @@ function createGetUserKeyHandler(ctx) {
646
754
  return { GET };
647
755
  }
648
756
 
757
+ // src/server/lib/webpush-webcrypto.js
758
+ import { sendWebPush as sharedSendWebPush } from "@rei-standard/amsg-shared";
759
+ import { buildVapidJwt, verifyVapidJwt, hmacSha256 as hmacSha2562 } from "@rei-standard/amsg-shared";
760
+ var WEB_PUSH_MAX_BODY_BYTES = 4096;
761
+ var WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = 16 + 4 + 1 + 65 + 1 + 16;
762
+ var MAX_PUSH_PAYLOAD_BYTES = WEB_PUSH_MAX_BODY_BYTES - WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES;
763
+ var PUSH_ENVELOPE_RESERVED_BYTES = 384;
764
+ var payloadEncoder = new TextEncoder();
765
+ function measurePushPayload(payload, options) {
766
+ const reserveEnvelope = !!(options && options.reserveEnvelope);
767
+ const bytes = payloadEncoder.encode(typeof payload === "string" ? payload : String(payload)).length;
768
+ const envelopeReservedBytes = reserveEnvelope ? PUSH_ENVELOPE_RESERVED_BYTES : 0;
769
+ const maxBytes = MAX_PUSH_PAYLOAD_BYTES - envelopeReservedBytes;
770
+ return {
771
+ bytes,
772
+ maxBytes,
773
+ remainingBytes: maxBytes - bytes,
774
+ withinLimit: bytes <= maxBytes,
775
+ envelopeReservedBytes
776
+ };
777
+ }
778
+ async function sendWebPush(args) {
779
+ const { payload } = args || {};
780
+ if (typeof payload === "string") {
781
+ const size = measurePushPayload(payload);
782
+ if (!size.withinLimit) {
783
+ const err5 = new Error(
784
+ `sendWebPush: payload is ${size.bytes} bytes, over the ${MAX_PUSH_PAYLOAD_BYTES}-byte limit (push services cap the encrypted body at ${WEB_PUSH_MAX_BODY_BYTES} bytes; aes128gcm adds ${WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES} bytes)`
785
+ );
786
+ err5.code = "PUSH_PAYLOAD_TOO_LARGE";
787
+ err5.bytes = size.bytes;
788
+ err5.maxBytes = MAX_PUSH_PAYLOAD_BYTES;
789
+ throw err5;
790
+ }
791
+ }
792
+ return sharedSendWebPush(args);
793
+ }
794
+ var SCHEDULED_DEFAULT_TTL = 2419200;
795
+ function createWebCryptoWebPush(vapid = {}, { ttl = SCHEDULED_DEFAULT_TTL } = {}) {
796
+ return {
797
+ async sendNotification(subscription, payload) {
798
+ return sendWebPush({
799
+ subscription,
800
+ payload,
801
+ ttl,
802
+ vapid: {
803
+ email: vapid.email,
804
+ publicKey: vapid.publicKey,
805
+ privateKey: vapid.privateKey
806
+ },
807
+ fetch: globalThis.fetch
808
+ });
809
+ }
810
+ };
811
+ }
812
+
813
+ // src/server/lib/errors.js
814
+ import { redactCredentials } from "@rei-standard/amsg-shared";
815
+ var NonRetryableError = class extends Error {
816
+ /**
817
+ * @param {string} message
818
+ * @param {{ code?: string, cause?: unknown }} [options]
819
+ */
820
+ constructor(message, options = {}) {
821
+ super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
822
+ this.name = "NonRetryableError";
823
+ this.permanent = true;
824
+ if (options.code) this.code = options.code;
825
+ }
826
+ };
827
+ function isNonRetryableError(error) {
828
+ return !!error && typeof error === "object" && /** @type {any} */
829
+ error.permanent === true;
830
+ }
831
+ var DeploymentConfigError = class extends Error {
832
+ /**
833
+ * @param {string} message
834
+ * @param {{ code?: string, cause?: unknown }} [options]
835
+ */
836
+ constructor(message, options = {}) {
837
+ super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
838
+ this.name = "DeploymentConfigError";
839
+ if (options.code) this.code = options.code;
840
+ }
841
+ };
842
+ function markPermanent(error, code) {
843
+ if (!error || typeof error !== "object") return error;
844
+ const target = (
845
+ /** @type {any} */
846
+ error
847
+ );
848
+ try {
849
+ target.permanent = true;
850
+ if (code && typeof target.code !== "string") target.code = code;
851
+ } catch (_frozen) {
852
+ }
853
+ return error;
854
+ }
855
+ var TASK_CANCELLED_CODE = "TASK_CANCELLED";
856
+ function isTaskCancelledError(error) {
857
+ return !!error && typeof error === "object" && /** @type {any} */
858
+ error.code === TASK_CANCELLED_CODE;
859
+ }
860
+ var TERMINAL_PUSH_STATUSES = /* @__PURE__ */ new Set([404, 410]);
861
+ var PUSH_PAYLOAD_TOO_LARGE_STATUS = 413;
862
+ var PERMANENT_ERROR_CODES = /* @__PURE__ */ new Set([
863
+ "PUSH_SUBSCRIPTION_MISSING",
864
+ "PUSH_SUBSCRIPTION_STORE_UNSUPPORTED",
865
+ "PUSH_PAYLOAD_TOO_LARGE"
866
+ ]);
867
+ function isPermanentDeliveryFailure(failure) {
868
+ const { permanent, errorCode, pushStatus } = failure || {};
869
+ return permanent === true || typeof errorCode === "string" && PERMANENT_ERROR_CODES.has(errorCode) || Number.isInteger(pushStatus) && (TERMINAL_PUSH_STATUSES.has(
870
+ /** @type {number} */
871
+ pushStatus
872
+ ) || pushStatus === PUSH_PAYLOAD_TOO_LARGE_STATUS);
873
+ }
874
+ var pushStatusCodes = /* @__PURE__ */ new WeakMap();
875
+ function tagPushStatusCode(error) {
876
+ if (error && typeof error === "object" && Number.isInteger(
877
+ /** @type {any} */
878
+ error.statusCode
879
+ )) {
880
+ pushStatusCodes.set(
881
+ /** @type {object} */
882
+ error,
883
+ /** @type {any} */
884
+ error.statusCode
885
+ );
886
+ }
887
+ return error;
888
+ }
889
+ async function sendTaggedPush(webpush, subscription, payloadJson) {
890
+ try {
891
+ return await webpush.sendNotification(subscription, payloadJson);
892
+ } catch (error) {
893
+ throw tagPushStatusCode(error);
894
+ }
895
+ }
896
+ function readPushStatusCode(error) {
897
+ if (!error || typeof error !== "object") return null;
898
+ const statusCode = pushStatusCodes.get(
899
+ /** @type {object} */
900
+ error
901
+ );
902
+ return Number.isInteger(statusCode) ? statusCode : null;
903
+ }
904
+ function buildErrorExtra(errorCode, pushStatus) {
905
+ const extra = {};
906
+ if (typeof errorCode === "string" && errorCode) extra.errorCode = errorCode.slice(0, 64);
907
+ if (Number.isInteger(pushStatus)) extra.pushStatus = /** @type {number} */
908
+ pushStatus;
909
+ return Object.keys(extra).length > 0 ? extra : void 0;
910
+ }
911
+ var ERROR_SUMMARY_MAX_CHARS = 500;
912
+ function sanitizeErrorSummary(reason) {
913
+ const flattened = String(reason ?? "").replace(/\s+/g, " ").trim();
914
+ const safe = redactCredentials(flattened);
915
+ return safe.length > ERROR_SUMMARY_MAX_CHARS ? `${safe.slice(0, ERROR_SUMMARY_MAX_CHARS - 3)}\u2026` : safe;
916
+ }
917
+ function summarizeErrorCause(error, stage) {
918
+ const raw = error && typeof error === "object" ? (
919
+ /** @type {any} */
920
+ error
921
+ ) : {};
922
+ const name = typeof raw.name === "string" && raw.name ? raw.name : "Error";
923
+ const rawMessage = typeof raw.message === "string" && raw.message ? raw.message : String(error ?? "");
924
+ const cause = {
925
+ stage,
926
+ name: sanitizeErrorSummary(name).slice(0, 100),
927
+ message: sanitizeErrorSummary(rawMessage)
928
+ };
929
+ if (typeof raw.code === "string" && raw.code) {
930
+ cause.code = sanitizeErrorSummary(raw.code).slice(0, 100);
931
+ }
932
+ return cause;
933
+ }
934
+
649
935
  // src/server/lib/agentic-fire.js
650
936
  import {
651
937
  assertValidDecision,
@@ -751,6 +1037,27 @@ var MAX_KEY_CHARS = 256;
751
1037
  function stateValueBytes(value) {
752
1038
  return utf82.encode(value).length;
753
1039
  }
1040
+ var warnedInvalidTtl = /* @__PURE__ */ new Set();
1041
+ function planClientStateCleanup(ttl, now) {
1042
+ if (!ttl || typeof ttl !== "object" || Array.isArray(ttl)) return [];
1043
+ const targets = [];
1044
+ for (const [namespace, days] of Object.entries(ttl)) {
1045
+ if (!namespace) continue;
1046
+ if (typeof days !== "number" || !Number.isFinite(days) || days <= 0) {
1047
+ if (!warnedInvalidTtl.has(namespace)) {
1048
+ warnedInvalidTtl.add(namespace);
1049
+ console.warn(
1050
+ `[amsg-server] clientStateTtl['${namespace}'] \u4E0D\u662F\u6B63\u6570\uFF08\u6536\u5230 ${JSON.stringify(days)}\uFF09\uFF0C\u8FD9\u4E2A\u547D\u540D\u7A7A\u95F4\u4E0D\u505A\u6E05\u7406`
1051
+ );
1052
+ }
1053
+ continue;
1054
+ }
1055
+ const updatedBefore = now - days * 24 * 60 * 60 * 1e3;
1056
+ targets.push({ namespace, updatedBefore });
1057
+ targets.push({ namespace: chunkNamespaceFor(namespace), updatedBefore });
1058
+ }
1059
+ return targets;
1060
+ }
754
1061
  async function writeClientStateEntries({ db, userId, userKey, entries }) {
755
1062
  const physicalRows = [];
756
1063
  const cleanups = [];
@@ -858,7 +1165,10 @@ function createStateAccessors({ db, userId, userKey, maxStateValueBytes, now })
858
1165
  throw new RangeError(`writeState: \u5355\u6B21\u6700\u591A ${MAX_STATE_ENTRIES_PER_BATCH} \u6761\uFF0C\u6536\u5230 ${entries.length} \u6761`);
859
1166
  }
860
1167
  if (!db || typeof db.upsertClientState !== "function") {
861
- throw new Error("AGENTIC_STATE_WRITE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state \u5199\u5165");
1168
+ throw new DeploymentConfigError(
1169
+ "AGENTIC_STATE_WRITE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state \u5199\u5165",
1170
+ { code: "AGENTIC_STATE_WRITE_UNSUPPORTED" }
1171
+ );
862
1172
  }
863
1173
  const at = nowFn();
864
1174
  const normalized = entries.map((entry, index) => {
@@ -899,51 +1209,88 @@ function createStateAccessors({ db, userId, userKey, maxStateValueBytes, now })
899
1209
  return { readState, writeState };
900
1210
  }
901
1211
 
902
- // src/server/lib/task-projection.js
903
- function projectTask(row, decryptedPayload, options = {}) {
904
- const payload = decryptedPayload || {};
905
- const metadata = payload.metadata && typeof payload.metadata === "object" ? payload.metadata : {};
906
- return {
907
- id: row.id ?? null,
908
- uuid: row.uuid ?? null,
909
- contactName: payload.contactName,
910
- // 行上的 message_type 是索引列,payload 里的是同一个值;列表接口一直读
911
- // 行上那个,这里保持一致,行上没有时退回 payload。
912
- messageType: row.message_type ?? payload.messageType,
913
- messageSubtype: payload.messageSubtype,
914
- nextSendAt: row.next_send_at ?? null,
915
- recurrenceType: payload.recurrenceType,
916
- tzId: payload.tzId ?? null,
917
- status: row.status ?? null,
918
- retryCount: row.retry_count ?? 0,
919
- createdAt: row.created_at ?? null,
920
- updatedAt: row.updated_at ?? null,
921
- // 角色归属 / 客户端侧的任务身份,取自排程方写进 metadata 的字段,宿主
922
- // 靠它按角色过滤(contactName 会跨角色重名)。缺席 → null。
923
- charId: metadata.charId ?? null,
924
- clientTaskId: metadata.amsgClientTaskId ?? null,
925
- // 凭据引用只是名字(cred_id),本体在 llm_credentials 表里,凭据字段本身
926
- // 照旧被白名单挡在外面。
927
- credRefs: payload.credRefs ?? null,
928
- // 整份 metadata 只在单条查询里给(见上面的 includeMetadata)。
929
- ...options.includeMetadata ? { metadata: payload.metadata ?? null } : {},
930
- // 上一次没发出去的原因(run-tick 记进 payload 的 lastError)。
931
- // reason 'stale' 表示错过触发时刻太久被判定不再补发;其余是投递失败的
932
- // 错误信息。payload 里没有时退回行上的 last_error 列(同形状的脱敏摘要,
933
- // 等重试期间的失败也记在那里)。没有记录 → null。
934
- // 记进去的字段整份带出来,不再挑一遍——`pushStatus`(410 = 订阅已注销,
935
- // 404 = 端点不存在)这类机读标注就是给客户端看的,白名单挡在这一层的话,
936
- // 客户端只能回去正则匹配 reason 那句人话。
937
- lastError: payload.lastError ?? parseRowLastError(row.last_error)
938
- };
1212
+ // src/server/lib/result-emitter.js
1213
+ import { buildResultPush } from "@rei-standard/amsg-shared";
1214
+
1215
+ // src/server/lib/outbox-store.js
1216
+ function supportsOutbox(db) {
1217
+ return !!db && typeof db.appendOutboxMessages === "function" && typeof db.markOutboxDelivered === "function";
939
1218
  }
940
- function parseRowLastError(raw) {
941
- if (typeof raw !== "string" || !raw) return null;
1219
+ async function appendPushesToOutbox({ db, userId, userKey, pushes }) {
1220
+ if (!supportsOutbox(db) || !pushes || pushes.length === 0) return false;
942
1221
  try {
943
- return JSON.parse(raw);
944
- } catch (_error) {
945
- return { reason: raw };
1222
+ await db.appendOutboxMessages(userId, await toOutboxRows(pushes, userKey, Date.now()));
1223
+ return true;
1224
+ } catch (error) {
1225
+ console.warn("[amsg-server] outbox \u843D\u884C\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u6295\u9012\uFF09:", error && error.message);
1226
+ return false;
1227
+ }
1228
+ }
1229
+ async function toOutboxRows(pushes, userKey, createdAt) {
1230
+ return Promise.all(pushes.map(async (push) => ({
1231
+ message_id: push.messageId,
1232
+ task_uuid: push.taskUuid ?? null,
1233
+ session_id: push.sessionId ?? null,
1234
+ message_index: push.messageIndex ?? null,
1235
+ total_messages: push.totalMessages ?? null,
1236
+ payload: await encryptForStorage(JSON.stringify(push), userKey),
1237
+ created_at: createdAt
1238
+ })));
1239
+ }
1240
+ async function markPushesDelivered({ db, userId, messageIds }) {
1241
+ if (!supportsOutbox(db) || !messageIds || messageIds.length === 0) return;
1242
+ try {
1243
+ await db.markOutboxDelivered(userId, messageIds, Date.now());
1244
+ } catch (error) {
1245
+ console.warn("[amsg-server] outbox \u6807\u8BB0 delivered \u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
1246
+ }
1247
+ }
1248
+ async function discardPushesFromOutbox({ db, userId, messageIds }) {
1249
+ if (!db || typeof db.discardOutboxMessages !== "function") return;
1250
+ if (!messageIds || messageIds.length === 0) return;
1251
+ try {
1252
+ await db.discardOutboxMessages(userId, messageIds);
1253
+ } catch (error) {
1254
+ console.warn("[amsg-server] outbox \u64A4\u56DE\u672A\u53D1\u51FA\u7684\u884C\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
1255
+ }
1256
+ }
1257
+ async function discardUndeliveredPushes({ db, userId, pushes, sentIds }) {
1258
+ const delivered = new Set(sentIds);
1259
+ await discardPushesFromOutbox({
1260
+ db,
1261
+ userId,
1262
+ messageIds: (pushes || []).map((push) => push.messageId).filter((messageId) => !delivered.has(messageId))
1263
+ });
1264
+ }
1265
+ var OUTBOX_SCAN_PAGE_SIZE = 100;
1266
+ var OUTBOX_SCAN_MAX_ROWS = 5e3;
1267
+ async function discardUndeliveredPushesForTask({ db, userId, taskUuid }) {
1268
+ if (!db || typeof db.listUnackedOutbox !== "function" || typeof db.discardOutboxMessages !== "function") return;
1269
+ if (!taskUuid) return;
1270
+ const messageIds = [];
1271
+ try {
1272
+ let cursor = 0;
1273
+ let scanned = 0;
1274
+ while (scanned < OUTBOX_SCAN_MAX_ROWS) {
1275
+ const rows = await db.listUnackedOutbox(userId, cursor, OUTBOX_SCAN_PAGE_SIZE);
1276
+ if (!rows || rows.length === 0) break;
1277
+ scanned += rows.length;
1278
+ let nextCursor = cursor;
1279
+ for (const row of rows) {
1280
+ if (row.id > nextCursor) nextCursor = row.id;
1281
+ if (row.task_uuid !== taskUuid) continue;
1282
+ if (row.delivered_at != null) continue;
1283
+ messageIds.push(row.message_id);
1284
+ }
1285
+ if (nextCursor <= cursor) break;
1286
+ cursor = nextCursor;
1287
+ if (rows.length < OUTBOX_SCAN_PAGE_SIZE) break;
1288
+ }
1289
+ } catch (error) {
1290
+ console.warn("[amsg-server] outbox \u67E5\u672A\u6295\u9012\u7684\u884C\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
1291
+ return;
946
1292
  }
1293
+ await discardPushesFromOutbox({ db, userId, messageIds });
947
1294
  }
948
1295
 
949
1296
  // src/server/lib/push-subscription-store.js
@@ -961,12 +1308,14 @@ async function savePushSubscription({ db, userId, userKey, subscription, updated
961
1308
  await db.upsertPushSubscription(userId, encrypted, at);
962
1309
  return { updatedAt: at };
963
1310
  }
964
- async function loadPushSubscription({ db, userId, userKey }) {
965
- const row = await db.getPushSubscription(userId);
1311
+ async function decodePushSubscriptionRow(row, userKey) {
966
1312
  if (!row || typeof row.subscription !== "string" || !row.subscription) return null;
967
1313
  const subscription = JSON.parse(await decryptFromStorage(row.subscription, userKey));
968
1314
  return { subscription, updatedAt: row.updated_at ?? null };
969
1315
  }
1316
+ async function loadPushSubscription({ db, userId, userKey }) {
1317
+ return decodePushSubscriptionRow(await db.getPushSubscription(userId), userKey);
1318
+ }
970
1319
  function codedError2(code, message) {
971
1320
  const error = new Error(`${code}: ${message}`);
972
1321
  error.code = code;
@@ -986,36 +1335,143 @@ async function resolvePushSubscription({ db, userId, userKey, legacyFallback = n
986
1335
  return stored.subscription;
987
1336
  }
988
1337
 
989
- // src/server/lib/outbox-store.js
990
- function supportsOutbox(db) {
991
- return !!db && typeof db.appendOutboxMessages === "function" && typeof db.markOutboxDelivered === "function";
1338
+ // src/server/lib/result-emitter.js
1339
+ function createResultEmitter({
1340
+ db,
1341
+ task,
1342
+ userKey,
1343
+ decryptedPayload,
1344
+ messageIdBase,
1345
+ sessionId,
1346
+ occurrenceMs,
1347
+ webpush,
1348
+ now
1349
+ }) {
1350
+ const nowFn = typeof now === "function" ? now : Date.now;
1351
+ let emitted = 0;
1352
+ const emitResult = async (payload) => {
1353
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
1354
+ throw new TypeError("emitResult(payload) \u9700\u8981\u4E00\u4E2A\u666E\u901A\u5BF9\u8C61");
1355
+ }
1356
+ if (!supportsOutbox(db)) {
1357
+ throw new DeploymentConfigError(
1358
+ "OUTBOX_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 message_outbox\uFF0CemitResult \u6CA1\u6709\u843D\u811A\u5904",
1359
+ { code: "OUTBOX_UNSUPPORTED" }
1360
+ );
1361
+ }
1362
+ const seq = emitted++;
1363
+ const push = buildResultPush({
1364
+ messageType: decryptedPayload.messageType || "auto",
1365
+ source: "scheduled",
1366
+ messageId: `${messageIdBase}_result_${seq}`,
1367
+ sessionId,
1368
+ ...payload,
1369
+ // 通知策略:宿主没表态就补一句「弹」。写进 payload 而不是只靠 SW 的默
1370
+ // 认行为,是为了让还没升级 SW 的客户端也弹得出来——旧版 SW 不认识
1371
+ // result 这个 kind,只认 notification.show。
1372
+ notification: withDefaultShow(payload.notification),
1373
+ // 任务身份由库覆盖写(与推送链路同一条规矩)。
1374
+ taskId: task.id ?? null,
1375
+ taskUuid: task.uuid ?? null,
1376
+ recurrenceType: decryptedPayload.recurrenceType || "none",
1377
+ occurrenceMs
1378
+ });
1379
+ await db.appendOutboxMessages(task.user_id, await toOutboxRows([push], userKey, nowFn()));
1380
+ let pushed;
1381
+ try {
1382
+ pushed = await sendResultPush({ db, task, userKey, decryptedPayload, webpush, push });
1383
+ } catch (error) {
1384
+ await discardUndeliveredPushes({ db, userId: task.user_id, pushes: [push], sentIds: [] });
1385
+ throw error;
1386
+ }
1387
+ if (pushed) {
1388
+ await markPushesDelivered({ db, userId: task.user_id, messageIds: [push.messageId] });
1389
+ }
1390
+ return { messageId: push.messageId, pushed };
1391
+ };
1392
+ return { emitResult };
992
1393
  }
993
- async function appendPushesToOutbox({ db, userId, userKey, pushes }) {
994
- if (!supportsOutbox(db) || !pushes || pushes.length === 0) return false;
1394
+ function withDefaultShow(notification) {
1395
+ if (!notification || typeof notification !== "object" || Array.isArray(notification)) {
1396
+ return { show: "always" };
1397
+ }
1398
+ if (notification.show === void 0) return { ...notification, show: "always" };
1399
+ return notification;
1400
+ }
1401
+ async function sendResultPush({ db, task, userKey, decryptedPayload, webpush, push }) {
1402
+ if (!webpush || typeof webpush.sendNotification !== "function") return false;
995
1403
  try {
996
- const now = Date.now();
997
- const rows = await Promise.all(pushes.map(async (push) => ({
998
- message_id: push.messageId,
999
- task_uuid: push.taskUuid ?? null,
1000
- session_id: push.sessionId ?? null,
1001
- message_index: push.messageIndex ?? null,
1002
- total_messages: push.totalMessages ?? null,
1003
- payload: await encryptForStorage(JSON.stringify(push), userKey),
1004
- created_at: now
1005
- })));
1006
- await db.appendOutboxMessages(userId, rows);
1404
+ const subscription = await resolvePushSubscription({
1405
+ db,
1406
+ userId: task.user_id,
1407
+ userKey,
1408
+ legacyFallback: (decryptedPayload && decryptedPayload.pushSubscription) ?? null
1409
+ });
1410
+ await sendTaggedPush(webpush, subscription, JSON.stringify(push));
1007
1411
  return true;
1008
1412
  } catch (error) {
1009
- console.warn("[amsg-server] outbox \u843D\u884C\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u6295\u9012\uFF09:", error && error.message);
1413
+ if (isTaskCancelledError(error)) throw error;
1414
+ console.warn(
1415
+ `[amsg-server] \u7ED3\u679C ${push.messageId} \u7684\u63A8\u9001\u6CA1\u53D1\u51FA\u53BB\uFF08\u5DF2\u843D\u8FDB\u6536\u4EF6\u7BB1\uFF0C\u7B49\u5BA2\u6237\u7AEF\u8865\u6536\uFF09:`,
1416
+ error && error.message
1417
+ );
1010
1418
  return false;
1011
1419
  }
1012
1420
  }
1013
- async function markPushesDelivered({ db, userId, messageIds }) {
1014
- if (!supportsOutbox(db) || !messageIds || messageIds.length === 0) return;
1421
+
1422
+ // src/server/lib/task-projection.js
1423
+ function projectTask(row, decryptedPayload, options = {}) {
1424
+ const payload = decryptedPayload || {};
1425
+ const metadata = payload.metadata && typeof payload.metadata === "object" ? payload.metadata : {};
1426
+ return {
1427
+ id: row.id ?? null,
1428
+ uuid: row.uuid ?? null,
1429
+ contactName: payload.contactName,
1430
+ // 行上的 message_type 是索引列,payload 里的是同一个值;列表接口一直读
1431
+ // 行上那个,这里保持一致,行上没有时退回 payload。
1432
+ messageType: row.message_type ?? payload.messageType,
1433
+ messageSubtype: payload.messageSubtype,
1434
+ nextSendAt: row.next_send_at ?? null,
1435
+ recurrenceType: payload.recurrenceType,
1436
+ tzId: payload.tzId ?? null,
1437
+ status: row.status ?? null,
1438
+ retryCount: row.retry_count ?? 0,
1439
+ createdAt: row.created_at ?? null,
1440
+ updatedAt: row.updated_at ?? null,
1441
+ // 角色归属 / 客户端侧的任务身份,取自排程方写进 metadata 的字段,宿主
1442
+ // 靠它按角色过滤(contactName 会跨角色重名)。缺席 → null。
1443
+ charId: metadata.charId ?? null,
1444
+ clientTaskId: metadata.amsgClientTaskId ?? null,
1445
+ // 凭据引用只是名字(cred_id),本体在 llm_credentials 表里,凭据字段本身
1446
+ // 照旧被白名单挡在外面。
1447
+ credRefs: payload.credRefs ?? null,
1448
+ // 整份 metadata 只在单条查询里给(见上面的 includeMetadata)。
1449
+ ...options.includeMetadata ? { metadata: payload.metadata ?? null } : {},
1450
+ // 上一次没发出去的原因。reason 'stale' 表示错过触发时刻太久被判定不再补
1451
+ // 发;其余是投递失败的错误信息。没有记录 → null。
1452
+ //
1453
+ // 行上的 last_error 列是权威的那一份:每次失败刷新、成功时清空。密文
1454
+ // payload 里那份是给没有这一列的适配器兜底的(run-tick 只在终审处置和过期
1455
+ // 快进时写它,成功时不会去重写整份密文)。所以只要行带着这一列——哪怕值是
1456
+ // NULL,那正说明「最近一次投递没失败」——就以它为准;反过来让 payload 优先
1457
+ // 的话,一次 410 会永远挂在这条任务上,用户重新登记订阅、之后天天正常送达
1458
+ // 也擦不掉。
1459
+ //
1460
+ // 记进去的字段整份带出来,不再挑一遍——`errorCode` 和 `pushStatus`(410 =
1461
+ // 订阅已注销,404 = 端点不存在)这类机读标注就是给客户端看的,白名单挡在
1462
+ // 这一层的话,客户端只能回去正则匹配 reason 那句人话。
1463
+ lastError: hasRowLastError(row) ? parseRowLastError(row.last_error) : payload.lastError ?? null
1464
+ };
1465
+ }
1466
+ function hasRowLastError(row) {
1467
+ return !!row && Object.prototype.hasOwnProperty.call(row, "last_error");
1468
+ }
1469
+ function parseRowLastError(raw) {
1470
+ if (typeof raw !== "string" || !raw) return null;
1015
1471
  try {
1016
- await db.markOutboxDelivered(userId, messageIds, Date.now());
1017
- } catch (error) {
1018
- console.warn("[amsg-server] outbox \u6807\u8BB0 delivered \u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
1472
+ return JSON.parse(raw);
1473
+ } catch (_error) {
1474
+ return { reason: raw };
1019
1475
  }
1020
1476
  }
1021
1477
 
@@ -1074,8 +1530,11 @@ function normalizeBeforeFireResult(result) {
1074
1530
  toolChoice: result.toolChoice
1075
1531
  };
1076
1532
  }
1077
- throw new TypeError(
1078
- "AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs?, tools?, toolChoice? } | { skip: true } | null"
1533
+ throw markPermanent(
1534
+ new TypeError(
1535
+ "AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs?, tools?, toolChoice? } | { skip: true } | null"
1536
+ ),
1537
+ "AGENTIC_BAD_BEFORE_FIRE"
1079
1538
  );
1080
1539
  }
1081
1540
  function firstPositiveInt(values, fallback) {
@@ -1093,7 +1552,10 @@ function firstPositiveNumber(values, fallback) {
1093
1552
  async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1094
1553
  const hooks = ctx.hooks;
1095
1554
  if (typeof hooks.onLLMOutput !== "function") {
1096
- throw new Error("AGENTIC_CONFIG_ERROR: hooks.onBeforeFire requires hooks.onLLMOutput to classify LLM rounds");
1555
+ throw new DeploymentConfigError(
1556
+ "AGENTIC_CONFIG_ERROR: hooks.onBeforeFire requires hooks.onLLMOutput to classify LLM rounds",
1557
+ { code: "AGENTIC_CONFIG_ERROR" }
1558
+ );
1097
1559
  }
1098
1560
  const nowFn = typeof ctx._agenticNow === "function" ? ctx._agenticNow : Date.now;
1099
1561
  const sleep = typeof ctx._agenticSleep === "function" ? ctx._agenticSleep : defaultSleep;
@@ -1104,6 +1566,20 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1104
1566
  maxStateValueBytes: ctx.maxStateValueBytes,
1105
1567
  now: nowFn
1106
1568
  });
1569
+ const sessionId = task.id != null ? `sess_task_${task.id}${occurrenceSuffix(task)}` : `sess_${randomUUID()}`;
1570
+ const messageIdBase = task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_${randomUUID()}`;
1571
+ const occurrenceMs = occurrenceMsOf(task);
1572
+ const { emitResult } = createResultEmitter({
1573
+ db: ctx.db,
1574
+ task,
1575
+ userKey,
1576
+ decryptedPayload,
1577
+ messageIdBase,
1578
+ sessionId,
1579
+ occurrenceMs,
1580
+ webpush: ctx.webpush,
1581
+ now: nowFn
1582
+ });
1107
1583
  const maxScheduledTasksPerFire = Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0 ? ctx.maxScheduledTasksPerFire : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
1108
1584
  let scheduledTaskCount = 0;
1109
1585
  async function readExistingTask(uuid) {
@@ -1178,15 +1654,6 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1178
1654
  if (typeof uuid !== "string" || !uuid.trim()) {
1179
1655
  throw new TypeError("scheduleTask: uuid \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
1180
1656
  }
1181
- if (scheduledTaskCount >= maxScheduledTasksPerFire) {
1182
- throw new RangeError(
1183
- `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`
1184
- );
1185
- }
1186
- if (!ctx.db || typeof ctx.db.createTask !== "function") {
1187
- throw new Error("AGENTIC_SCHEDULE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5EFA\u4EFB\u52A1\uFF08\u7F3A createTask\uFF09");
1188
- }
1189
- scheduledTaskCount++;
1190
1657
  const fullTaskData = {
1191
1658
  contactName,
1192
1659
  avatarUrl: options.avatarUrl === void 0 ? decryptedPayload.avatarUrl || null : options.avatarUrl || null,
@@ -1227,7 +1694,27 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1227
1694
  llmExtraBody: decryptedPayload.llmExtraBody ?? null,
1228
1695
  metadata
1229
1696
  };
1230
- const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
1697
+ const serializedTaskData = JSON.stringify(fullTaskData);
1698
+ const sizeError = validateTaskPayloadSize(serializedTaskData);
1699
+ if (sizeError) {
1700
+ throw markPermanent(
1701
+ new RangeError(`${sizeError.code}: ${sizeError.message}`),
1702
+ sizeError.code
1703
+ );
1704
+ }
1705
+ if (scheduledTaskCount >= maxScheduledTasksPerFire) {
1706
+ throw new RangeError(
1707
+ `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`
1708
+ );
1709
+ }
1710
+ if (!ctx.db || typeof ctx.db.createTask !== "function") {
1711
+ throw new DeploymentConfigError(
1712
+ "AGENTIC_SCHEDULE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5EFA\u4EFB\u52A1\uFF08\u7F3A createTask\uFF09",
1713
+ { code: "AGENTIC_SCHEDULE_UNSUPPORTED" }
1714
+ );
1715
+ }
1716
+ scheduledTaskCount++;
1717
+ const encryptedPayload = await encryptForStorage(serializedTaskData, userKey);
1231
1718
  let created;
1232
1719
  try {
1233
1720
  created = await ctx.db.createTask({
@@ -1249,7 +1736,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1249
1736
  throw error;
1250
1737
  }
1251
1738
  if (!created) {
1252
- throw new Error("AGENTIC_SCHEDULE_FAILED: createTask \u6CA1\u6709\u8FD4\u56DE\u65B0\u5EFA\u7684\u4EFB\u52A1\u884C");
1739
+ throw new NonRetryableError(
1740
+ "AGENTIC_SCHEDULE_FAILED: createTask \u6CA1\u6709\u8FD4\u56DE\u65B0\u5EFA\u7684\u4EFB\u52A1\u884C",
1741
+ { code: "AGENTIC_SCHEDULE_FAILED" }
1742
+ );
1253
1743
  }
1254
1744
  return {
1255
1745
  created: true,
@@ -1268,7 +1758,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1268
1758
  );
1269
1759
  }
1270
1760
  if (!ctx.db || typeof ctx.db.deleteTaskByUuid !== "function") {
1271
- throw new Error("AGENTIC_CANCEL_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5220\u4EFB\u52A1\uFF08\u7F3A deleteTaskByUuid\uFF09");
1761
+ throw new DeploymentConfigError(
1762
+ "AGENTIC_CANCEL_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5220\u4EFB\u52A1\uFF08\u7F3A deleteTaskByUuid\uFF09",
1763
+ { code: "AGENTIC_CANCEL_UNSUPPORTED" }
1764
+ );
1272
1765
  }
1273
1766
  const cancelled = await ctx.db.deleteTaskByUuid(uuid, task.user_id);
1274
1767
  return { cancelled: !!cancelled };
@@ -1294,7 +1787,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1294
1787
  );
1295
1788
  }
1296
1789
  if (!ctx.db || typeof ctx.db.getTaskByUuid !== "function" || typeof ctx.db.updateTaskByUuid !== "function") {
1297
- throw new Error("AGENTIC_RENEW_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u6539\u4EFB\u52A1\uFF08\u7F3A getTaskByUuid / updateTaskByUuid\uFF09");
1790
+ throw new DeploymentConfigError(
1791
+ "AGENTIC_RENEW_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u6539\u4EFB\u52A1\uFF08\u7F3A getTaskByUuid / updateTaskByUuid\uFF09",
1792
+ { code: "AGENTIC_RENEW_UNSUPPORTED" }
1793
+ );
1298
1794
  }
1299
1795
  const row = await ctx.db.getTaskByUuid(uuid, task.user_id);
1300
1796
  if (!row) return { renewed: false, reason: "not_found" };
@@ -1322,6 +1818,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1322
1818
  userId: task.user_id,
1323
1819
  readState,
1324
1820
  writeState,
1821
+ emitResult,
1325
1822
  scheduleTask,
1326
1823
  cancelTask,
1327
1824
  renewTask,
@@ -1343,13 +1840,17 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1343
1840
  sleep,
1344
1841
  readState,
1345
1842
  writeState,
1843
+ emitResult,
1346
1844
  scratch,
1347
1845
  scheduleTask,
1348
1846
  cancelTask,
1349
1847
  renewTask,
1350
1848
  resolveLlmCredential: resolveLlmCredential2,
1351
1849
  fireCtx,
1352
- progress
1850
+ progress,
1851
+ sessionId,
1852
+ messageIdBase,
1853
+ occurrenceMs
1353
1854
  });
1354
1855
  settledStatus = !outcome.handled ? "not-handled" : outcome.result.status === "skipped" ? "skipped" : "sent";
1355
1856
  return outcome;
@@ -1375,7 +1876,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1375
1876
  usage: progress.usage,
1376
1877
  scratch,
1377
1878
  readState,
1378
- writeState
1879
+ writeState,
1880
+ emitResult
1379
1881
  });
1380
1882
  }
1381
1883
  }
@@ -1389,13 +1891,17 @@ async function runFireChain({
1389
1891
  sleep,
1390
1892
  readState,
1391
1893
  writeState,
1894
+ emitResult,
1392
1895
  scratch,
1393
1896
  scheduleTask,
1394
1897
  cancelTask,
1395
1898
  renewTask,
1396
1899
  resolveLlmCredential: resolveLlmCredential2,
1397
1900
  fireCtx,
1398
- progress
1901
+ progress,
1902
+ sessionId,
1903
+ messageIdBase,
1904
+ occurrenceMs
1399
1905
  }) {
1400
1906
  const before = await hooks.onBeforeFire(fireCtx);
1401
1907
  if (before == null) return { handled: false };
@@ -1413,8 +1919,6 @@ async function runFireChain({
1413
1919
  DEFAULT_TOTAL_TIMEOUT_MS
1414
1920
  );
1415
1921
  const deadline = nowFn() + totalTimeoutMs;
1416
- const sessionId = task.id != null ? `sess_task_${task.id}${occurrenceSuffix(task)}` : `sess_${randomUUID()}`;
1417
- const occurrenceMs = occurrenceMsOf(task);
1418
1922
  let messages = normalized.messages.slice();
1419
1923
  const chatCred = await resolveFireCredentials({
1420
1924
  db: ctx.db,
@@ -1457,13 +1961,18 @@ async function runFireChain({
1457
1961
  occurrenceMs,
1458
1962
  readState,
1459
1963
  writeState,
1964
+ emitResult,
1460
1965
  scheduleTask,
1461
1966
  cancelTask,
1462
1967
  renewTask,
1463
1968
  resolveLlmCredential: resolveLlmCredential2
1464
1969
  });
1465
1970
  const decision = await hooks.onLLMOutput(sessionCtx);
1466
- assertValidDecision(decision, { inlineToolCalls: true });
1971
+ try {
1972
+ assertValidDecision(decision, { inlineToolCalls: true });
1973
+ } catch (error) {
1974
+ throw markPermanent(error, "AGENTIC_BAD_DECISION");
1975
+ }
1467
1976
  if (decision.decision === "continue") {
1468
1977
  messages = decision.nextHistory.slice();
1469
1978
  continue;
@@ -1478,6 +1987,7 @@ async function runFireChain({
1478
1987
  decryptedPayload,
1479
1988
  ctx,
1480
1989
  sessionId,
1990
+ messageIdBase,
1481
1991
  occurrenceMs,
1482
1992
  task,
1483
1993
  userKey,
@@ -1485,16 +1995,23 @@ async function runFireChain({
1485
1995
  scratch,
1486
1996
  readState,
1487
1997
  writeState,
1998
+ emitResult,
1488
1999
  progress
1489
2000
  });
1490
2001
  return { handled: true, result: { success: true, messagesSent, status: "finished", iterations: iteration + 1 } };
1491
2002
  }
1492
2003
  const toolCalls = extractToolCallsFromDecision(decision);
1493
2004
  if (toolCalls.length === 0) {
1494
- throw new Error("AGENTIC_EMPTY_TOOL_REQUEST: tool-request decision carried no toolCalls (neither decision.toolCalls nor pushPayloads[].toolCalls)");
2005
+ throw taggedRetryableError(
2006
+ "AGENTIC_EMPTY_TOOL_REQUEST: tool-request decision carried no toolCalls (neither decision.toolCalls nor pushPayloads[].toolCalls)",
2007
+ "AGENTIC_EMPTY_TOOL_REQUEST"
2008
+ );
1495
2009
  }
1496
2010
  if (typeof hooks.executeToolCalls !== "function") {
1497
- throw new Error("AGENTIC_CONFIG_ERROR: onLLMOutput returned tool-request but hooks.executeToolCalls is not configured");
2011
+ throw new DeploymentConfigError(
2012
+ "AGENTIC_CONFIG_ERROR: onLLMOutput returned tool-request but hooks.executeToolCalls is not configured",
2013
+ { code: "AGENTIC_CONFIG_ERROR" }
2014
+ );
1498
2015
  }
1499
2016
  if (iteration === maxToolIterations - 1) {
1500
2017
  break;
@@ -1518,7 +2035,15 @@ async function runFireChain({
1518
2035
  const assistantWithTools = synthesized.length === 0 ? assistantMessage : { ...assistantMessage, tool_calls: [...nativeCalls, ...synthesized] };
1519
2036
  messages = [...messages.slice(0, -1), assistantWithTools, ...toolResults];
1520
2037
  }
1521
- throw new Error(`AGENTIC_LOOP_EXCEEDED: no finish/skip-push decision within ${maxToolIterations} LLM round(s)`);
2038
+ throw taggedRetryableError(
2039
+ `AGENTIC_LOOP_EXCEEDED: no finish/skip-push decision within ${maxToolIterations} LLM round(s)`,
2040
+ "AGENTIC_LOOP_EXCEEDED"
2041
+ );
2042
+ }
2043
+ function taggedRetryableError(message, code) {
2044
+ const error = new Error(message);
2045
+ error.code = code;
2046
+ return error;
1522
2047
  }
1523
2048
  async function notifyAfterSend(ctx, info) {
1524
2049
  if (typeof ctx.onAfterSend !== "function") return;
@@ -1547,6 +2072,7 @@ async function sendHookPushPayloads({
1547
2072
  decryptedPayload,
1548
2073
  ctx,
1549
2074
  sessionId,
2075
+ messageIdBase,
1550
2076
  occurrenceMs,
1551
2077
  task,
1552
2078
  userKey,
@@ -1554,13 +2080,15 @@ async function sendHookPushPayloads({
1554
2080
  scratch,
1555
2081
  readState,
1556
2082
  writeState,
2083
+ emitResult,
1557
2084
  progress
1558
2085
  }) {
1559
2086
  const total = pushPayloads.length;
1560
2087
  let sentCount = 0;
1561
2088
  progress.total = total;
1562
- const afterSendBase = { task, total, scratch, readState, writeState, usage: progress.usage };
2089
+ const afterSendBase = { task, total, scratch, readState, writeState, emitResult, usage: progress.usage };
1563
2090
  const sentIds = [];
2091
+ const finalized = [];
1564
2092
  try {
1565
2093
  if (!ctx.vapid || !ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
1566
2094
  throw new Error("VAPID configuration missing - push notifications cannot be sent");
@@ -1571,8 +2099,6 @@ async function sendHookPushPayloads({
1571
2099
  userKey,
1572
2100
  legacyFallback: (decryptedPayload && decryptedPayload.pushSubscription) ?? null
1573
2101
  });
1574
- const messageIdBase = task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_${randomUUID()}`;
1575
- const finalized = [];
1576
2102
  for (let i = 0; i < total; i++) {
1577
2103
  const push = { ...pushPayloads[i] };
1578
2104
  if (typeof push.messageId !== "string" || !push.messageId) push.messageId = `${messageIdBase}_hook_${i}`;
@@ -1585,7 +2111,7 @@ async function sendHookPushPayloads({
1585
2111
  }
1586
2112
  await appendPushesToOutbox({ db: ctx.db, userId: task.user_id, userKey, pushes: finalized });
1587
2113
  for (let i = 0; i < total; i++) {
1588
- await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(finalized[i]));
2114
+ await sendTaggedPush(ctx.webpush, pushSubscription, JSON.stringify(finalized[i]));
1589
2115
  sentCount++;
1590
2116
  progress.sentCount = sentCount;
1591
2117
  sentIds.push(finalized[i].messageId);
@@ -1593,6 +2119,14 @@ async function sendHookPushPayloads({
1593
2119
  }
1594
2120
  } catch (error) {
1595
2121
  await markPushesDelivered({ db: ctx.db, userId: task.user_id, messageIds: sentIds });
2122
+ if (isTaskCancelledError(error)) {
2123
+ await discardUndeliveredPushes({
2124
+ db: ctx.db,
2125
+ userId: task.user_id,
2126
+ pushes: finalized,
2127
+ sentIds
2128
+ });
2129
+ }
1596
2130
  await notifyAfterSend(ctx, { ...afterSendBase, sentCount, error });
1597
2131
  throw error;
1598
2132
  }
@@ -1601,58 +2135,22 @@ async function sendHookPushPayloads({
1601
2135
  return total;
1602
2136
  }
1603
2137
 
1604
- // src/server/lib/errors.js
1605
- var NonRetryableError = class extends Error {
1606
- /**
1607
- * @param {string} message
1608
- * @param {{ code?: string, cause?: unknown }} [options]
1609
- */
1610
- constructor(message, options = {}) {
1611
- super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
1612
- this.name = "NonRetryableError";
1613
- this.permanent = true;
1614
- if (options.code) this.code = options.code;
1615
- }
1616
- };
1617
- function isNonRetryableError(error) {
1618
- return !!error && typeof error === "object" && /** @type {any} */
1619
- error.permanent === true;
1620
- }
1621
- function sanitizeErrorSummary(reason) {
1622
- let s = String(reason ?? "").replace(/\s+/g, " ").trim();
1623
- s = s.replace(/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer [redacted]");
1624
- s = s.replace(/\b[A-Za-z]{2,6}-[A-Za-z0-9_-]{16,}/g, "[redacted]");
1625
- s = s.replace(/[A-Za-z0-9+/_.-]{48,}/g, "[redacted]");
1626
- if (s.length > 500) s = `${s.slice(0, 497)}\u2026`;
1627
- return s;
1628
- }
1629
- function summarizeErrorCause(error, stage) {
1630
- const raw = error && typeof error === "object" ? (
1631
- /** @type {any} */
1632
- error
1633
- ) : {};
1634
- const name = typeof raw.name === "string" && raw.name ? raw.name : "Error";
1635
- const rawMessage = typeof raw.message === "string" && raw.message ? raw.message : String(error ?? "");
1636
- const cause = {
1637
- stage,
1638
- name: sanitizeErrorSummary(name).slice(0, 100),
1639
- message: sanitizeErrorSummary(rawMessage)
1640
- };
1641
- if (typeof raw.code === "string" && raw.code) {
1642
- cause.code = sanitizeErrorSummary(raw.code).slice(0, 100);
1643
- }
1644
- return cause;
1645
- }
1646
-
1647
2138
  // src/server/lib/message-processor.js
1648
2139
  import {
1649
2140
  buildContentPush,
2141
+ buildMultipartPushPayloads,
1650
2142
  buildReasoningPush,
1651
2143
  readReasoningContent,
1652
- stripReasoningTags
2144
+ stripReasoningTags,
2145
+ DEFAULT_MULTIPART_CHUNK_BYTES,
2146
+ DEFAULT_MULTIPART_MAX_CHUNKS,
2147
+ DEFAULT_MULTIPART_MAX_TOTAL_BYTES,
2148
+ DEFAULT_MULTIPART_TTL_MS
1653
2149
  } from "@rei-standard/amsg-shared";
1654
2150
  var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
1655
2151
  var SLEEP_BETWEEN_MESSAGES_MS2 = 1500;
2152
+ var MULTIPART_WINDOW_USAGE = 0.5;
2153
+ var MIN_SLEEP_BETWEEN_CHUNKS_MS = 50;
1656
2154
  function splitOnceByRegex(chunk, regex) {
1657
2155
  const out = chunk.split(regex).reduce((acc, part, i, arr) => {
1658
2156
  if (i % 2 === 0 && part.trim()) {
@@ -1672,6 +2170,74 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
1672
2170
  }
1673
2171
  return chunks.length > 0 ? chunks : [messageContent];
1674
2172
  }
2173
+ async function deliverReasoningPush(ctx, pushSubscription, reasoningPush) {
2174
+ const multipart = resolveMultipartOptions(ctx);
2175
+ try {
2176
+ const serialized = JSON.stringify(reasoningPush);
2177
+ const { bytes, withinLimit } = measurePushPayload(serialized);
2178
+ if (withinLimit) {
2179
+ await sendTaggedPush(ctx.webpush, pushSubscription, serialized);
2180
+ return { shipped: true };
2181
+ }
2182
+ if (bytes > multipart.maxTotalBytes) {
2183
+ throw new Error(`\u601D\u8003\u8FC7\u7A0B ${bytes} \u5B57\u8282\uFF0C\u8D85\u8FC7\u5206\u7247\u4F20\u8F93\u7684 ${multipart.maxTotalBytes} \u5B57\u8282\u4E0A\u9650`);
2184
+ }
2185
+ const parts = buildMultipartPushPayloads(reasoningPush, {
2186
+ serializedPayload: serialized,
2187
+ maxChunkBytes: multipart.maxChunkBytes,
2188
+ ttlMs: multipart.ttlMs
2189
+ });
2190
+ if (parts.length > multipart.maxChunks) {
2191
+ throw new Error(`\u601D\u8003\u8FC7\u7A0B\u8981\u5207 ${parts.length} \u7247\uFF0C\u8D85\u8FC7\u5206\u7247\u4F20\u8F93\u7684 ${multipart.maxChunks} \u7247\u4E0A\u9650`);
2192
+ }
2193
+ const intervalMs = resolveChunkIntervalMs(parts.length, multipart.ttlMs);
2194
+ if (intervalMs === null) {
2195
+ throw new Error(
2196
+ `\u601D\u8003\u8FC7\u7A0B\u8981\u5207 ${parts.length} \u7247\uFF0C${multipart.ttlMs} \u6BEB\u79D2\u7684\u5206\u7247\u91CD\u7EC4\u7A97\u53E3\u5185\u53D1\u4E0D\u5B8C`
2197
+ );
2198
+ }
2199
+ for (let i = 0; i < parts.length; i++) {
2200
+ await sendTaggedPush(ctx.webpush, pushSubscription, JSON.stringify(parts[i]));
2201
+ if (i < parts.length - 1) {
2202
+ await sleepFor(ctx, intervalMs);
2203
+ }
2204
+ }
2205
+ return { shipped: true };
2206
+ } catch (error) {
2207
+ if (isTaskCancelledError(error)) throw error;
2208
+ const reason = sanitizeErrorSummary(error && error.message);
2209
+ console.warn("[amsg-server] \u601D\u8003\u8FC7\u7A0B\u672A\u9001\u8FBE\uFF08\u6B63\u6587\u7167\u5E38\u53D1\u9001\uFF09:", reason);
2210
+ return { shipped: false, error: reason };
2211
+ }
2212
+ }
2213
+ function resolveChunkIntervalMs(chunkCount, windowMs) {
2214
+ if (chunkCount <= 1) return 0;
2215
+ const budgetMs = Math.floor(windowMs * MULTIPART_WINDOW_USAGE);
2216
+ const evenlySpaced = Math.floor(budgetMs / (chunkCount - 1));
2217
+ if (evenlySpaced < MIN_SLEEP_BETWEEN_CHUNKS_MS) return null;
2218
+ return Math.min(SLEEP_BETWEEN_MESSAGES_MS2, evenlySpaced);
2219
+ }
2220
+ function sleepFor(ctx, ms) {
2221
+ if (!(ms > 0)) return Promise.resolve();
2222
+ if (ctx && typeof ctx._pushSleep === "function") return ctx._pushSleep(ms);
2223
+ return new Promise((resolve) => setTimeout(resolve, ms));
2224
+ }
2225
+ function resolveMultipartOptions(ctx) {
2226
+ const configured = ctx && ctx.multipart && typeof ctx.multipart === "object" ? ctx.multipart : {};
2227
+ return {
2228
+ maxChunkBytes: positiveIntegerOr(configured.maxChunkBytes, DEFAULT_MULTIPART_CHUNK_BYTES),
2229
+ maxChunks: positiveIntegerOr(configured.maxChunks, DEFAULT_MULTIPART_MAX_CHUNKS),
2230
+ maxTotalBytes: positiveIntegerOr(configured.maxTotalBytes, DEFAULT_MULTIPART_MAX_TOTAL_BYTES),
2231
+ ttlMs: positiveIntegerOr(configured.ttlMs, DEFAULT_MULTIPART_TTL_MS)
2232
+ };
2233
+ }
2234
+ function positiveIntegerOr(value, fallback) {
2235
+ return Number.isInteger(value) && /** @type {number} */
2236
+ value > 0 ? (
2237
+ /** @type {number} */
2238
+ value
2239
+ ) : fallback;
2240
+ }
1675
2241
  async function processSingleMessage(task, ctx, providedMasterKey, predecrypted = null) {
1676
2242
  try {
1677
2243
  const masterKey = providedMasterKey || ctx.masterKey;
@@ -1730,9 +2296,10 @@ async function processSingleMessage(task, ctx, providedMasterKey, predecrypted =
1730
2296
  const metadata = decryptedPayload.metadata || {};
1731
2297
  const messageIdBase = task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_${randomUUID()}_instant`;
1732
2298
  const occurrenceMs = occurrenceMsOf(task);
1733
- const pushesToSend = [];
2299
+ const contentPushes = [];
2300
+ let reasoningPush = null;
1734
2301
  if (reasoning) {
1735
- const reasoningPush = buildReasoningPush({
2302
+ reasoningPush = buildReasoningPush({
1736
2303
  messageType: decryptedPayload.messageType,
1737
2304
  source,
1738
2305
  messageId: `${messageIdBase}_reasoning`,
@@ -1746,7 +2313,6 @@ async function processSingleMessage(task, ctx, providedMasterKey, predecrypted =
1746
2313
  metadata
1747
2314
  });
1748
2315
  stampTaskIdentity(reasoningPush, task, decryptedPayload, occurrenceMs);
1749
- pushesToSend.push(reasoningPush);
1750
2316
  }
1751
2317
  for (let i = 0; i < messages.length; i++) {
1752
2318
  const contentPush = buildContentPush({
@@ -1765,29 +2331,56 @@ async function processSingleMessage(task, ctx, providedMasterKey, predecrypted =
1765
2331
  metadata
1766
2332
  });
1767
2333
  stampTaskIdentity(contentPush, task, decryptedPayload, occurrenceMs);
1768
- pushesToSend.push(contentPush);
2334
+ contentPushes.push(contentPush);
1769
2335
  }
2336
+ const pushesToSend = reasoningPush ? [reasoningPush, ...contentPushes] : contentPushes;
1770
2337
  await appendPushesToOutbox({ db: ctx.db, userId: task.user_id, userKey, pushes: pushesToSend });
1771
2338
  const sentIds = [];
2339
+ let cancelledMidBurst = false;
2340
+ let reasoningError;
1772
2341
  try {
1773
- for (let i = 0; i < pushesToSend.length; i++) {
1774
- await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(pushesToSend[i]));
1775
- sentIds.push(pushesToSend[i].messageId);
1776
- if (i < pushesToSend.length - 1) {
1777
- await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS2));
2342
+ if (reasoningPush) {
2343
+ const reasoning2 = await deliverReasoningPush(ctx, pushSubscription, reasoningPush);
2344
+ if (reasoning2.shipped) {
2345
+ sentIds.push(reasoningPush.messageId);
2346
+ await sleepFor(ctx, SLEEP_BETWEEN_MESSAGES_MS2);
2347
+ } else {
2348
+ reasoningError = reasoning2.error;
2349
+ }
2350
+ }
2351
+ for (let i = 0; i < contentPushes.length; i++) {
2352
+ await sendTaggedPush(ctx.webpush, pushSubscription, JSON.stringify(contentPushes[i]));
2353
+ sentIds.push(contentPushes[i].messageId);
2354
+ if (i < contentPushes.length - 1) {
2355
+ await sleepFor(ctx, SLEEP_BETWEEN_MESSAGES_MS2);
1778
2356
  }
1779
2357
  }
2358
+ } catch (error) {
2359
+ cancelledMidBurst = isTaskCancelledError(error);
2360
+ throw error;
1780
2361
  } finally {
1781
2362
  await markPushesDelivered({ db: ctx.db, userId: task.user_id, messageIds: sentIds });
2363
+ if (cancelledMidBurst) {
2364
+ await discardUndeliveredPushes({
2365
+ db: ctx.db,
2366
+ userId: task.user_id,
2367
+ pushes: pushesToSend,
2368
+ sentIds
2369
+ });
2370
+ }
1782
2371
  }
1783
- return { success: true, messagesSent: messages.length };
2372
+ return {
2373
+ success: true,
2374
+ messagesSent: messages.length,
2375
+ ...reasoningError ? { reasoningError } : {}
2376
+ };
1784
2377
  } catch (error) {
1785
2378
  return {
1786
2379
  success: false,
1787
2380
  messagesSent: 0,
1788
2381
  error: error.message,
1789
2382
  errorCode: error.code || null,
1790
- pushStatusCode: Number.isInteger(error.statusCode) ? error.statusCode : null,
2383
+ pushStatusCode: readPushStatusCode(error),
1791
2384
  permanent: isNonRetryableError(error)
1792
2385
  };
1793
2386
  }
@@ -1821,7 +2414,12 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
1821
2414
  }
1822
2415
  const result = await processSingleMessage(task, ctx, masterKey);
1823
2416
  if (!result.success) {
1824
- if (!result.permanent && retryCount < maxRetries) {
2417
+ const permanent = isPermanentDeliveryFailure({
2418
+ permanent: result.permanent,
2419
+ errorCode: result.errorCode,
2420
+ pushStatus: result.pushStatusCode
2421
+ });
2422
+ if (!permanent && retryCount < maxRetries) {
1825
2423
  retryCount++;
1826
2424
  await new Promise((resolve) => setTimeout(resolve, 1e3 * retryCount));
1827
2425
  continue;
@@ -1830,10 +2428,15 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
1830
2428
  await ctx.db.updateTaskById(task.id, {
1831
2429
  status: "failed",
1832
2430
  retry_count: retryCount,
2431
+ // 记录的形状跟定时任务那条路一致(同一个 buildErrorExtra):reason
2432
+ // 是给用户看的人话,errorCode / pushStatus 是给下游判定用的——410 =
2433
+ // 订阅已注销,客户端要据此引导用户重新登记,而不是回去正则匹配
2434
+ // reason 里那句话。
1833
2435
  last_error: JSON.stringify({
1834
2436
  at: (/* @__PURE__ */ new Date()).toISOString(),
1835
2437
  occurrence: task.next_send_at ?? null,
1836
- reason: sanitizeErrorSummary(result.error)
2438
+ reason: sanitizeErrorSummary(result.error),
2439
+ ...buildErrorExtra(result.errorCode, result.pushStatusCode) || {}
1837
2440
  })
1838
2441
  });
1839
2442
  } catch (_updateError) {
@@ -1848,7 +2451,7 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
1848
2451
  code: "PROCESSING_ERROR",
1849
2452
  message: result.error,
1850
2453
  retriesAttempted: retryCount,
1851
- ...result.permanent ? { permanent: true } : {}
2454
+ ...permanent ? { permanent: true } : {}
1852
2455
  }
1853
2456
  };
1854
2457
  }
@@ -1868,7 +2471,12 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
1868
2471
  }
1869
2472
  };
1870
2473
  }
1871
- return { success: true, messagesSent: result.messagesSent, retriesUsed: retryCount };
2474
+ return {
2475
+ success: true,
2476
+ messagesSent: result.messagesSent,
2477
+ retriesUsed: retryCount,
2478
+ ...result.reasoningError ? { reasoningError: result.reasoningError } : {}
2479
+ };
1872
2480
  }
1873
2481
  }
1874
2482
 
@@ -2004,7 +2612,12 @@ function createScheduleMessageHandler(ctx) {
2004
2612
  llmExtraBody: payload.llmExtraBody ?? null,
2005
2613
  metadata: payload.metadata || {}
2006
2614
  };
2007
- const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
2615
+ const serializedTaskData = JSON.stringify(fullTaskData);
2616
+ const sizeError = validateTaskPayloadSize(serializedTaskData);
2617
+ if (sizeError) {
2618
+ return { status: 400, body: { success: false, error: sizeError } };
2619
+ }
2620
+ const encryptedPayload = await encryptForStorage(serializedTaskData, userKey);
2008
2621
  if (payload.messageType === "instant") {
2009
2622
  if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
2010
2623
  return {
@@ -2064,6 +2677,9 @@ function createScheduleMessageHandler(ctx) {
2064
2677
  if (!dbResult) {
2065
2678
  return { status: 500, body: { success: false, error: { code: "TASK_CREATE_FAILED", message: "\u521B\u5EFA\u4EFB\u52A1\u5931\u8D25" } } };
2066
2679
  }
2680
+ if (superseded) {
2681
+ await discardUndeliveredPushesForTask({ db, userId, taskUuid: supersedesUuid });
2682
+ }
2067
2683
  if (payload.messageType === "instant") {
2068
2684
  try {
2069
2685
  const sendResult = await processMessagesByUuid(taskUuid, {
@@ -2085,6 +2701,9 @@ function createScheduleMessageHandler(ctx) {
2085
2701
  sentAt: (/* @__PURE__ */ new Date()).toISOString(),
2086
2702
  status: "sent",
2087
2703
  retriesUsed: sendResult.retriesUsed || 0,
2704
+ // 思考过程那条 push 没发出去的原因(有才带上)。正文照发,所以
2705
+ // 这次调用仍是成功的;调用方拿它决定要不要提示 / 重发。
2706
+ ...sendResult.reasoningError ? { reasoningError: sendResult.reasoningError } : {},
2088
2707
  ...supersedesUuid ? { superseded } : {}
2089
2708
  }
2090
2709
  }
@@ -2120,10 +2739,50 @@ var CLAIM_LEASE_MARGIN_MS = 2 * 60 * 1e3;
2120
2739
  var DEFAULT_LEASE_HEARTBEAT_MS = 30 * 1e3;
2121
2740
  var DEFAULT_HEARTBEAT_LEASE_TTL_MS = 90 * 1e3;
2122
2741
  var STALE_AFTER_MS = 60 * 60 * 1e3;
2123
- var TERMINAL_PUSH_STATUSES = /* @__PURE__ */ new Set([404, 410]);
2742
+ var adaptersWithoutLastErrorColumn = /* @__PURE__ */ new WeakSet();
2743
+ var lastErrorColumnSuspicions = /* @__PURE__ */ new WeakMap();
2744
+ var warnedAboutMissingLastErrorColumn = false;
2745
+ var adaptersWarnedAboutRetryAfter = /* @__PURE__ */ new WeakSet();
2746
+ function warnMissingRetryAfterColumn(db) {
2747
+ if (!db || adaptersWarnedAboutRetryAfter.has(db)) return;
2748
+ adaptersWarnedAboutRetryAfter.add(db);
2749
+ console.warn(
2750
+ "[amsg-server] \u9002\u914D\u5668\u8FD4\u56DE\u7684\u4EFB\u52A1\u884C\u91CC\u6CA1\u6709 retry_after\uFF0C\u9000\u907F\u5B88\u536B\u5931\u6548\uFF1A\u8FD8\u5728\u9000\u907F\u91CC\u7684\u4EFB\u52A1\u4F1A\u88AB\u7ACB\u523B\u91CD\u8DD1\u3002\u6295\u9012\u8DEF\u5F84\u7684\u884C\u8981\u5E26\u4E0A\u8FD9\u4E00\u5217\uFF0C\u89C1 adapters/interface.js \u7684 TASK_DELIVERY_COLUMNS\u3002"
2751
+ );
2752
+ }
2753
+ function warnMissingLastErrorColumn(error) {
2754
+ if (warnedAboutMissingLastErrorColumn) return;
2755
+ warnedAboutMissingLastErrorColumn = true;
2756
+ console.warn(
2757
+ "[amsg-server] \u5E26 last_error \u7684\u5199\u6CA1\u6210\u529F\uFF0C\u5DF2\u9000\u56DE\u53EA\u5199\u72B6\u6001\u5B57\u6BB5\uFF08\u5931\u8D25\u539F\u56E0\u4ECD\u8BB0\u5728 payload \u7684 lastError \u91CC\uFF09\u3002\u5E93\u5347\u7EA7\u540E\u6CA1\u91CD\u8DD1\u8FC7 /init-tenant \u7684\u8BDD\uFF0C\u8865\u4E00\u6B21\u8868\u7ED3\u6784\u5C31\u80FD\u6062\u590D\uFF1B\u53EA\u662F\u5076\u53D1\u7684\u8BDD\u4E0D\u7528\u7BA1:",
2758
+ error && /** @type {{ message?: unknown }} */
2759
+ error.message
2760
+ );
2761
+ }
2124
2762
  function toPushStatus(value) {
2125
2763
  return Number.isInteger(value) ? value : null;
2126
2764
  }
2765
+ function rowVanished(writeResult) {
2766
+ return writeResult === null || writeResult === false;
2767
+ }
2768
+ function guardWebpushWithLease(webpush, lease) {
2769
+ if (!webpush || typeof webpush.sendNotification !== "function") return webpush;
2770
+ const guarded = Object.create(webpush);
2771
+ Object.defineProperty(guarded, "sendNotification", {
2772
+ value: async (...args) => {
2773
+ if (lease.lost) {
2774
+ const error = new Error("\u4EFB\u52A1\u5728\u6295\u9012\u671F\u95F4\u88AB\u53D6\u6D88\u6216\u9876\u66FF\uFF0C\u63A8\u9001\u5DF2\u4E2D\u6B62");
2775
+ error.code = TASK_CANCELLED_CODE;
2776
+ throw error;
2777
+ }
2778
+ return webpush.sendNotification(...args);
2779
+ },
2780
+ writable: true,
2781
+ enumerable: true,
2782
+ configurable: true
2783
+ });
2784
+ return guarded;
2785
+ }
2127
2786
  function resolveStaleAfterMs(ctx) {
2128
2787
  return positiveNumber(ctx.staleAfterMs) || STALE_AFTER_MS;
2129
2788
  }
@@ -2153,8 +2812,19 @@ async function runScheduledTick(ctx) {
2153
2812
  console.warn("[amsg-server] cleanupOutbox \u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
2154
2813
  }
2155
2814
  }
2815
+ await cleanupExpiredClientState(ctx);
2156
2816
  return summary;
2157
2817
  }
2818
+ async function cleanupExpiredClientState(ctx) {
2819
+ if (typeof ctx.db.cleanupClientState !== "function") return;
2820
+ const targets = planClientStateCleanup(ctx.clientStateTtl, Date.now());
2821
+ if (targets.length === 0) return;
2822
+ try {
2823
+ await ctx.db.cleanupClientState(targets);
2824
+ } catch (error) {
2825
+ console.warn("[amsg-server] cleanupClientState \u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
2826
+ }
2827
+ }
2158
2828
  async function runTask(ctx, uuid) {
2159
2829
  if (typeof uuid !== "string" || !uuid.trim()) {
2160
2830
  throw new TypeError("runTask(ctx, uuid) requires a non-empty uuid string");
@@ -2171,6 +2841,9 @@ async function runTask(ctx, uuid) {
2171
2841
  if (Number.isFinite(dueMs) && dueMs > Date.now()) {
2172
2842
  return { ran: false, reason: "not_due", nextSendAt: task.next_send_at };
2173
2843
  }
2844
+ if (!Object.prototype.hasOwnProperty.call(task, "retry_after")) {
2845
+ warnMissingRetryAfterColumn(ctx.db);
2846
+ }
2174
2847
  const retryAfterMs = task.retry_after ? Date.parse(task.retry_after) : NaN;
2175
2848
  if (Number.isFinite(retryAfterMs) && retryAfterMs > Date.now()) {
2176
2849
  return { ran: false, reason: "retry_pending", retryAfter: task.retry_after };
@@ -2197,6 +2870,8 @@ async function deliverTasks(ctx, tasks) {
2197
2870
  deletedOnceOffTasks: 0,
2198
2871
  updatedRecurringTasks: 0,
2199
2872
  staleTasks: [],
2873
+ cancelledTasks: [],
2874
+ reasoningSkippedTasks: [],
2200
2875
  failedTasks: []
2201
2876
  };
2202
2877
  const groupsTakenThisTick = /* @__PURE__ */ new Set();
@@ -2209,17 +2884,25 @@ async function deliverTasks(ctx, tasks) {
2209
2884
  return !!await db.claimTask(task.id, task.next_send_at, leaseUntil, serializeGroup);
2210
2885
  }
2211
2886
  function startLeaseHeartbeat(task) {
2212
- if (!heartbeatEnabled) return () => {
2213
- };
2887
+ const lease = { lost: false, released: false, stop: () => {
2888
+ } };
2889
+ if (!heartbeatEnabled) return lease;
2214
2890
  let stopped = false;
2215
2891
  let timer = null;
2216
2892
  const beat = async () => {
2217
2893
  if (stopped) return;
2894
+ let renewed;
2218
2895
  try {
2219
- await db.renewTaskLease(task.id, new Date(Date.now() + heartbeatLeaseTtlMs).toISOString());
2896
+ renewed = await db.renewTaskLease(task.id, new Date(Date.now() + heartbeatLeaseTtlMs).toISOString());
2220
2897
  } catch (error) {
2221
2898
  console.warn("[amsg-server] \u79DF\u7EA6\u7EED\u79DF\u5931\u8D25\uFF08\u4E0B\u4E2A\u5FC3\u8DF3\u518D\u8BD5\uFF09:", error && error.message);
2222
2899
  }
2900
+ if (renewed === false) {
2901
+ if (stopped || lease.released) return;
2902
+ lease.lost = true;
2903
+ console.warn(`[amsg-server] \u4EFB\u52A1 ${task.id} \u7684\u79DF\u7EA6\u5DF2\u5931\u6548\uFF08\u884C\u88AB\u53D6\u6D88\u6216\u9876\u66FF\uFF09\uFF0C\u5269\u4F59\u63A8\u9001\u5C06\u4E2D\u6B62`);
2904
+ return;
2905
+ }
2223
2906
  if (!stopped) schedule();
2224
2907
  };
2225
2908
  const schedule = () => {
@@ -2227,22 +2910,42 @@ async function deliverTasks(ctx, tasks) {
2227
2910
  if (timer && typeof timer.unref === "function") timer.unref();
2228
2911
  };
2229
2912
  schedule();
2230
- return () => {
2913
+ lease.stop = () => {
2231
2914
  stopped = true;
2232
2915
  if (timer) clearTimeout(timer);
2233
2916
  };
2917
+ return lease;
2918
+ }
2919
+ const activeLeases = /* @__PURE__ */ new Map();
2920
+ function markLeaseReleased(taskId) {
2921
+ const lease = activeLeases.get(taskId);
2922
+ if (lease) lease.released = true;
2234
2923
  }
2235
- const supportsLastError = supportsClaim;
2236
2924
  async function updateTaskWithLastError(taskId, fields) {
2925
+ if (fields.lease_until === null) markLeaseReleased(taskId);
2926
+ if (!Object.prototype.hasOwnProperty.call(fields, "last_error")) {
2927
+ return db.updateTaskById(taskId, fields);
2928
+ }
2929
+ const { last_error: _lastError, ...stateFields } = fields;
2930
+ if (adaptersWithoutLastErrorColumn.has(db)) return db.updateTaskById(taskId, stateFields);
2931
+ let combinedError;
2237
2932
  try {
2238
- return await db.updateTaskById(taskId, fields);
2933
+ const result2 = await db.updateTaskById(taskId, fields);
2934
+ lastErrorColumnSuspicions.delete(db);
2935
+ return result2;
2239
2936
  } catch (error) {
2240
- if (Object.prototype.hasOwnProperty.call(fields, "last_error") && /last_error/i.test(error.message || "")) {
2241
- const { last_error: _omit, ...rest } = fields;
2242
- return db.updateTaskById(taskId, rest);
2243
- }
2244
- throw error;
2937
+ combinedError = error;
2938
+ }
2939
+ const result = await db.updateTaskById(taskId, stateFields);
2940
+ warnMissingLastErrorColumn(combinedError);
2941
+ const suspicions = (lastErrorColumnSuspicions.get(db) || 0) + 1;
2942
+ if (suspicions >= 2) {
2943
+ adaptersWithoutLastErrorColumn.add(db);
2944
+ lastErrorColumnSuspicions.delete(db);
2945
+ } else {
2946
+ lastErrorColumnSuspicions.set(db, suspicions);
2245
2947
  }
2948
+ return result;
2246
2949
  }
2247
2950
  function lastErrorJson(task, reason, extra) {
2248
2951
  return JSON.stringify({
@@ -2282,8 +2985,26 @@ async function deliverTasks(ctx, tasks) {
2282
2985
  groupsTakenThisTick.add(scopedKey);
2283
2986
  return { taken: false, rawKey };
2284
2987
  }
2988
+ function recordCancelled(task, status) {
2989
+ results.cancelledTasks.push({
2990
+ taskId: task.id,
2991
+ reason: "\u4EFB\u52A1\u5728\u6295\u9012\u671F\u95F4\u88AB\u53D6\u6D88\u6216\u9876\u66FF",
2992
+ status
2993
+ });
2994
+ console.warn(`[amsg-server] \u4EFB\u52A1 ${task.id} \u5728\u6295\u9012\u671F\u95F4\u88AB\u53D6\u6D88\u6216\u9876\u66FF\uFF08${status}\uFF09`);
2995
+ }
2996
+ async function payloadStillFresh(task) {
2997
+ if (typeof db.getTaskByUuid !== "function" || !task.uuid) return true;
2998
+ try {
2999
+ const current = await db.getTaskByUuid(task.uuid, task.user_id);
3000
+ return Boolean(current) && current.encrypted_payload === task.encrypted_payload;
3001
+ } catch (_readError) {
3002
+ return false;
3003
+ }
3004
+ }
2285
3005
  async function encryptPayloadWithLastError(task, decryptedPayload, userKey, reason, extra) {
2286
3006
  if (!decryptedPayload || !userKey) return null;
3007
+ if (!await payloadStillFresh(task)) return null;
2287
3008
  try {
2288
3009
  return await encryptForStorage(JSON.stringify({
2289
3010
  ...decryptedPayload,
@@ -2298,6 +3019,16 @@ async function deliverTasks(ctx, tasks) {
2298
3019
  return null;
2299
3020
  }
2300
3021
  }
3022
+ async function encryptPayloadWithoutLastError(task, decryptedPayload, userKey) {
3023
+ if (!decryptedPayload || !userKey) return null;
3024
+ if (!await payloadStillFresh(task)) return null;
3025
+ try {
3026
+ const { lastError: _cleared, ...rest } = decryptedPayload;
3027
+ return await encryptForStorage(JSON.stringify(rest), userKey);
3028
+ } catch (_encryptError) {
3029
+ return null;
3030
+ }
3031
+ }
2301
3032
  async function notifyStaleSkip(task, info) {
2302
3033
  if (typeof ctx.onStaleSkip !== "function") return;
2303
3034
  try {
@@ -2311,8 +3042,8 @@ async function deliverTasks(ctx, tasks) {
2311
3042
  const errorCode = failure.errorCode ?? null;
2312
3043
  const pushStatus = toPushStatus(failure.pushStatus);
2313
3044
  const tzId = decryptedPayload ? decryptedPayload.tzId ?? null : null;
2314
- const permanent = failure.permanent === true || errorCode === "PUSH_SUBSCRIPTION_MISSING" || errorCode === "PUSH_SUBSCRIPTION_STORE_UNSUPPORTED" || TERMINAL_PUSH_STATUSES.has(pushStatus);
2315
- const errorExtra = pushStatus === null ? void 0 : { pushStatus };
3045
+ const permanent = isPermanentDeliveryFailure({ permanent: failure.permanent, errorCode, pushStatus });
3046
+ const errorExtra = buildErrorExtra(errorCode, pushStatus);
2316
3047
  try {
2317
3048
  if (permanent || task.retry_count >= 3) {
2318
3049
  const encrypted = await encryptPayloadWithLastError(task, decryptedPayload, userKey, reason, errorExtra);
@@ -2322,14 +3053,14 @@ async function deliverTasks(ctx, tasks) {
2322
3053
  next_send_at: nextSendAt,
2323
3054
  retry_count: 0,
2324
3055
  ...encrypted ? { encrypted_payload: encrypted } : {},
2325
- ...supportsLastError ? { last_error: lastErrorJson(task, reason, errorExtra) } : {}
3056
+ last_error: lastErrorJson(task, reason, errorExtra)
2326
3057
  });
2327
3058
  results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "occurrence_skipped", nextSendAt, ...permanent ? { permanent: true } : {} });
2328
3059
  } else {
2329
3060
  await updateAndRelease(task.id, {
2330
3061
  status: "failed",
2331
3062
  ...encrypted ? { encrypted_payload: encrypted } : {},
2332
- ...supportsLastError ? { last_error: lastErrorJson(task, reason, errorExtra) } : {}
3063
+ last_error: lastErrorJson(task, reason, errorExtra)
2333
3064
  });
2334
3065
  results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "permanently_failed", ...permanent ? { permanent: true } : {} });
2335
3066
  }
@@ -2340,10 +3071,14 @@ async function deliverTasks(ctx, tasks) {
2340
3071
  retry_after: nextRetryTime.toISOString(),
2341
3072
  lease_until: null,
2342
3073
  retry_count: task.retry_count + 1,
2343
- ...supportsLastError ? { last_error: lastErrorJson(task, reason, errorExtra) } : {}
3074
+ last_error: lastErrorJson(task, reason, errorExtra)
2344
3075
  });
2345
3076
  } else {
2346
- await db.updateTaskById(task.id, { next_send_at: nextRetryTime.toISOString(), retry_count: task.retry_count + 1 });
3077
+ await updateTaskWithLastError(task.id, {
3078
+ next_send_at: nextRetryTime.toISOString(),
3079
+ retry_count: task.retry_count + 1,
3080
+ last_error: lastErrorJson(task, reason, errorExtra)
3081
+ });
2347
3082
  }
2348
3083
  results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count + 1, nextRetryAt: nextRetryTime.toISOString() });
2349
3084
  }
@@ -2401,14 +3136,16 @@ async function deliverTasks(ctx, tasks) {
2401
3136
  results.claimSkippedTasks++;
2402
3137
  return;
2403
3138
  }
2404
- const stopHeartbeat = startLeaseHeartbeat(task);
3139
+ const lease = startLeaseHeartbeat(task);
3140
+ activeLeases.set(task.id, lease);
2405
3141
  try {
2406
- await deliverClaimedTask(task, decrypted);
3142
+ await deliverClaimedTask(task, decrypted, lease);
2407
3143
  } finally {
2408
- stopHeartbeat();
3144
+ lease.stop();
3145
+ activeLeases.delete(task.id);
2409
3146
  }
2410
3147
  }
2411
- async function deliverClaimedTask(task, decrypted) {
3148
+ async function deliverClaimedTask(task, decrypted, lease) {
2412
3149
  const decryptedPayload = decrypted.ok ? decrypted.payload : null;
2413
3150
  const userKey = decrypted.ok ? decrypted.userKey : null;
2414
3151
  if (!decrypted.ok) {
@@ -2428,6 +3165,16 @@ async function deliverTasks(ctx, tasks) {
2428
3165
  userKey,
2429
3166
  maxStateValueBytes: ctx.maxStateValueBytes
2430
3167
  });
3168
+ const { emitResult } = createResultEmitter({
3169
+ db,
3170
+ task,
3171
+ userKey,
3172
+ decryptedPayload,
3173
+ messageIdBase: task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_stale_${task.uuid || ""}`,
3174
+ sessionId: task.id != null ? `sess_task_${task.id}${occurrenceSuffix(task)}` : `sess_stale_${task.uuid || ""}`,
3175
+ occurrenceMs,
3176
+ webpush: ctx.webpush
3177
+ });
2431
3178
  const recurring = isRecurringType(recurrenceType);
2432
3179
  const plan = recurring ? planNextOccurrence(occurrenceMs, recurrenceType, Date.now(), tzId) : null;
2433
3180
  const nextSendAt = recurring ? new Date(plan.nextMs).toISOString() : null;
@@ -2443,7 +3190,7 @@ async function deliverTasks(ctx, tasks) {
2443
3190
  await updateAndRelease(task.id, {
2444
3191
  ...recurring ? { next_send_at: nextSendAt, retry_count: 0 } : { status: "failed" },
2445
3192
  ...encrypted ? { encrypted_payload: encrypted } : {},
2446
- ...supportsLastError ? { last_error: lastErrorJson(task, "stale", recurring ? { skippedCount, nextSendAt } : void 0) } : {}
3193
+ last_error: lastErrorJson(task, "stale", recurring ? { skippedCount, nextSendAt } : void 0)
2447
3194
  });
2448
3195
  results.staleTasks.push({
2449
3196
  taskId: task.id,
@@ -2462,7 +3209,8 @@ async function deliverTasks(ctx, tasks) {
2462
3209
  skippedOccurrences: recurring ? plan.skippedOccurrences : [occurrenceMs],
2463
3210
  skippedTruncated: recurring ? plan.skippedTruncated : false,
2464
3211
  nextSendAt,
2465
- ...stateAccessors
3212
+ ...stateAccessors,
3213
+ emitResult
2466
3214
  });
2467
3215
  } catch (error) {
2468
3216
  results.failedCount++;
@@ -2474,22 +3222,30 @@ async function deliverTasks(ctx, tasks) {
2474
3222
  try {
2475
3223
  sendResult = await processSingleMessage(
2476
3224
  task,
2477
- { ...ctx, db, masterKey },
3225
+ { ...ctx, db, masterKey, webpush: guardWebpushWithLease(ctx.webpush, lease) },
2478
3226
  masterKey,
2479
3227
  { userKey, payload: decryptedPayload }
2480
3228
  );
2481
3229
  } catch (error) {
3230
+ if (lease.lost) {
3231
+ recordCancelled(task, "cancelled_mid_delivery");
3232
+ return;
3233
+ }
2482
3234
  await handleDeliveryFailure(
2483
3235
  task,
2484
3236
  error.message || "\u6D88\u606F\u53D1\u9001\u5931\u8D25",
2485
3237
  recurrenceType,
2486
3238
  decryptedPayload,
2487
3239
  userKey,
2488
- { errorCode: error.code || null, permanent: error.permanent === true, pushStatus: error.statusCode }
3240
+ { errorCode: error.code || null, permanent: error.permanent === true, pushStatus: null }
2489
3241
  );
2490
3242
  return;
2491
3243
  }
2492
3244
  if (!sendResult.success) {
3245
+ if (lease.lost) {
3246
+ recordCancelled(task, "cancelled_mid_delivery");
3247
+ return;
3248
+ }
2493
3249
  await handleDeliveryFailure(
2494
3250
  task,
2495
3251
  sendResult.error || "\u6D88\u606F\u53D1\u9001\u5931\u8D25",
@@ -2500,17 +3256,33 @@ async function deliverTasks(ctx, tasks) {
2500
3256
  );
2501
3257
  return;
2502
3258
  }
3259
+ if (sendResult.reasoningError) {
3260
+ results.reasoningSkippedTasks.push({ taskId: task.id, reason: sendResult.reasoningError });
3261
+ console.warn(
3262
+ `[amsg-server] \u4EFB\u52A1 ${task.id} \u7684\u6B63\u6587\u5DF2\u9001\u8FBE\uFF0C\u601D\u8003\u8FC7\u7A0B\u6CA1\u53D1\u51FA\u53BB: ${sendResult.reasoningError}`
3263
+ );
3264
+ }
2503
3265
  try {
2504
3266
  if (recurrenceType === "none") {
2505
- await db.deleteTaskById(task.id);
3267
+ markLeaseReleased(task.id);
3268
+ if (rowVanished(await db.deleteTaskById(task.id))) {
3269
+ recordCancelled(task, "cancelled_after_delivery");
3270
+ return;
3271
+ }
2506
3272
  results.deletedOnceOffTasks++;
2507
3273
  } else {
2508
3274
  const nextSendAt = nextFutureOccurrence(occurrenceMs, recurrenceType, Date.now(), tzId);
2509
- await updateAndRelease(task.id, {
3275
+ const clearedPayload = decryptedPayload && decryptedPayload.lastError ? await encryptPayloadWithoutLastError(task, decryptedPayload, userKey) : null;
3276
+ const updated = await updateAndRelease(task.id, {
2510
3277
  next_send_at: nextSendAt,
2511
3278
  retry_count: 0,
2512
- ...supportsLastError ? { last_error: null } : {}
3279
+ last_error: null,
3280
+ ...clearedPayload ? { encrypted_payload: clearedPayload } : {}
2513
3281
  });
3282
+ if (rowVanished(updated)) {
3283
+ recordCancelled(task, "cancelled_after_delivery");
3284
+ return;
3285
+ }
2514
3286
  results.updatedRecurringTasks++;
2515
3287
  }
2516
3288
  results.successCount++;
@@ -2558,12 +3330,20 @@ async function deliverTasks(ctx, tasks) {
2558
3330
  deletedOnceOffTasks: results.deletedOnceOffTasks,
2559
3331
  updatedRecurringTasks: results.updatedRecurringTasks,
2560
3332
  staleTasks: results.staleTasks,
3333
+ // 投递期间行被取消 / 顶替的任务。`cancelled_mid_delivery` = 推送在发出去
3334
+ // 之前被拦下;`cancelled_after_delivery` = 推送已经发完,收尾写库才发现
3335
+ // 行没了。两种都不计入 successCount / failedCount。
3336
+ cancelledTasks: results.cancelledTasks,
3337
+ // 正文送到了、只有思考过程没发出去的任务({ taskId, reason })。这些任务
3338
+ // 照常计入 successCount——列在这里只是让「这次没有思考过程」看得见。
3339
+ reasoningSkippedTasks: results.reasoningSkippedTasks,
2561
3340
  failedTasks: results.failedTasks
2562
3341
  }
2563
3342
  };
2564
3343
  }
2565
3344
 
2566
3345
  // src/server/handlers/update-message.js
3346
+ var REQUEST_FIELD_BY_COLUMN = { next_send_at: "nextSendAt" };
2567
3347
  function createUpdateMessageHandler(ctx) {
2568
3348
  async function PUT(url, headers, body) {
2569
3349
  const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
@@ -2610,6 +3390,15 @@ function createUpdateMessageHandler(ctx) {
2610
3390
  if (Object.prototype.hasOwnProperty.call(updates, "contactName") && (typeof updates.contactName !== "string" || !updates.contactName.trim())) {
2611
3391
  return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "contactName \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32", details: { invalidFields: ["contactName"] } } } };
2612
3392
  }
3393
+ if (Object.prototype.hasOwnProperty.call(updates, "userMessage") && updates.userMessage !== null && typeof updates.userMessage !== "string") {
3394
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "userMessage \u5FC5\u987B\u662F\u5B57\u7B26\u4E32", details: { invalidFields: ["userMessage"] } } } };
3395
+ }
3396
+ if (Object.prototype.hasOwnProperty.call(updates, "messageSubtype") && updates.messageSubtype !== null && typeof updates.messageSubtype !== "string") {
3397
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "messageSubtype \u5FC5\u987B\u662F\u5B57\u7B26\u4E32", details: { invalidFields: ["messageSubtype"] } } } };
3398
+ }
3399
+ if (Object.prototype.hasOwnProperty.call(updates, "llmExtraBody") && updates.llmExtraBody !== null && !isPlainObject(updates.llmExtraBody)) {
3400
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "llmExtraBody \u5FC5\u987B\u662F\u666E\u901A\u5BF9\u8C61", details: { invalidFields: ["llmExtraBody"] } } } };
3401
+ }
2613
3402
  if (updates.recurrenceType && !["none", "daily", "weekly"].includes(updates.recurrenceType)) {
2614
3403
  return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } } } };
2615
3404
  }
@@ -2685,8 +3474,7 @@ function createUpdateMessageHandler(ctx) {
2685
3474
  promptUpdates.messages = updates.messages;
2686
3475
  promptUpdates.completePrompt = null;
2687
3476
  }
2688
- const updatedData = {
2689
- ...existingData,
3477
+ const appliedPatch = {
2690
3478
  ...promptUpdates,
2691
3479
  ...Object.prototype.hasOwnProperty.call(updates, "contactName") && { contactName: updates.contactName },
2692
3480
  ...updates.userMessage && { userMessage: updates.userMessage },
@@ -2710,9 +3498,20 @@ function createUpdateMessageHandler(ctx) {
2710
3498
  // splitPattern: hasOwnProperty so that explicit `null` (= revert to
2711
3499
  // default) doesn't get swallowed by truthy-spread the way the optional
2712
3500
  // string fields above are.
2713
- ...Object.prototype.hasOwnProperty.call(updates, "splitPattern") && { splitPattern: updates.splitPattern ?? null }
3501
+ ...Object.prototype.hasOwnProperty.call(updates, "splitPattern") && { splitPattern: updates.splitPattern ?? null },
3502
+ // messageSubtype / llmExtraBody:显式传 null 表示改回默认(分别是投递时
3503
+ // 的 'chat' 和「不透传额外参数」),所以跟 tzId 那几个一样走
3504
+ // hasOwnProperty,不被 truthy spread 吞掉。
3505
+ ...Object.prototype.hasOwnProperty.call(updates, "messageSubtype") && { messageSubtype: updates.messageSubtype ?? null },
3506
+ ...Object.prototype.hasOwnProperty.call(updates, "llmExtraBody") && { llmExtraBody: updates.llmExtraBody ?? null }
2714
3507
  };
2715
- const encryptedPayload = await encryptForStorage(JSON.stringify(updatedData), userKey);
3508
+ const updatedData = { ...existingData, ...appliedPatch };
3509
+ const serializedUpdatedData = JSON.stringify(updatedData);
3510
+ const sizeError = validateTaskPayloadSize(serializedUpdatedData);
3511
+ if (sizeError && sizeError.details.bytes > taskPayloadByteLength(JSON.stringify(existingData))) {
3512
+ return { status: 400, body: { success: false, error: sizeError } };
3513
+ }
3514
+ const encryptedPayload = await encryptForStorage(serializedUpdatedData, userKey);
2716
3515
  const extraFields = {
2717
3516
  retry_count: 0,
2718
3517
  ...typeof db.claimTask === "function" ? { retry_after: null } : {},
@@ -2722,13 +3521,18 @@ function createUpdateMessageHandler(ctx) {
2722
3521
  if (!result) {
2723
3522
  return { status: 409, body: { success: false, error: { code: "UPDATE_CONFLICT", message: "\u4EFB\u52A1\u66F4\u65B0\u5931\u8D25\uFF0C\u4EFB\u52A1\u53EF\u80FD\u5DF2\u88AB\u4FEE\u6539\u6216\u5220\u9664" } } };
2724
3523
  }
3524
+ const applied = /* @__PURE__ */ new Set([
3525
+ ...Object.keys(appliedPatch),
3526
+ ...Object.keys(extraFields).map((column) => REQUEST_FIELD_BY_COLUMN[column])
3527
+ ]);
3528
+ const updatedFields = Object.keys(updates).filter((name) => applied.has(name));
2725
3529
  return {
2726
3530
  status: 200,
2727
3531
  body: {
2728
3532
  success: true,
2729
3533
  data: {
2730
3534
  uuid: taskUuid,
2731
- updatedFields: Object.keys(updates),
3535
+ updatedFields,
2732
3536
  updatedAt: result.updated_at
2733
3537
  }
2734
3538
  }
@@ -2760,6 +3564,7 @@ function createCancelMessageHandler(ctx) {
2760
3564
  body: { success: false, error: { code: "TASK_NOT_FOUND", message: "\u6307\u5B9A\u7684\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664" } }
2761
3565
  };
2762
3566
  }
3567
+ await discardUndeliveredPushesForTask({ db, userId, taskUuid });
2763
3568
  return {
2764
3569
  status: 200,
2765
3570
  body: {
@@ -2942,10 +3747,18 @@ function createPushSubscriptionHandler(ctx) {
2942
3747
  const { userId } = gate;
2943
3748
  if (!supportsPushSubscriptionStore(db)) return UNSUPPORTED;
2944
3749
  const userKey = await deriveUserEncryptionKey(userId, masterKey);
3750
+ let row;
3751
+ try {
3752
+ row = await db.getPushSubscription(userId);
3753
+ } catch (error) {
3754
+ console.error("[amsg-server] push-subscription \u67E5\u8BE2\u5931\u8D25:", error && error.message);
3755
+ return err(503, "PUSH_SUBSCRIPTION_LOOKUP_FAILED", "\u63A8\u9001\u8BA2\u9605\u8BFB\u53D6\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
3756
+ }
2945
3757
  let stored = null;
2946
3758
  try {
2947
- stored = await loadPushSubscription({ db, userId, userKey });
2948
- } catch (_error) {
3759
+ stored = await decodePushSubscriptionRow(row, userKey);
3760
+ } catch (error) {
3761
+ console.warn("[amsg-server] push-subscription \u884C\u89E3\u5BC6\u5931\u8D25\uFF08\u6309\u672A\u767B\u8BB0\u5904\u7406\uFF09:", error && error.message);
2949
3762
  stored = null;
2950
3763
  }
2951
3764
  return {
@@ -3516,7 +4329,7 @@ var D1Adapter = class {
3516
4329
  }
3517
4330
  async getTaskByUuid(uuid, userId) {
3518
4331
  return this._db.prepare(
3519
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, last_error, created_at, updated_at
4332
+ `SELECT ${TASK_DETAIL_COLUMNS}
3520
4333
  FROM scheduled_messages
3521
4334
  WHERE uuid = ? AND user_id = ? AND status = 'pending'
3522
4335
  LIMIT 1`
@@ -3524,7 +4337,7 @@ var D1Adapter = class {
3524
4337
  }
3525
4338
  async getTaskByUuidOnly(uuid) {
3526
4339
  return this._db.prepare(
3527
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, retry_after, status, retry_count
4340
+ `SELECT ${TASK_DELIVERY_COLUMNS}
3528
4341
  FROM scheduled_messages
3529
4342
  WHERE uuid = ? AND status = 'pending'
3530
4343
  LIMIT 1`
@@ -3597,7 +4410,7 @@ var D1Adapter = class {
3597
4410
  async getPendingTasks(limit = 50) {
3598
4411
  const now = this._now();
3599
4412
  const res = await this._db.prepare(
3600
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, retry_after, status, retry_count
4413
+ `SELECT ${TASK_DELIVERY_COLUMNS}
3601
4414
  FROM scheduled_messages
3602
4415
  WHERE status = 'pending' AND next_send_at <= ?
3603
4416
  AND (lease_until IS NULL OR lease_until <= ?)
@@ -3700,7 +4513,7 @@ var D1Adapter = class {
3700
4513
  ).bind(...params).first();
3701
4514
  const total = Number(countRow.count) || 0;
3702
4515
  const res = await this._db.prepare(
3703
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, last_error, created_at, updated_at
4516
+ `SELECT ${TASK_DETAIL_COLUMNS}
3704
4517
  FROM scheduled_messages
3705
4518
  WHERE ${where}
3706
4519
  ORDER BY next_send_at ASC
@@ -3816,6 +4629,34 @@ var D1Adapter = class {
3816
4629
  ).bind(userId, namespace).all();
3817
4630
  return res.results || [];
3818
4631
  }
4632
+ /**
4633
+ * 例行清理:把指定命名空间下太久没更新的条目删掉(run-tick 每跳顺手调,
4634
+ * 宿主配了 `clientStateTtl` 才会调)。
4635
+ *
4636
+ * 不限用户——「这个命名空间只留最近 N 天」是命名空间级的约定,单用户部署
4637
+ * 下也就是这一个用户的行。指令由 lib/client-state-store.js 的
4638
+ * `planClientStateCleanup` 算好(含大值切片所在的保留命名空间),这里只负
4639
+ * 责照着删。
4640
+ *
4641
+ * @param {Array<{ namespace: string, updatedBefore: number }>} targets
4642
+ * `updatedBefore` 是 epoch 毫秒,与 `updated_at` 列同一把尺子。
4643
+ * @returns {Promise<number>} 删掉的行数
4644
+ */
4645
+ async cleanupClientState(targets = []) {
4646
+ if (!Array.isArray(targets) || targets.length === 0) return 0;
4647
+ const SQL = "DELETE FROM client_state WHERE namespace = ? AND updated_at < ?";
4648
+ const statements = targets.map(
4649
+ (target) => this._db.prepare(SQL).bind(target.namespace, target.updatedBefore)
4650
+ );
4651
+ let results;
4652
+ if (typeof this._db.batch === "function") {
4653
+ results = await this._db.batch(statements);
4654
+ } else {
4655
+ results = [];
4656
+ for (const statement of statements) results.push(await statement.run());
4657
+ }
4658
+ return results.reduce((sum, res) => sum + (res.meta.changes || 0), 0);
4659
+ }
3819
4660
  /**
3820
4661
  * Wipe every entry of this user.
3821
4662
  *
@@ -4017,6 +4858,26 @@ var D1Adapter = class {
4017
4858
  messageIds
4018
4859
  );
4019
4860
  }
4861
+ /**
4862
+ * 把这一批还没发出去的行删掉(任务投递到一半被取消 / 顶替时用)。
4863
+ *
4864
+ * 只删 delivered_at 仍为 NULL 的行:已经推给设备的那几条撤不回来,行留着让
4865
+ * 客户端照常 ack。已 ack 的行更不动。
4866
+ *
4867
+ * @param {string} userId
4868
+ * @param {string[]} messageIds
4869
+ * @returns {Promise<number>} 删掉的行数
4870
+ */
4871
+ async discardOutboxMessages(userId, messageIds) {
4872
+ if (!messageIds || messageIds.length === 0) return 0;
4873
+ return this._runInClauseWrite(
4874
+ (placeholders) => `DELETE FROM message_outbox
4875
+ WHERE user_id = ? AND delivered_at IS NULL AND acked_at IS NULL
4876
+ AND message_id IN (${placeholders})`,
4877
+ [userId],
4878
+ messageIds
4879
+ );
4880
+ }
4020
4881
  /**
4021
4882
  * 未 ack 的行(id 升序,游标翻页)。payload 仍是密文,解密在 handler。
4022
4883
  *
@@ -4334,7 +5195,7 @@ function createClientStateHandler(ctx) {
4334
5195
  }
4335
5196
 
4336
5197
  // src/server/lib/version.js
4337
- var SERVER_VERSION = true ? "2.6.0-next.20" : "0.0.0-dev";
5198
+ var SERVER_VERSION = true ? "2.6.0-next.21" : "0.0.0-dev";
4338
5199
 
4339
5200
  // src/server/handlers/capabilities.js
4340
5201
  var SERVER_FEATURES = Object.freeze([
@@ -4414,7 +5275,13 @@ var SERVER_FEATURES = Object.freeze([
4414
5275
  // 用户级 LLM 凭据存储:PUT/GET/DELETE /llm-credentials,任务 payload 认
4415
5276
  // credRefs(fire 时按引用现读、自排任务复制引用),hook ctx 带
4416
5277
  // resolveLlmCredential。
4417
- "llm-credentials"
5278
+ "llm-credentials",
5279
+ // 请求体带 Content-Encoding: gzip 时自动解压,所有带 body 的端点都认。
5280
+ "gzip-request-body",
5281
+ // client_state 按命名空间过期清理(config 的 clientStateTtl,cron 每跳顺手做)。
5282
+ "client-state-ttl",
5283
+ // hook ctx 带 emitResult(payload):往客户端补一条自定义结果(落收件箱 + 推送)。
5284
+ "emit-result"
4418
5285
  ]);
4419
5286
  function createCapabilitiesHandler(ctx) {
4420
5287
  async function GET(url, headers) {
@@ -4558,6 +5425,9 @@ function createSingleUserServer(config) {
4558
5425
  privateKey: vapid.privateKey || ""
4559
5426
  },
4560
5427
  webpush: config.webpush || null,
5428
+ // 分片传输的限额(与 installReiSW 的 multipart 同一份)。instant 消息也走
5429
+ // 这个 ctx 发,所以它要跟着 handlers 一起进来。
5430
+ multipart: config.multipart || null,
4561
5431
  tenantManager,
4562
5432
  // Fire-time hooks (optional): the in-server instant path fires through
4563
5433
  // processMessagesByUuid with this ctx, so instant-type tasks can take
@@ -4674,6 +5544,21 @@ function corsHeadersFor(cors, requestOrigin) {
4674
5544
  function degradedCorsHeaders(requestOrigin) {
4675
5545
  return corsHeadersFor({ origin: requestOrigin, maxAge: 0 }, requestOrigin);
4676
5546
  }
5547
+ function isSameOriginCall(request, requestOrigin) {
5548
+ if (!requestOrigin) return true;
5549
+ try {
5550
+ return new URL(request.url).origin === requestOrigin;
5551
+ } catch (_error) {
5552
+ return false;
5553
+ }
5554
+ }
5555
+ function publicErrorCause(cause) {
5556
+ const { message: _serverSideOnly, ...safe } = cause;
5557
+ return (
5558
+ /** @type {import('../lib/errors.js').ErrorCause} */
5559
+ safe
5560
+ );
5561
+ }
4677
5562
  function internalErrorResponse(cors, cause) {
4678
5563
  return jsonResponse(500, {
4679
5564
  success: false,
@@ -4743,7 +5628,14 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
4743
5628
  // 租约心跳间隔(默认 30s;0 = 关掉心跳,退回一次性长租约)。
4744
5629
  leaseHeartbeatMs: cfg.leaseHeartbeatMs,
4745
5630
  // 补发新鲜度阈值(默认 60 分钟;见 lib/run-tick.js 的 STALE_AFTER_MS)。
4746
- staleAfterMs: cfg.staleAfterMs
5631
+ staleAfterMs: cfg.staleAfterMs,
5632
+ // client_state 的按命名空间过期清理 `{ 命名空间: 天数 }`。不配 = 一个都
5633
+ // 不清(见 lib/run-tick.js 的 cleanupExpiredClientState)。
5634
+ clientStateTtl: cfg.clientStateTtl,
5635
+ // 分片传输的限额 { maxChunkBytes, maxChunks, maxTotalBytes, ttlMs }:跟宿主
5636
+ // 传给 installReiSW 的那一份保持一致。装不下一条 push 的思考过程按它切片、
5637
+ // 按它排发送节奏(见 lib/message-processor.js)。
5638
+ multipart: cfg.multipart
4747
5639
  };
4748
5640
  }
4749
5641
  function pushConfigured(cfg) {
@@ -4762,7 +5654,10 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
4762
5654
  await reportError({ stage: "config", error, cause, path: pathOf(request) });
4763
5655
  const degraded = degradedCorsHeaders(requestOrigin);
4764
5656
  if (method === "OPTIONS" && degraded) return new Response(null, { status: 204, headers: degraded });
4765
- return internalErrorResponse(degraded, cause);
5657
+ return internalErrorResponse(
5658
+ degraded,
5659
+ isSameOriginCall(request, requestOrigin) ? cause : publicErrorCause(cause)
5660
+ );
4766
5661
  }
4767
5662
  const cors = corsHeadersFor(cfg.cors, requestOrigin);
4768
5663
  try {
@@ -4773,19 +5668,25 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
4773
5668
  const url = request.url;
4774
5669
  const pathname = new URL(url).pathname.replace(/\/+$/, "") || "/";
4775
5670
  const headers = headersToObject(request.headers);
5671
+ let body = "";
5672
+ if (method === "POST" || method === "PUT" || method === "DELETE") {
5673
+ const read = await readRequestBody(request, { maxBytes: cfg.maxRequestBodyBytes });
5674
+ if (!read.ok) return jsonResponse(read.error.status, read.error.body, cors);
5675
+ body = read.body;
5676
+ }
4776
5677
  let result;
4777
5678
  if (method === "POST" && pathname.endsWith("/init-tenant")) {
4778
- result = await server.handlers.init.POST(headers, await request.text());
5679
+ result = await server.handlers.init.POST(headers, body);
4779
5680
  } else if (method === "GET" && pathname.endsWith("/get-user-key")) {
4780
5681
  result = await server.handlers.getUserKey.GET(url, headers);
4781
5682
  } else if (method === "POST" && pathname.endsWith("/schedule-message")) {
4782
- result = await server.handlers.scheduleMessage.POST(headers, await request.text());
5683
+ result = await server.handlers.scheduleMessage.POST(headers, body);
4783
5684
  } else if (method === "GET" && pathname.endsWith("/messages")) {
4784
5685
  result = await server.handlers.messages.GET(url, headers);
4785
5686
  } else if (method === "GET" && pathname.endsWith("/message")) {
4786
5687
  result = await server.handlers.getMessage.GET(url, headers);
4787
5688
  } else if (method === "PUT" && pathname.endsWith("/update-message")) {
4788
- result = await server.handlers.updateMessage.PUT(url, headers, await request.text());
5689
+ result = await server.handlers.updateMessage.PUT(url, headers, body);
4789
5690
  } else if (method === "DELETE" && pathname.endsWith("/cancel-message")) {
4790
5691
  result = await server.handlers.cancelMessage.DELETE(url, headers);
4791
5692
  } else if (method === "GET" && pathname.endsWith("/vapid-public-key")) {
@@ -4793,7 +5694,7 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
4793
5694
  } else if (method === "GET" && pathname.endsWith("/capabilities")) {
4794
5695
  result = await server.handlers.capabilities.GET(url, headers);
4795
5696
  } else if (method === "PUT" && pathname.endsWith("/client-state")) {
4796
- result = await server.handlers.clientState.PUT(headers, await request.text());
5697
+ result = await server.handlers.clientState.PUT(headers, body);
4797
5698
  } else if (method === "GET" && pathname.endsWith("/client-state")) {
4798
5699
  result = await server.handlers.clientState.GET(url, headers);
4799
5700
  } else if (method === "DELETE" && pathname.endsWith("/client-state")) {
@@ -4801,19 +5702,19 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
4801
5702
  } else if (method === "GET" && pathname.endsWith("/outbox")) {
4802
5703
  result = await server.handlers.outbox.GET(url, headers);
4803
5704
  } else if (method === "POST" && pathname.endsWith("/outbox/ack")) {
4804
- result = await server.handlers.outbox.POST(headers, await request.text());
5705
+ result = await server.handlers.outbox.POST(headers, body);
4805
5706
  } else if (method === "PUT" && pathname.endsWith("/push-subscription")) {
4806
- result = await server.handlers.pushSubscription.PUT(headers, await request.text());
5707
+ result = await server.handlers.pushSubscription.PUT(headers, body);
4807
5708
  } else if (method === "GET" && pathname.endsWith("/push-subscription")) {
4808
5709
  result = await server.handlers.pushSubscription.GET(url, headers);
4809
5710
  } else if (method === "DELETE" && pathname.endsWith("/push-subscription")) {
4810
5711
  result = await server.handlers.pushSubscription.DELETE(url, headers);
4811
5712
  } else if (method === "PUT" && pathname.endsWith("/llm-credentials")) {
4812
- result = await server.handlers.llmCredentials.PUT(headers, await request.text());
5713
+ result = await server.handlers.llmCredentials.PUT(headers, body);
4813
5714
  } else if (method === "GET" && pathname.endsWith("/llm-credentials")) {
4814
5715
  result = await server.handlers.llmCredentials.GET(url, headers);
4815
5716
  } else if (method === "DELETE" && pathname.endsWith("/llm-credentials")) {
4816
- result = await server.handlers.llmCredentials.DELETE(url, headers, await request.text());
5717
+ result = await server.handlers.llmCredentials.DELETE(url, headers, body);
4817
5718
  } else {
4818
5719
  result = { status: 404, body: { success: false, error: { code: "NOT_FOUND", message: "Unknown route" } } };
4819
5720
  }
@@ -4870,62 +5771,6 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
4870
5771
  return { fetch, scheduled, runTask: runTask2, getSchemaVersion: getSchemaVersion2, ensureSchema: ensureSchema2 };
4871
5772
  }
4872
5773
 
4873
- // src/server/lib/webpush-webcrypto.js
4874
- import { sendWebPush as sharedSendWebPush } from "@rei-standard/amsg-shared";
4875
- import { buildVapidJwt, verifyVapidJwt, hmacSha256 as hmacSha2562 } from "@rei-standard/amsg-shared";
4876
- var WEB_PUSH_MAX_BODY_BYTES = 4096;
4877
- var WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = 16 + 4 + 1 + 65 + 1 + 16;
4878
- var MAX_PUSH_PAYLOAD_BYTES = WEB_PUSH_MAX_BODY_BYTES - WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES;
4879
- var PUSH_ENVELOPE_RESERVED_BYTES = 384;
4880
- var payloadEncoder = new TextEncoder();
4881
- function measurePushPayload(payload, options) {
4882
- const reserveEnvelope = !!(options && options.reserveEnvelope);
4883
- const bytes = payloadEncoder.encode(typeof payload === "string" ? payload : String(payload)).length;
4884
- const envelopeReservedBytes = reserveEnvelope ? PUSH_ENVELOPE_RESERVED_BYTES : 0;
4885
- const maxBytes = MAX_PUSH_PAYLOAD_BYTES - envelopeReservedBytes;
4886
- return {
4887
- bytes,
4888
- maxBytes,
4889
- remainingBytes: maxBytes - bytes,
4890
- withinLimit: bytes <= maxBytes,
4891
- envelopeReservedBytes
4892
- };
4893
- }
4894
- async function sendWebPush(args) {
4895
- const { payload } = args || {};
4896
- if (typeof payload === "string") {
4897
- const size = measurePushPayload(payload);
4898
- if (!size.withinLimit) {
4899
- const err5 = new Error(
4900
- `sendWebPush: payload is ${size.bytes} bytes, over the ${MAX_PUSH_PAYLOAD_BYTES}-byte limit (push services cap the encrypted body at ${WEB_PUSH_MAX_BODY_BYTES} bytes; aes128gcm adds ${WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES} bytes)`
4901
- );
4902
- err5.code = "PUSH_PAYLOAD_TOO_LARGE";
4903
- err5.bytes = size.bytes;
4904
- err5.maxBytes = MAX_PUSH_PAYLOAD_BYTES;
4905
- throw err5;
4906
- }
4907
- }
4908
- return sharedSendWebPush(args);
4909
- }
4910
- var SCHEDULED_DEFAULT_TTL = 2419200;
4911
- function createWebCryptoWebPush(vapid = {}, { ttl = SCHEDULED_DEFAULT_TTL } = {}) {
4912
- return {
4913
- async sendNotification(subscription, payload) {
4914
- return sendWebPush({
4915
- subscription,
4916
- payload,
4917
- ttl,
4918
- vapid: {
4919
- email: vapid.email,
4920
- publicKey: vapid.publicKey,
4921
- privateKey: vapid.privateKey
4922
- },
4923
- fetch: globalThis.fetch
4924
- });
4925
- }
4926
- };
4927
- }
4928
-
4929
5774
  export {
4930
5775
  isValidTimeZoneId,
4931
5776
  advanceOccurrence,
@@ -4957,18 +5802,28 @@ export {
4957
5802
  isValidUUIDv4,
4958
5803
  validateSplitPattern,
4959
5804
  validateLlmMessagesArray,
5805
+ MAX_TASK_PAYLOAD_BYTES,
4960
5806
  validateScheduleMessagePayload,
5807
+ DEFAULT_MAX_REQUEST_BODY_BYTES,
5808
+ readRequestBody,
4961
5809
  parseJsonBody,
4962
5810
  getHeader,
4963
5811
  createGetUserKeyHandler,
4964
- DEFAULT_MAX_TOOL_ITERATIONS,
4965
- DEFAULT_TOTAL_TIMEOUT_MS,
4966
- DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE,
4967
- MIN_SCHEDULE_LEAD_MS,
5812
+ WEB_PUSH_MAX_BODY_BYTES,
5813
+ WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
5814
+ MAX_PUSH_PAYLOAD_BYTES,
5815
+ PUSH_ENVELOPE_RESERVED_BYTES,
5816
+ measurePushPayload,
5817
+ createWebCryptoWebPush,
4968
5818
  NonRetryableError,
4969
5819
  isNonRetryableError,
5820
+ DeploymentConfigError,
4970
5821
  sanitizeErrorSummary,
4971
5822
  summarizeErrorCause,
5823
+ DEFAULT_MAX_TOOL_ITERATIONS,
5824
+ DEFAULT_TOTAL_TIMEOUT_MS,
5825
+ DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE,
5826
+ MIN_SCHEDULE_LEAD_MS,
4972
5827
  createScheduleMessageHandler,
4973
5828
  DEFAULT_CLAIM_LEASE_MS,
4974
5829
  DEFAULT_LEASE_HEARTBEAT_MS,
@@ -4988,11 +5843,5 @@ export {
4988
5843
  ensureSchema,
4989
5844
  CORS_ALLOW_HEADERS,
4990
5845
  CORS_ALLOW_METHODS,
4991
- createSingleUserCloudflareWorker,
4992
- WEB_PUSH_MAX_BODY_BYTES,
4993
- WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
4994
- MAX_PUSH_PAYLOAD_BYTES,
4995
- PUSH_ENVELOPE_RESERVED_BYTES,
4996
- measurePushPayload,
4997
- createWebCryptoWebPush
5846
+ createSingleUserCloudflareWorker
4998
5847
  };