@rei-standard/amsg-server 2.6.0-next.5 → 2.6.0-next.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/{chunk-RGECD4OH.cjs → chunk-DCKCAGFV.cjs} +10 -1
- package/dist/{chunk-5ENJGVZX.mjs → chunk-GOBCWCJP.mjs} +9 -0
- package/dist/{chunk-ES2HUXYZ.mjs → chunk-I7EUMYQ4.mjs} +283 -81
- package/dist/{chunk-IFI4EWDW.cjs → chunk-YQPWXPIN.cjs} +284 -82
- package/dist/{cloudflare-DM_Njij8.d.cts → cloudflare-BBPmRS2L.d.cts} +488 -93
- package/dist/{cloudflare-DM_Njij8.d.ts → cloudflare-BBPmRS2L.d.ts} +488 -93
- package/dist/cloudflare.cjs +10 -2
- package/dist/cloudflare.d.cts +1 -1
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.mjs +9 -1
- package/dist/index.cjs +21 -13
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +11 -3
- package/dist/{neon-DYvGnCzx.d.ts → neon-D8Z2Nn97.d.cts} +45 -1
- package/dist/{neon-CJl66EGy.d.cts → neon-PqNFJIz2.d.ts} +45 -1
- package/dist/{neon-CU5N3CSW.mjs → neon-QJ7KYHEX.mjs} +45 -1
- package/dist/{neon-BFUS25UX.cjs → neon-V4VOH55N.cjs} +49 -5
- package/dist/{pg-IIH3M4OM.cjs → pg-6X74HKMX.cjs} +48 -5
- package/dist/{pg-Du-pN_UT.d.ts → pg-C48FwCWW.d.cts} +44 -1
- package/dist/{pg-07S-u_H4.d.cts → pg-C5gtWUfU.d.ts} +44 -1
- package/dist/{pg-QO6NKTGL.mjs → pg-DFJSFOJQ.mjs} +44 -1
- package/dist/{schema-C8OnYk6j.d.cts → schema-BHuyH_6Z.d.cts} +15 -1
- package/dist/{schema-C8OnYk6j.d.ts → schema-BHuyH_6Z.d.ts} +15 -1
- package/package.json +1 -1
|
@@ -559,6 +559,79 @@ async function resolveClientStateEntries(rows, fetchChunkRows, decryptValue) {
|
|
|
559
559
|
return entries;
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
// src/server/lib/client-state-store.js
|
|
563
|
+
var utf82 = new TextEncoder();
|
|
564
|
+
var MAX_STATE_ENTRIES_PER_BATCH = 200;
|
|
565
|
+
var MAX_NAMESPACE_CHARS = 128;
|
|
566
|
+
var MAX_KEY_CHARS = 256;
|
|
567
|
+
function stateValueBytes(value) {
|
|
568
|
+
return utf82.encode(value).length;
|
|
569
|
+
}
|
|
570
|
+
async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
571
|
+
const physicalRows = [];
|
|
572
|
+
const cleanups = [];
|
|
573
|
+
const rootRowIndexes = [];
|
|
574
|
+
let deleted = 0;
|
|
575
|
+
for (const entry of entries) {
|
|
576
|
+
cleanups.push({
|
|
577
|
+
namespace: chunkNamespaceFor(entry.namespace),
|
|
578
|
+
keyPrefix: chunkKeyPrefixFor(entry.key),
|
|
579
|
+
updatedAt: entry.updatedAt
|
|
580
|
+
});
|
|
581
|
+
if (entry.value === null) {
|
|
582
|
+
cleanups.push({
|
|
583
|
+
namespace: entry.namespace,
|
|
584
|
+
key: entry.key,
|
|
585
|
+
updatedAt: entry.updatedAt
|
|
586
|
+
});
|
|
587
|
+
deleted++;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
rootRowIndexes.push(physicalRows.length);
|
|
591
|
+
if (stateValueBytes(entry.value) <= STATE_CHUNK_SLICE_BYTES) {
|
|
592
|
+
physicalRows.push({
|
|
593
|
+
namespace: entry.namespace,
|
|
594
|
+
key: entry.key,
|
|
595
|
+
value: await encryptForStorage(entry.value, userKey),
|
|
596
|
+
updatedAt: entry.updatedAt
|
|
597
|
+
});
|
|
598
|
+
} else {
|
|
599
|
+
const slices = splitStateValue(entry.value);
|
|
600
|
+
physicalRows.push({
|
|
601
|
+
namespace: entry.namespace,
|
|
602
|
+
key: entry.key,
|
|
603
|
+
value: buildChunkedRootValue(slices.length),
|
|
604
|
+
updatedAt: entry.updatedAt
|
|
605
|
+
});
|
|
606
|
+
const encryptedSlices = await Promise.all(slices.map((slice) => encryptForStorage(slice, userKey)));
|
|
607
|
+
for (let c = 0; c < encryptedSlices.length; c++) {
|
|
608
|
+
physicalRows.push({
|
|
609
|
+
namespace: chunkNamespaceFor(entry.namespace),
|
|
610
|
+
key: chunkKeyFor(entry.key, c),
|
|
611
|
+
value: encryptedSlices[c],
|
|
612
|
+
updatedAt: entry.updatedAt
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (physicalRows.length === 0 && cleanups.length === 0) {
|
|
618
|
+
return { upserted: 0, skipped: 0, deleted: 0 };
|
|
619
|
+
}
|
|
620
|
+
const result = await db.upsertClientState(userId, physicalRows, cleanups);
|
|
621
|
+
let upserted = 0;
|
|
622
|
+
let skipped = 0;
|
|
623
|
+
if (Array.isArray(result.outcomes) && result.outcomes.length === physicalRows.length) {
|
|
624
|
+
for (const rootIndex of rootRowIndexes) {
|
|
625
|
+
if (result.outcomes[rootIndex]) upserted++;
|
|
626
|
+
else skipped++;
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
upserted = result.upserted;
|
|
630
|
+
skipped = result.skipped;
|
|
631
|
+
}
|
|
632
|
+
return { upserted, skipped, deleted };
|
|
633
|
+
}
|
|
634
|
+
|
|
562
635
|
// src/server/lib/agentic-fire.js
|
|
563
636
|
var DEFAULT_MAX_TOOL_ITERATIONS = 5;
|
|
564
637
|
var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
|
|
@@ -632,11 +705,64 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
632
705
|
(value) => decryptFromStorage(value, userKey)
|
|
633
706
|
);
|
|
634
707
|
};
|
|
708
|
+
const maxStateValueBytes = Number.isInteger(ctx.maxStateValueBytes) && ctx.maxStateValueBytes > 0 ? ctx.maxStateValueBytes : DEFAULT_MAX_STATE_VALUE_BYTES;
|
|
709
|
+
const writeState = async (namespace, entries) => {
|
|
710
|
+
if (typeof namespace !== "string" || !namespace.trim()) {
|
|
711
|
+
throw new TypeError("writeState(namespace, entries) requires a non-empty string namespace");
|
|
712
|
+
}
|
|
713
|
+
if (namespace.length > MAX_NAMESPACE_CHARS || INTERNAL_STATE_CHAR_RE.test(namespace)) {
|
|
714
|
+
throw new TypeError(
|
|
715
|
+
`writeState: namespace \u5FC5\u987B\u662F 1-${MAX_NAMESPACE_CHARS} \u5B57\u7B26\u4E14\u4E0D\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
if (!Array.isArray(entries)) {
|
|
719
|
+
throw new TypeError("writeState(namespace, entries) requires an array of { key, value }");
|
|
720
|
+
}
|
|
721
|
+
if (entries.length === 0) return { upserted: 0, skipped: 0, deleted: 0 };
|
|
722
|
+
if (entries.length > MAX_STATE_ENTRIES_PER_BATCH) {
|
|
723
|
+
throw new RangeError(`writeState: \u5355\u6B21\u6700\u591A ${MAX_STATE_ENTRIES_PER_BATCH} \u6761\uFF0C\u6536\u5230 ${entries.length} \u6761`);
|
|
724
|
+
}
|
|
725
|
+
if (!ctx.db || typeof ctx.db.upsertClientState !== "function") {
|
|
726
|
+
throw new Error("AGENTIC_STATE_WRITE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state \u5199\u5165");
|
|
727
|
+
}
|
|
728
|
+
const now = nowFn();
|
|
729
|
+
const normalized2 = entries.map((entry, index) => {
|
|
730
|
+
if (!entry || typeof entry !== "object") {
|
|
731
|
+
throw new TypeError(`writeState: entries[${index}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
|
|
732
|
+
}
|
|
733
|
+
if (typeof entry.key !== "string" || !entry.key.trim() || entry.key.length > MAX_KEY_CHARS) {
|
|
734
|
+
throw new TypeError(`writeState: entries[${index}].key \u5FC5\u987B\u662F 1-${MAX_KEY_CHARS} \u5B57\u7B26\u7684\u5B57\u7B26\u4E32`);
|
|
735
|
+
}
|
|
736
|
+
if (INTERNAL_STATE_CHAR_RE.test(entry.key)) {
|
|
737
|
+
throw new TypeError(`writeState: entries[${index}].key \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09`);
|
|
738
|
+
}
|
|
739
|
+
if (entry.value !== null && typeof entry.value !== "string") {
|
|
740
|
+
throw new TypeError(`writeState: entries[${index}].value \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u5BBF\u4E3B\u81EA\u884C\u5E8F\u5217\u5316\uFF09\uFF0C\u6216 null \u8868\u793A\u5220\u9664`);
|
|
741
|
+
}
|
|
742
|
+
if (typeof entry.value === "string") {
|
|
743
|
+
const bytes = stateValueBytes(entry.value);
|
|
744
|
+
if (bytes > maxStateValueBytes) {
|
|
745
|
+
throw new RangeError(`writeState: entries[${index}].value \u4E3A ${bytes} \u5B57\u8282\uFF0C\u8D85\u8FC7\u5355\u6761\u4E0A\u9650 ${maxStateValueBytes} \u5B57\u8282`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (entry.updatedAt !== void 0 && (!Number.isInteger(entry.updatedAt) || entry.updatedAt <= 0)) {
|
|
749
|
+
throw new TypeError(`writeState: entries[${index}].updatedAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF08epoch \u6BEB\u79D2\uFF09`);
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
namespace,
|
|
753
|
+
key: entry.key,
|
|
754
|
+
value: entry.value,
|
|
755
|
+
updatedAt: entry.updatedAt ?? now
|
|
756
|
+
};
|
|
757
|
+
});
|
|
758
|
+
return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized2 });
|
|
759
|
+
};
|
|
635
760
|
const scratch = {};
|
|
636
761
|
const fireCtx = Object.freeze({
|
|
637
762
|
task: buildHookTask(task, decryptedPayload),
|
|
638
763
|
userId: task.user_id,
|
|
639
764
|
readState,
|
|
765
|
+
writeState,
|
|
640
766
|
now: new Date(nowFn()),
|
|
641
767
|
scratch
|
|
642
768
|
});
|
|
@@ -668,16 +794,20 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
668
794
|
);
|
|
669
795
|
const assistantMessage = extractAssistantMessage(llmResponse);
|
|
670
796
|
messages = [...messages, assistantMessage];
|
|
671
|
-
const sessionCtx =
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
797
|
+
const sessionCtx = Object.freeze({
|
|
798
|
+
...buildSessionContext({
|
|
799
|
+
sessionId,
|
|
800
|
+
messages,
|
|
801
|
+
llmResponse,
|
|
802
|
+
iteration,
|
|
803
|
+
contactName: decryptedPayload.contactName,
|
|
804
|
+
avatarUrl: decryptedPayload.avatarUrl || void 0,
|
|
805
|
+
charId: decryptedPayload.charId,
|
|
806
|
+
metadata: decryptedPayload.metadata,
|
|
807
|
+
scratch
|
|
808
|
+
}),
|
|
809
|
+
readState,
|
|
810
|
+
writeState
|
|
681
811
|
});
|
|
682
812
|
const decision = await hooks.onLLMOutput(sessionCtx);
|
|
683
813
|
assertValidDecision(decision, { inlineToolCalls: true });
|
|
@@ -1091,9 +1221,18 @@ function createScheduleMessageHandler(ctx) {
|
|
|
1091
1221
|
}
|
|
1092
1222
|
|
|
1093
1223
|
// src/server/lib/run-tick.js
|
|
1224
|
+
var DEFAULT_CLAIM_LEASE_MS = 10 * 60 * 1e3;
|
|
1225
|
+
var CLAIM_LEASE_MARGIN_MS = 2 * 60 * 1e3;
|
|
1226
|
+
function positiveNumber(value) {
|
|
1227
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
1228
|
+
}
|
|
1229
|
+
function resolveClaimLeaseMs(ctx) {
|
|
1230
|
+
return positiveNumber(ctx.claimLeaseMs) || Math.max(DEFAULT_CLAIM_LEASE_MS, positiveNumber(ctx.totalTimeoutMs) + CLAIM_LEASE_MARGIN_MS);
|
|
1231
|
+
}
|
|
1094
1232
|
async function runScheduledTick(ctx) {
|
|
1095
1233
|
const db = ctx.db;
|
|
1096
1234
|
const masterKey = ctx.masterKey;
|
|
1235
|
+
const claimLeaseMs = resolveClaimLeaseMs(ctx);
|
|
1097
1236
|
const startTime = Date.now();
|
|
1098
1237
|
const tasks = await db.getPendingTasks(50);
|
|
1099
1238
|
const MAX_CONCURRENT = 8;
|
|
@@ -1101,19 +1240,29 @@ async function runScheduledTick(ctx) {
|
|
|
1101
1240
|
totalTasks: tasks.length,
|
|
1102
1241
|
successCount: 0,
|
|
1103
1242
|
failedCount: 0,
|
|
1243
|
+
claimSkippedTasks: 0,
|
|
1104
1244
|
deletedOnceOffTasks: 0,
|
|
1105
1245
|
updatedRecurringTasks: 0,
|
|
1106
1246
|
failedTasks: []
|
|
1107
1247
|
};
|
|
1248
|
+
const supportsClaim = typeof db.claimTask === "function";
|
|
1249
|
+
async function claimForThisTick(task) {
|
|
1250
|
+
if (!supportsClaim) return true;
|
|
1251
|
+
const leaseUntil = new Date(Date.now() + claimLeaseMs).toISOString();
|
|
1252
|
+
return !!await db.claimTask(task.id, task.next_send_at, leaseUntil);
|
|
1253
|
+
}
|
|
1254
|
+
async function updateAndRelease(taskId, fields) {
|
|
1255
|
+
return db.updateTaskById(taskId, supportsClaim ? { ...fields, lease_until: null } : fields);
|
|
1256
|
+
}
|
|
1108
1257
|
async function handleDeliveryFailure(task, reason) {
|
|
1109
1258
|
results.failedCount++;
|
|
1110
1259
|
try {
|
|
1111
1260
|
if (task.retry_count >= 3) {
|
|
1112
|
-
await
|
|
1261
|
+
await updateAndRelease(task.id, { status: "failed" });
|
|
1113
1262
|
results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "permanently_failed" });
|
|
1114
1263
|
} else {
|
|
1115
1264
|
const nextRetryTime = new Date(Date.now() + (task.retry_count + 1) * 2 * 60 * 1e3);
|
|
1116
|
-
await
|
|
1265
|
+
await updateAndRelease(task.id, { next_send_at: nextRetryTime.toISOString(), retry_count: task.retry_count + 1 });
|
|
1117
1266
|
results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count + 1, nextRetryAt: nextRetryTime.toISOString() });
|
|
1118
1267
|
}
|
|
1119
1268
|
} catch (updateError) {
|
|
@@ -1124,7 +1273,7 @@ async function runScheduledTick(ctx) {
|
|
|
1124
1273
|
results.failedCount++;
|
|
1125
1274
|
let markedSent = false;
|
|
1126
1275
|
try {
|
|
1127
|
-
await
|
|
1276
|
+
await updateAndRelease(task.id, { status: "sent", retry_count: 0 });
|
|
1128
1277
|
markedSent = true;
|
|
1129
1278
|
} catch (_markSentError) {
|
|
1130
1279
|
markedSent = false;
|
|
@@ -1137,6 +1286,18 @@ async function runScheduledTick(ctx) {
|
|
|
1137
1286
|
});
|
|
1138
1287
|
}
|
|
1139
1288
|
async function processTask(task) {
|
|
1289
|
+
let claimed;
|
|
1290
|
+
try {
|
|
1291
|
+
claimed = await claimForThisTick(task);
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
results.failedCount++;
|
|
1294
|
+
results.failedTasks.push({ taskId: task.id, reason: error.message || "\u4EFB\u52A1\u5360\u4F4D\u5931\u8D25", status: "claim_failed" });
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (!claimed) {
|
|
1298
|
+
results.claimSkippedTasks++;
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1140
1301
|
let sendResult;
|
|
1141
1302
|
try {
|
|
1142
1303
|
sendResult = await processSingleMessage(task, { ...ctx, db, masterKey }, masterKey);
|
|
@@ -1162,7 +1323,7 @@ async function runScheduledTick(ctx) {
|
|
|
1162
1323
|
} else if (decryptedPayload.recurrenceType === "weekly") {
|
|
1163
1324
|
nextSendAt = new Date(currentSendAt.getTime() + 7 * 24 * 60 * 60 * 1e3);
|
|
1164
1325
|
}
|
|
1165
|
-
await
|
|
1326
|
+
await updateAndRelease(task.id, { next_send_at: nextSendAt.toISOString(), retry_count: 0 });
|
|
1166
1327
|
results.updatedRecurringTasks++;
|
|
1167
1328
|
}
|
|
1168
1329
|
results.successCount++;
|
|
@@ -1195,6 +1356,7 @@ async function runScheduledTick(ctx) {
|
|
|
1195
1356
|
processedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1196
1357
|
executionTime,
|
|
1197
1358
|
details: {
|
|
1359
|
+
claimSkippedTasks: results.claimSkippedTasks,
|
|
1198
1360
|
deletedOnceOffTasks: results.deletedOnceOffTasks,
|
|
1199
1361
|
updatedRecurringTasks: results.updatedRecurringTasks,
|
|
1200
1362
|
failedTasks: results.failedTasks
|
|
@@ -1456,12 +1618,20 @@ var SQLITE_TABLE_SQL = `
|
|
|
1456
1618
|
encrypted_payload TEXT NOT NULL,
|
|
1457
1619
|
message_type TEXT NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),
|
|
1458
1620
|
next_send_at TEXT NOT NULL,
|
|
1621
|
+
lease_until TEXT,
|
|
1459
1622
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
|
|
1460
1623
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
1461
1624
|
created_at TEXT NOT NULL,
|
|
1462
1625
|
updated_at TEXT NOT NULL
|
|
1463
1626
|
)
|
|
1464
1627
|
`;
|
|
1628
|
+
var SQLITE_MIGRATIONS = [
|
|
1629
|
+
{
|
|
1630
|
+
name: "add_lease_until",
|
|
1631
|
+
sql: "ALTER TABLE scheduled_messages ADD COLUMN lease_until TEXT",
|
|
1632
|
+
description: "Task claim lease (2.6.0)"
|
|
1633
|
+
}
|
|
1634
|
+
];
|
|
1465
1635
|
var SQLITE_INDEXES = [
|
|
1466
1636
|
{
|
|
1467
1637
|
name: "idx_pending_tasks_optimized",
|
|
@@ -1521,6 +1691,7 @@ var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
|
|
|
1521
1691
|
"encrypted_payload",
|
|
1522
1692
|
"message_type",
|
|
1523
1693
|
"next_send_at",
|
|
1694
|
+
"lease_until",
|
|
1524
1695
|
"status",
|
|
1525
1696
|
"retry_count",
|
|
1526
1697
|
"created_at",
|
|
@@ -1549,6 +1720,13 @@ var D1Adapter = class {
|
|
|
1549
1720
|
async initSchema() {
|
|
1550
1721
|
await this._db.prepare(SQLITE_TABLE_SQL).run();
|
|
1551
1722
|
await this._db.prepare(CLIENT_STATE_TABLE_SQL).run();
|
|
1723
|
+
for (const migration of SQLITE_MIGRATIONS) {
|
|
1724
|
+
try {
|
|
1725
|
+
await this._db.prepare(migration.sql).run();
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
if (!/duplicate column name/i.test(error.message || "")) throw error;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1552
1730
|
const indexResults = [];
|
|
1553
1731
|
for (const index of SQLITE_INDEXES) {
|
|
1554
1732
|
try {
|
|
@@ -1566,7 +1744,7 @@ var D1Adapter = class {
|
|
|
1566
1744
|
);
|
|
1567
1745
|
}
|
|
1568
1746
|
return {
|
|
1569
|
-
columnsCreated:
|
|
1747
|
+
columnsCreated: 11,
|
|
1570
1748
|
indexesCreated: indexResults.filter((r) => r.status === "success").length,
|
|
1571
1749
|
indexesFailed: indexResults.filter((r) => r.status === "failed").length,
|
|
1572
1750
|
columns: [],
|
|
@@ -1658,15 +1836,53 @@ var D1Adapter = class {
|
|
|
1658
1836
|
return res.meta.changes > 0;
|
|
1659
1837
|
}
|
|
1660
1838
|
async getPendingTasks(limit = 50) {
|
|
1839
|
+
const now = this._now();
|
|
1661
1840
|
const res = await this._db.prepare(
|
|
1662
1841
|
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
1663
1842
|
FROM scheduled_messages
|
|
1664
1843
|
WHERE status = 'pending' AND next_send_at <= ?
|
|
1844
|
+
AND (lease_until IS NULL OR lease_until <= ?)
|
|
1665
1845
|
ORDER BY next_send_at ASC
|
|
1666
1846
|
LIMIT ?`
|
|
1667
|
-
).bind(
|
|
1847
|
+
).bind(now, now, limit).all();
|
|
1668
1848
|
return res.results || [];
|
|
1669
1849
|
}
|
|
1850
|
+
/**
|
|
1851
|
+
* 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
|
|
1852
|
+
* 本次投递期间别的 tick 领不走它。
|
|
1853
|
+
*
|
|
1854
|
+
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
1855
|
+
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
1856
|
+
*
|
|
1857
|
+
* 两个 tick 抢同一行时只有一个能改到行,另一个拿到 changes = 0,据此跳过。
|
|
1858
|
+
* WHERE 里的两个条件各管一件事:
|
|
1859
|
+
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
1860
|
+
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
1861
|
+
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
1862
|
+
* 这一跳就不该再按旧时刻发。
|
|
1863
|
+
*
|
|
1864
|
+
* 不加一个 'sending' 状态来表达「正在跑」:建表语句里 status 有
|
|
1865
|
+
* CHECK (status IN ('pending','sent','failed')),加值要重建表。
|
|
1866
|
+
*
|
|
1867
|
+
* expectedNextSendAt 按读到的原样比对,不做时区归一化——老部署里可能还留
|
|
1868
|
+
* 着非归一化写法的行(如 +08:00 结尾),归一化后反而对不上,那条任务会永
|
|
1869
|
+
* 远领不到。
|
|
1870
|
+
*
|
|
1871
|
+
* @param {number} taskId
|
|
1872
|
+
* @param {string} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
1873
|
+
* @param {string|Date} leaseUntil - 租期末尾
|
|
1874
|
+
* @returns {Promise<boolean>} true = 领到了;false = 别人正拿着租约、排期被改过、或行已不是 pending
|
|
1875
|
+
*/
|
|
1876
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
1877
|
+
const expected = typeof expectedNextSendAt === "string" ? expectedNextSendAt : this._iso(expectedNextSendAt);
|
|
1878
|
+
const res = await this._db.prepare(
|
|
1879
|
+
`UPDATE scheduled_messages
|
|
1880
|
+
SET lease_until = ?, updated_at = ?
|
|
1881
|
+
WHERE id = ? AND status = 'pending' AND next_send_at = ?
|
|
1882
|
+
AND (lease_until IS NULL OR lease_until <= ?)`
|
|
1883
|
+
).bind(this._iso(leaseUntil), this._now(), taskId, expected, this._now()).run();
|
|
1884
|
+
return (res.meta.changes || 0) > 0;
|
|
1885
|
+
}
|
|
1670
1886
|
async listTasks(userId, opts = {}) {
|
|
1671
1887
|
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
1672
1888
|
const conditions = ["user_id = ?"];
|
|
@@ -1710,9 +1926,15 @@ var D1Adapter = class {
|
|
|
1710
1926
|
* than the stored row (updatedAt strictly lower) is skipped; equal or
|
|
1711
1927
|
* newer overwrites. Values arrive pre-encrypted (the handler encrypts).
|
|
1712
1928
|
*
|
|
1713
|
-
* `cleanups`
|
|
1714
|
-
*
|
|
1715
|
-
* `
|
|
1929
|
+
* `cleanups` 是删除项:在同一 batch 里先于 upsert 执行,`updated_at <= ?`
|
|
1930
|
+
* 条件保证陈旧批次删不动更新写入的行。两种形态——
|
|
1931
|
+
* - `{ namespace, keyPrefix, updatedAt }` 删 key 前缀下的所有行,用来清掉
|
|
1932
|
+
* 大值旧写入留下的切片行(见 lib/state-chunks.js);
|
|
1933
|
+
* - `{ namespace, key, updatedAt }` 删这一个 key,用来删整条状态(前缀会
|
|
1934
|
+
* 连带删掉同前缀的兄弟 key,删单条必须走精确匹配)。
|
|
1935
|
+
*
|
|
1936
|
+
* 删掉一整条状态就是这两种各来一条(切片行走前缀、根行走精确 key),
|
|
1937
|
+
* `entries` 传空数组即可——见 lib/client-state-store.js。
|
|
1716
1938
|
*
|
|
1717
1939
|
* Uses D1's batch() — one network round trip for the whole set (implicit
|
|
1718
1940
|
* transaction). The client calls this endpoint inside its few-seconds
|
|
@@ -1722,7 +1944,7 @@ var D1Adapter = class {
|
|
|
1722
1944
|
*
|
|
1723
1945
|
* @param {string} userId
|
|
1724
1946
|
* @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
|
|
1725
|
-
* @param {Array<{ namespace: string, keyPrefix
|
|
1947
|
+
* @param {Array<{ namespace: string, key?: string, keyPrefix?: string, updatedAt: number }>} [cleanups]
|
|
1726
1948
|
* @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[] }>}
|
|
1727
1949
|
* `outcomes[i]` 对应 entries[i] 是否真的写入(changes > 0)。
|
|
1728
1950
|
*/
|
|
@@ -1733,12 +1955,12 @@ var D1Adapter = class {
|
|
|
1733
1955
|
value = excluded.value,
|
|
1734
1956
|
updated_at = excluded.updated_at
|
|
1735
1957
|
WHERE excluded.updated_at >= client_state.updated_at`;
|
|
1736
|
-
const
|
|
1958
|
+
const CLEANUP_PREFIX_SQL = `DELETE FROM client_state
|
|
1737
1959
|
WHERE user_id = ? AND namespace = ? AND key LIKE ? ESCAPE '\\' AND updated_at <= ?`;
|
|
1960
|
+
const CLEANUP_KEY_SQL = `DELETE FROM client_state
|
|
1961
|
+
WHERE user_id = ? AND namespace = ? AND key = ? AND updated_at <= ?`;
|
|
1738
1962
|
const buildStatements = () => [
|
|
1739
|
-
...cleanups.map(
|
|
1740
|
-
(c) => this._db.prepare(CLEANUP_SQL).bind(userId, c.namespace, `${escapeLikePrefix(c.keyPrefix)}%`, c.updatedAt)
|
|
1741
|
-
),
|
|
1963
|
+
...cleanups.map((c) => typeof c.key === "string" ? this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt) : this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace, `${escapeLikePrefix(c.keyPrefix)}%`, c.updatedAt)),
|
|
1742
1964
|
...entries.map(
|
|
1743
1965
|
(entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt)
|
|
1744
1966
|
)
|
|
@@ -1901,10 +2123,7 @@ function createVapidPublicKeyHandler(ctx) {
|
|
|
1901
2123
|
}
|
|
1902
2124
|
|
|
1903
2125
|
// src/server/handlers/client-state.js
|
|
1904
|
-
var MAX_STATE_ENTRIES_PER_REQUEST =
|
|
1905
|
-
var MAX_NAMESPACE_CHARS = 128;
|
|
1906
|
-
var MAX_KEY_CHARS = 256;
|
|
1907
|
-
var utf82 = new TextEncoder();
|
|
2126
|
+
var MAX_STATE_ENTRIES_PER_REQUEST = MAX_STATE_ENTRIES_PER_BATCH;
|
|
1908
2127
|
function err(status, code, message, details) {
|
|
1909
2128
|
const error = details === void 0 ? { code, message } : { code, message, details };
|
|
1910
2129
|
return { status, body: { success: false, error } };
|
|
@@ -1942,7 +2161,7 @@ function validateEntry(entry, index, maxValueBytes) {
|
|
|
1942
2161
|
if (typeof entry.value !== "string") {
|
|
1943
2162
|
return rejectEntry(entry, index, "INVALID_STATE_VALUE", `entries[${index}].value \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u5BBF\u4E3B\u81EA\u884C\u5E8F\u5217\u5316\uFF09`);
|
|
1944
2163
|
}
|
|
1945
|
-
const bytes =
|
|
2164
|
+
const bytes = stateValueBytes(entry.value);
|
|
1946
2165
|
if (bytes > maxValueBytes) {
|
|
1947
2166
|
return rejectEntry(entry, index, "STATE_VALUE_TOO_LARGE", `entries[${index}].value \u8D85\u8FC7\u5355\u6761\u603B\u4E0A\u9650`, { bytes, maxBytes: maxValueBytes });
|
|
1948
2167
|
}
|
|
@@ -1996,56 +2215,7 @@ function createClientStateHandler(ctx) {
|
|
|
1996
2215
|
if (typeof db.upsertClientState !== "function") {
|
|
1997
2216
|
return err(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
|
|
1998
2217
|
}
|
|
1999
|
-
const
|
|
2000
|
-
const cleanups = [];
|
|
2001
|
-
const rootRowIndexes = [];
|
|
2002
|
-
for (const entry of accepted) {
|
|
2003
|
-
cleanups.push({
|
|
2004
|
-
namespace: chunkNamespaceFor(entry.namespace),
|
|
2005
|
-
keyPrefix: chunkKeyPrefixFor(entry.key),
|
|
2006
|
-
updatedAt: entry.updatedAt
|
|
2007
|
-
});
|
|
2008
|
-
rootRowIndexes.push(physicalRows.length);
|
|
2009
|
-
if (utf82.encode(entry.value).length <= STATE_CHUNK_SLICE_BYTES) {
|
|
2010
|
-
physicalRows.push({
|
|
2011
|
-
namespace: entry.namespace,
|
|
2012
|
-
key: entry.key,
|
|
2013
|
-
value: await encryptForStorage(entry.value, userKey),
|
|
2014
|
-
updatedAt: entry.updatedAt
|
|
2015
|
-
});
|
|
2016
|
-
} else {
|
|
2017
|
-
const slices = splitStateValue(entry.value);
|
|
2018
|
-
physicalRows.push({
|
|
2019
|
-
namespace: entry.namespace,
|
|
2020
|
-
key: entry.key,
|
|
2021
|
-
value: buildChunkedRootValue(slices.length),
|
|
2022
|
-
updatedAt: entry.updatedAt
|
|
2023
|
-
});
|
|
2024
|
-
const encryptedSlices = await Promise.all(slices.map((slice) => encryptForStorage(slice, userKey)));
|
|
2025
|
-
for (let c = 0; c < encryptedSlices.length; c++) {
|
|
2026
|
-
physicalRows.push({
|
|
2027
|
-
namespace: chunkNamespaceFor(entry.namespace),
|
|
2028
|
-
key: chunkKeyFor(entry.key, c),
|
|
2029
|
-
value: encryptedSlices[c],
|
|
2030
|
-
updatedAt: entry.updatedAt
|
|
2031
|
-
});
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
let upserted = 0;
|
|
2036
|
-
let skipped = 0;
|
|
2037
|
-
if (physicalRows.length > 0) {
|
|
2038
|
-
const result = await db.upsertClientState(userId, physicalRows, cleanups);
|
|
2039
|
-
if (Array.isArray(result.outcomes) && result.outcomes.length === physicalRows.length) {
|
|
2040
|
-
for (const rootIndex of rootRowIndexes) {
|
|
2041
|
-
if (result.outcomes[rootIndex]) upserted++;
|
|
2042
|
-
else skipped++;
|
|
2043
|
-
}
|
|
2044
|
-
} else {
|
|
2045
|
-
upserted = result.upserted;
|
|
2046
|
-
skipped = result.skipped;
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2218
|
+
const { upserted, skipped } = await writeClientStateEntries({ db, userId, userKey, entries: accepted });
|
|
2049
2219
|
const data = { upserted, skipped };
|
|
2050
2220
|
if (rejected.length > 0) data.rejected = rejected;
|
|
2051
2221
|
return { status: 200, body: { success: true, data } };
|
|
@@ -2092,7 +2262,7 @@ function createClientStateHandler(ctx) {
|
|
|
2092
2262
|
}
|
|
2093
2263
|
|
|
2094
2264
|
// src/server/lib/version.js
|
|
2095
|
-
var SERVER_VERSION = true ? "2.6.0-next.
|
|
2265
|
+
var SERVER_VERSION = true ? "2.6.0-next.7" : "0.0.0-dev";
|
|
2096
2266
|
|
|
2097
2267
|
// src/server/handlers/capabilities.js
|
|
2098
2268
|
var SERVER_FEATURES = Object.freeze([
|
|
@@ -2101,6 +2271,7 @@ var SERVER_FEATURES = Object.freeze([
|
|
|
2101
2271
|
"client-state-partial-failure",
|
|
2102
2272
|
"agentic-hooks",
|
|
2103
2273
|
"agentic-scratch",
|
|
2274
|
+
"agentic-write-state",
|
|
2104
2275
|
"vapid-public-key"
|
|
2105
2276
|
]);
|
|
2106
2277
|
function createCapabilitiesHandler(ctx) {
|
|
@@ -2168,6 +2339,19 @@ var NONCE_INFO = utf8("Content-Encoding: nonce\0");
|
|
|
2168
2339
|
var VAPID_DEFAULT_TTL = 60;
|
|
2169
2340
|
var VAPID_TOKEN_LIFETIME = 12 * 3600;
|
|
2170
2341
|
var RECORD_SIZE = 4096;
|
|
2342
|
+
var WEB_PUSH_MAX_BODY_BYTES = 4096;
|
|
2343
|
+
var WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = 16 + 4 + 1 + 65 + 1 + 16;
|
|
2344
|
+
var MAX_PUSH_PAYLOAD_BYTES = WEB_PUSH_MAX_BODY_BYTES - WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES;
|
|
2345
|
+
var payloadEncoder = new TextEncoder();
|
|
2346
|
+
function measurePushPayload(payload) {
|
|
2347
|
+
const bytes = payloadEncoder.encode(typeof payload === "string" ? payload : String(payload)).length;
|
|
2348
|
+
return {
|
|
2349
|
+
bytes,
|
|
2350
|
+
maxBytes: MAX_PUSH_PAYLOAD_BYTES,
|
|
2351
|
+
remainingBytes: MAX_PUSH_PAYLOAD_BYTES - bytes,
|
|
2352
|
+
withinLimit: bytes <= MAX_PUSH_PAYLOAD_BYTES
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2171
2355
|
async function sendWebPush({ subscription, payload, vapid, ttl, fetch: fetchImpl }) {
|
|
2172
2356
|
if (!subscription || typeof subscription.endpoint !== "string") {
|
|
2173
2357
|
throw new Error("sendWebPush: invalid subscription");
|
|
@@ -2182,6 +2366,16 @@ async function sendWebPush({ subscription, payload, vapid, ttl, fetch: fetchImpl
|
|
|
2182
2366
|
if (typeof subscriptionKeys.p256dh !== "string" || typeof subscriptionKeys.auth !== "string") {
|
|
2183
2367
|
throw new Error("sendWebPush: subscription.keys.p256dh and .auth are required");
|
|
2184
2368
|
}
|
|
2369
|
+
const size = measurePushPayload(payload);
|
|
2370
|
+
if (!size.withinLimit) {
|
|
2371
|
+
const err2 = new Error(
|
|
2372
|
+
`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)`
|
|
2373
|
+
);
|
|
2374
|
+
err2.code = "PUSH_PAYLOAD_TOO_LARGE";
|
|
2375
|
+
err2.bytes = size.bytes;
|
|
2376
|
+
err2.maxBytes = MAX_PUSH_PAYLOAD_BYTES;
|
|
2377
|
+
throw err2;
|
|
2378
|
+
}
|
|
2185
2379
|
const encryptedBody = await encryptPushPayload({
|
|
2186
2380
|
plaintext: utf8(payload),
|
|
2187
2381
|
uaPublicKey: base64UrlToBytes(subscriptionKeys.p256dh),
|
|
@@ -2455,7 +2649,11 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
2455
2649
|
// spreads its ctx into processSingleMessage, so these ride along.
|
|
2456
2650
|
hooks: cfg.hooks || null,
|
|
2457
2651
|
maxToolIterations: cfg.maxToolIterations,
|
|
2458
|
-
totalTimeoutMs: cfg.totalTimeoutMs
|
|
2652
|
+
totalTimeoutMs: cfg.totalTimeoutMs,
|
|
2653
|
+
// hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
|
|
2654
|
+
maxStateValueBytes: cfg.maxStateValueBytes,
|
|
2655
|
+
// 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
|
|
2656
|
+
claimLeaseMs: cfg.claimLeaseMs
|
|
2459
2657
|
});
|
|
2460
2658
|
} catch (error) {
|
|
2461
2659
|
console.error("[amsg single-user] scheduled(): tick failed:", error && error.message);
|
|
@@ -2487,6 +2685,10 @@ export {
|
|
|
2487
2685
|
createMessagesHandler,
|
|
2488
2686
|
createD1Adapter,
|
|
2489
2687
|
createSingleUserServer,
|
|
2688
|
+
WEB_PUSH_MAX_BODY_BYTES,
|
|
2689
|
+
WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
|
|
2690
|
+
MAX_PUSH_PAYLOAD_BYTES,
|
|
2691
|
+
measurePushPayload,
|
|
2490
2692
|
createWebCryptoWebPush,
|
|
2491
2693
|
createSingleUserCloudflareWorker
|
|
2492
2694
|
};
|