@rei-standard/amsg-server 3.0.0 → 3.1.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -428,6 +428,9 @@ function taskPayloadByteLength(serializedPayload) {
428
428
  return taskPayloadEncoder.encode(serializedPayload).length;
429
429
  }
430
430
  function validateScheduleMessagePayload(payload) {
431
+ if (payload.taskInput != null && (typeof payload.taskInput !== "object" || Array.isArray(payload.taskInput))) {
432
+ return { valid: false, errorCode: "INVALID_TASK_INPUT", errorMessage: "taskInput must be an object or null" };
433
+ }
431
434
  if (!payload.contactName || typeof payload.contactName !== "string") {
432
435
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["contactName"] } };
433
436
  }
@@ -982,6 +985,40 @@ function createWebCryptoWebPush(vapid = {}, { ttl = SCHEDULED_DEFAULT_TTL } = {}
982
985
  };
983
986
  }
984
987
 
988
+ // src/server/lib/attachments.js
989
+ var ATTACHMENT_NAMESPACE = "amsg:attachments";
990
+ var ATTACHMENT_RETENTION_DAYS = 30;
991
+ var MAX_ATTACHMENT_TTL_MS = ATTACHMENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
992
+ function createAttachmentStore({ task, occurrenceMs, writeState, now }) {
993
+ return async function storeAttachment(value, { ttlMs = MAX_ATTACHMENT_TTL_MS, owner, kind = "attachment", ownerGeneration } = {}) {
994
+ if (typeof value !== "string") throw new TypeError("storeAttachment value must be a serialized string");
995
+ if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > MAX_ATTACHMENT_TTL_MS) {
996
+ throw new RangeError("storeAttachment ttlMs must be between 1 millisecond and 30 days");
997
+ }
998
+ if (typeof task.uuid !== "string" || !task.uuid || !Number.isFinite(occurrenceMs)) {
999
+ throw new TypeError("storeAttachment requires a task UUID and a valid occurrence");
1000
+ }
1001
+ const at = now();
1002
+ const ref = { version: (
1003
+ /** @type {1} */
1004
+ 1
1005
+ ), namespace: ATTACHMENT_NAMESPACE, key: randomUUID(), taskUuid: task.uuid, occurrenceMs, expiresAt: at + ttlMs };
1006
+ let result;
1007
+ try {
1008
+ result = await writeState(ATTACHMENT_NAMESPACE, [{ key: ref.key, value: JSON.stringify({ ...ref, owner, kind, ownerGeneration, value }), updatedAt: at, owner, kind, ownerGeneration }]);
1009
+ } catch (error) {
1010
+ if (error?.code === "AGENTIC_STATE_WRITE_UNSUPPORTED") {
1011
+ throw new DeploymentConfigError("ATTACHMENT_STORAGE_UNSUPPORTED: attachments require a client_state adapter (built-in support: D1)", { code: "ATTACHMENT_STORAGE_UNSUPPORTED" });
1012
+ }
1013
+ throw error;
1014
+ }
1015
+ if (result.upserted !== 1 || result.skipped !== 0) {
1016
+ throw new Error("ATTACHMENT_WRITE_INCOMPLETE: attachment was not stored");
1017
+ }
1018
+ return ref;
1019
+ };
1020
+ }
1021
+
985
1022
  // src/server/lib/agentic-fire.js
986
1023
  import {
987
1024
  assertValidDecision,
@@ -1192,6 +1229,7 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1192
1229
  let upserted = 0;
1193
1230
  let skipped = 0;
1194
1231
  let deleted = 0;
1232
+ let unknown = 0;
1195
1233
  const skippedEntries = [];
1196
1234
  if (Array.isArray(result.outcomes) && result.outcomes.length === physicalRows.length) {
1197
1235
  for (let i = 0; i < rootRowIndexes.length; i++) {
@@ -1203,9 +1241,10 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1203
1241
  skippedEntries.push({ namespace: entry.namespace, key: entry.key });
1204
1242
  }
1205
1243
  }
1244
+ } else if (result.upserted === physicalRows.length && result.skipped === 0) {
1245
+ upserted = rootRowIndexes.length;
1206
1246
  } else {
1207
- upserted = result.upserted;
1208
- skipped = result.skipped;
1247
+ unknown = rootRowIndexes.length;
1209
1248
  }
1210
1249
  const cleanupOutcomes = Array.isArray(result.cleanupOutcomes) && result.cleanupOutcomes.length === cleanups.length ? result.cleanupOutcomes : null;
1211
1250
  for (const { cleanupIndex, entry } of deletions) {
@@ -1216,22 +1255,26 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1216
1255
  deleted++;
1217
1256
  }
1218
1257
  }
1219
- return { upserted, skipped, deleted, skippedEntries };
1258
+ return { upserted, skipped, deleted, skippedEntries, ...unknown > 0 ? { unknown } : {} };
1220
1259
  }
1221
1260
 
1222
1261
  // src/server/lib/state-accessors.js
1223
1262
  function createStateAccessors({ db, userId, userKey, maxStateValueBytes, now }) {
1224
1263
  const nowFn = typeof now === "function" ? now : Date.now;
1225
1264
  const valueCeiling = Number.isInteger(maxStateValueBytes) && maxStateValueBytes > 0 ? maxStateValueBytes : DEFAULT_MAX_STATE_VALUE_BYTES;
1226
- const readState = async (namespace) => {
1265
+ const readState = async (namespace, { key } = {}) => {
1227
1266
  if (typeof namespace !== "string" || !namespace.trim()) {
1228
1267
  throw new TypeError("readState(namespace) requires a non-empty string");
1229
1268
  }
1269
+ if (key !== void 0 && (typeof key !== "string" || !key.trim() || key.length > MAX_KEY_CHARS || INTERNAL_STATE_CHAR_RE.test(key))) {
1270
+ throw new TypeError(`readState key must be a non-empty string of at most ${MAX_KEY_CHARS} characters without control characters`);
1271
+ }
1230
1272
  if (!db || typeof db.getClientState !== "function") return [];
1231
- const rows = await db.getClientState(userId, namespace);
1273
+ const rows = await db.getClientState(userId, namespace, key === void 0 ? void 0 : { key });
1232
1274
  return resolveClientStateEntries(
1233
- rows,
1234
- () => db.getClientState(userId, chunkNamespaceFor(namespace)),
1275
+ // 老适配器可能忽略第三个参数,仍在解密前过滤根行,保持返回契约。
1276
+ key === void 0 ? rows : rows.filter((row) => row.key === key),
1277
+ () => db.getClientState(userId, chunkNamespaceFor(namespace), key === void 0 ? void 0 : { keyPrefix: chunkKeyPrefixFor(key) }),
1235
1278
  (value) => decryptFromStorage(value, userKey)
1236
1279
  );
1237
1280
  };
@@ -1287,6 +1330,9 @@ function createStateAccessors({ db, userId, userKey, maxStateValueBytes, now })
1287
1330
  namespace,
1288
1331
  key: entry.key,
1289
1332
  value: entry.value,
1333
+ owner: entry.owner,
1334
+ kind: entry.kind,
1335
+ ownerGeneration: entry.ownerGeneration,
1290
1336
  updatedAt: entry.updatedAt ?? at,
1291
1337
  ...entry.version !== void 0 ? { version: entry.version } : {}
1292
1338
  };
@@ -1618,7 +1664,7 @@ function projectTask(row, decryptedPayload, options = {}) {
1618
1664
  // 凭据引用只是名字(cred_id),本体在 llm_credentials 表里,凭据字段本身
1619
1665
  // 照旧被白名单挡在外面。
1620
1666
  credRefs: payload.credRefs ?? null,
1621
- // 整份 metadata 只在单条查询里给(见上面的 includeMetadata)。
1667
+ // 整份 metadata 由 includeMetadata 控制,单条查询与 hook 回执会带。
1622
1668
  ...options.includeMetadata ? { metadata: payload.metadata ?? null } : {},
1623
1669
  // 上一次没发出去的原因。reason 'stale' 表示错过触发时刻太久被判定不再补
1624
1670
  // 发;其余是投递失败的错误信息。没有记录 → null。
@@ -1709,6 +1755,9 @@ function normalizeBeforeFireResult(result) {
1709
1755
  return { messages: result };
1710
1756
  }
1711
1757
  if (result && typeof result === "object" && Array.isArray(result.messages)) {
1758
+ if (result.allowedToolNames !== void 0 && (!Array.isArray(result.allowedToolNames) || result.allowedToolNames.some((name) => typeof name !== "string" || !name.trim()))) {
1759
+ throw markPermanent(new TypeError("allowedToolNames must be an array of non-empty strings"), "AGENTIC_BAD_BEFORE_FIRE");
1760
+ }
1712
1761
  return {
1713
1762
  messages: result.messages,
1714
1763
  maxToolIterations: result.maxToolIterations,
@@ -1717,7 +1766,8 @@ function normalizeBeforeFireResult(result) {
1717
1766
  // 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
1718
1767
  // buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
1719
1768
  tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : void 0,
1720
- toolChoice: result.toolChoice
1769
+ toolChoice: result.toolChoice,
1770
+ allowedToolNames: result.allowedToolNames === void 0 ? void 0 : new Set(result.allowedToolNames)
1721
1771
  };
1722
1772
  }
1723
1773
  throw markPermanent(new TypeError(BAD_BEFORE_FIRE_MESSAGE), "AGENTIC_BAD_BEFORE_FIRE");
@@ -1769,6 +1819,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1769
1819
  const sessionId = task.id != null ? `sess_task_${task.id}${occurrenceSuffix(task)}` : `sess_${randomUUID()}`;
1770
1820
  const messageIdBase = task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_${randomUUID()}`;
1771
1821
  const occurrenceMs = occurrenceMsOf(task);
1822
+ const storeAttachment = createAttachmentStore({ task, occurrenceMs, writeState, now: nowFn });
1823
+ const storeSettledAttachment = createAttachmentStore({ task, occurrenceMs, writeState: writeSettledState, now: nowFn });
1772
1824
  const { emitResult } = createResultEmitter({
1773
1825
  db: ctx.db,
1774
1826
  task,
@@ -1791,17 +1843,37 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1791
1843
  const row = await ctx.db.getTaskByUuid(uuid, task.user_id);
1792
1844
  if (!row) return null;
1793
1845
  const payload = JSON.parse(await decryptFromStorage(row.encrypted_payload, userKey));
1794
- return projectTask(row, payload);
1846
+ return projectTask(row, payload, { includeMetadata: true });
1795
1847
  } catch (error) {
1796
1848
  console.warn("[amsg-server] scheduleTask: \u8BFB\u53D6\u649E\u8F66\u4EFB\u52A1\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
1797
1849
  return null;
1798
1850
  }
1799
1851
  }
1852
+ const listTasks = async (options = {}) => {
1853
+ throwIfCancelled();
1854
+ const { status = "pending", limit = 100, offset = 0, includeMetadata = true } = options;
1855
+ if (!["pending", "sent", "failed", "all"].includes(status) || !Number.isInteger(limit) || limit < 1 || limit > 100 || !Number.isInteger(offset) || offset < 0) {
1856
+ throw new TypeError("listTasks requires a valid status, limit 1-100, and non-negative offset");
1857
+ }
1858
+ if (typeof ctx.db?.listTasks !== "function") {
1859
+ throw new DeploymentConfigError("AGENTIC_TASK_VIEW_UNSUPPORTED: adapter has no listTasks", { code: "AGENTIC_TASK_VIEW_UNSUPPORTED" });
1860
+ }
1861
+ const { tasks, total } = await ctx.db.listTasks(task.user_id, { status, limit, offset });
1862
+ const projected = await Promise.all(tasks.map(async (row) => projectTask(
1863
+ row,
1864
+ JSON.parse(await decryptFromStorage(row.encrypted_payload, userKey)),
1865
+ { includeMetadata }
1866
+ )));
1867
+ return { tasks: projected, pagination: { total, limit, offset, hasMore: offset + tasks.length < total } };
1868
+ };
1800
1869
  const scheduleTask = async (options) => {
1801
1870
  throwIfCancelled();
1802
1871
  if (!options || typeof options !== "object" || Array.isArray(options)) {
1803
1872
  throw new TypeError("scheduleTask(options) \u9700\u8981\u4E00\u4E2A\u5BF9\u8C61\uFF0C\u81F3\u5C11\u5305\u542B { firstSendTime }");
1804
1873
  }
1874
+ if (options.taskInput != null && (typeof options.taskInput !== "object" || Array.isArray(options.taskInput))) {
1875
+ throw new TypeError("scheduleTask: taskInput must be an object or null");
1876
+ }
1805
1877
  if (typeof options.firstSendTime !== "string" || !options.firstSendTime.trim()) {
1806
1878
  throw new RangeError("scheduleTask: firstSendTime \u5FC5\u586B\uFF0C\u4E14\u5FC5\u987B\u662F ISO 8601 \u5B57\u7B26\u4E32");
1807
1879
  }
@@ -1896,7 +1968,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1896
1968
  // 与凭据同列的投递配置:中转的非标准参数(thinking 之类)跟着任务走,
1897
1969
  // 自排的后续任务不该突然掉档。
1898
1970
  llmExtraBody: decryptedPayload.llmExtraBody ?? null,
1899
- metadata
1971
+ metadata,
1972
+ taskInput: options.taskInput ?? null
1900
1973
  };
1901
1974
  const serializedTaskData = JSON.stringify(fullTaskData);
1902
1975
  const sizeError = validateTaskPayloadSize(serializedTaskData);
@@ -1950,7 +2023,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1950
2023
  created: true,
1951
2024
  id: created.id ?? null,
1952
2025
  uuid: created.uuid ?? uuid,
1953
- nextSendAt: created.next_send_at ?? nextSendAt
2026
+ nextSendAt: created.next_send_at ?? nextSendAt,
2027
+ task: projectTask({ status: "pending", next_send_at: nextSendAt, ...created }, fullTaskData, { includeMetadata: true })
1954
2028
  };
1955
2029
  };
1956
2030
  const cancelTask = async (uuid) => {
@@ -2018,7 +2092,11 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
2018
2092
  const stillPending = await ctx.db.getTaskByUuid(uuid, task.user_id);
2019
2093
  return { renewed: false, reason: stillPending ? "in_flight" : "not_found" };
2020
2094
  }
2021
- return { renewed: true, uuid, nextSendAt: nextSendAtIso };
2095
+ return { renewed: true, uuid, nextSendAt: nextSendAtIso, task: projectTask(
2096
+ { ...row, next_send_at: nextSendAtIso, retry_count: 0 },
2097
+ payload,
2098
+ { includeMetadata: true }
2099
+ ) };
2022
2100
  };
2023
2101
  const resolveLlmCredential2 = async (credId) => {
2024
2102
  if (typeof credId !== "string" || !credId.trim()) {
@@ -2035,7 +2113,9 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
2035
2113
  readState,
2036
2114
  writeState,
2037
2115
  emitResult,
2116
+ storeAttachment,
2038
2117
  scheduleTask,
2118
+ listTasks,
2039
2119
  cancelTask,
2040
2120
  renewTask,
2041
2121
  resolveLlmCredential: resolveLlmCredential2,
@@ -2068,8 +2148,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
2068
2148
  readState,
2069
2149
  writeState,
2070
2150
  emitResult,
2151
+ storeAttachment,
2071
2152
  scratch,
2072
2153
  scheduleTask,
2154
+ listTasks,
2073
2155
  cancelTask,
2074
2156
  renewTask,
2075
2157
  resolveLlmCredential: resolveLlmCredential2,
@@ -2128,7 +2210,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
2128
2210
  scratch,
2129
2211
  readState,
2130
2212
  writeState: writeSettledState,
2131
- emitResult
2213
+ emitResult,
2214
+ storeAttachment: storeSettledAttachment
2132
2215
  });
2133
2216
  }
2134
2217
  }
@@ -2143,8 +2226,10 @@ async function runFireChain({
2143
2226
  readState,
2144
2227
  writeState,
2145
2228
  emitResult,
2229
+ storeAttachment,
2146
2230
  scratch,
2147
2231
  scheduleTask,
2232
+ listTasks,
2148
2233
  cancelTask,
2149
2234
  renewTask,
2150
2235
  resolveLlmCredential: resolveLlmCredential2,
@@ -2243,15 +2328,35 @@ async function runFireChain({
2243
2328
  readState,
2244
2329
  writeState,
2245
2330
  emitResult,
2331
+ storeAttachment,
2246
2332
  scheduleTask,
2333
+ listTasks,
2247
2334
  cancelTask,
2248
2335
  renewTask,
2249
2336
  resolveLlmCredential: resolveLlmCredential2
2250
2337
  });
2251
2338
  const decision = await hooks.onLLMOutput(sessionCtx);
2252
2339
  ctx.throwIfCancelled();
2340
+ const rawCalls = decision?.decision === "tool-request" ? extractToolCallsFromDecision(decision) : [];
2341
+ const validId = (call) => typeof call?.id === "string" && call.id.trim().length > 0;
2342
+ const nativeCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
2343
+ const usedIds = new Set([...rawCalls, ...nativeCalls].filter(validId).map((call) => call.id));
2344
+ const malformedIds = /* @__PURE__ */ new Set();
2345
+ const toolCalls = rawCalls.map((call, index) => {
2346
+ let id = validId(call) ? call.id : `call_${iteration + 1}_${index + 1}`;
2347
+ if (!validId(call)) {
2348
+ while (usedIds.has(id)) id += "_";
2349
+ usedIds.add(id);
2350
+ }
2351
+ if (typeof call?.function?.name !== "string" || !call.function.name.trim()) {
2352
+ malformedIds.add(id);
2353
+ return { id, type: "function", function: { name: "invalid_tool_call", arguments: "{}" } };
2354
+ }
2355
+ return { ...call, id };
2356
+ });
2357
+ const validationDecision = decision?.decision === "tool-request" && Array.isArray(decision.toolCalls) && decision.toolCalls.length > 0 ? { ...decision, toolCalls } : decision;
2253
2358
  try {
2254
- assertValidDecision(decision, { inlineToolCalls: true });
2359
+ assertValidDecision(validationDecision, { inlineToolCalls: true });
2255
2360
  } catch (error) {
2256
2361
  throw markPermanent(error, "AGENTIC_BAD_DECISION");
2257
2362
  }
@@ -2282,14 +2387,19 @@ async function runFireChain({
2282
2387
  });
2283
2388
  return { handled: true, result: { success: true, messagesSent, status: "finished", iterations: iteration + 1 } };
2284
2389
  }
2285
- const toolCalls = extractToolCallsFromDecision(decision);
2286
2390
  if (toolCalls.length === 0) {
2287
2391
  throw taggedRetryableError(
2288
2392
  "AGENTIC_EMPTY_TOOL_REQUEST: tool-request decision carried no toolCalls (neither decision.toolCalls nor pushPayloads[].toolCalls)",
2289
2393
  "AGENTIC_EMPTY_TOOL_REQUEST"
2290
2394
  );
2291
2395
  }
2292
- if (typeof hooks.executeToolCalls !== "function") {
2396
+ const permittedCalls = toolCalls.filter((call) => !malformedIds.has(call.id) && (normalized.allowedToolNames === void 0 || normalized.allowedToolNames.has(call.function.name)));
2397
+ const deniedResults = toolCalls.filter((call) => !permittedCalls.includes(call)).map((call) => ({
2398
+ role: "tool",
2399
+ tool_call_id: call.id,
2400
+ content: JSON.stringify({ error: malformedIds.has(call.id) ? { code: "INVALID_TOOL_CALL", message: "Tool call requires a function name" } : { code: "TOOL_NOT_ALLOWED", message: "Tool execution is not allowed for this fire" } })
2401
+ }));
2402
+ if (permittedCalls.length > 0 && typeof hooks.executeToolCalls !== "function") {
2293
2403
  throw new DeploymentConfigError(
2294
2404
  "AGENTIC_CONFIG_ERROR: onLLMOutput returned tool-request but hooks.executeToolCalls is not configured",
2295
2405
  { code: "AGENTIC_CONFIG_ERROR" }
@@ -2301,7 +2411,7 @@ async function runFireChain({
2301
2411
  let toolResults;
2302
2412
  try {
2303
2413
  ctx.throwIfCancelled();
2304
- toolResults = await hooks.executeToolCalls(toolCalls, sessionCtx);
2414
+ toolResults = permittedCalls.length > 0 ? await hooks.executeToolCalls(permittedCalls, sessionCtx) : [];
2305
2415
  ctx.throwIfCancelled();
2306
2416
  if (!Array.isArray(toolResults)) {
2307
2417
  throw new TypeError('executeToolCalls must resolve to an array of { tool_call_id, role: "tool", content }');
@@ -2309,16 +2419,17 @@ async function runFireChain({
2309
2419
  } catch (error) {
2310
2420
  ctx.throwIfCancelled();
2311
2421
  if (isTaskCancelledError(error)) throw error;
2312
- toolResults = toolCalls.map((toolCall) => ({
2422
+ toolResults = permittedCalls.map((toolCall) => ({
2313
2423
  tool_call_id: toolCall && typeof toolCall === "object" && typeof toolCall.id === "string" ? toolCall.id : "",
2314
2424
  role: "tool",
2315
2425
  content: `Tool execution failed: ${error?.message ?? String(error)}`
2316
2426
  }));
2317
2427
  }
2318
- const nativeCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
2319
- const nativeIds = new Set(nativeCalls.map((tc) => tc && tc.id));
2320
- const synthesized = toolCalls.filter((tc) => !nativeIds.has(tc && tc.id));
2321
- const assistantWithTools = synthesized.length === 0 ? assistantMessage : { ...assistantMessage, tool_calls: [...nativeCalls, ...synthesized] };
2428
+ toolResults = [...toolResults, ...deniedResults];
2429
+ const declared = nativeCalls.filter(validId).map((call) => toolCalls.find((item) => item.id === call.id) || call);
2430
+ const nativeIds = new Set(declared.map((call) => call.id));
2431
+ const synthesized = toolCalls.filter((call) => !nativeIds.has(call.id));
2432
+ const assistantWithTools = { ...assistantMessage, tool_calls: [...declared, ...synthesized] };
2322
2433
  messages = [...messages.slice(0, -1), assistantWithTools, ...toolResults];
2323
2434
  }
2324
2435
  throw taggedRetryableError(
@@ -3392,6 +3503,9 @@ function createScheduleMessageHandler(ctx) {
3392
3503
  owner: payload.owner,
3393
3504
  kind: payload.kind,
3394
3505
  ownerGeneration: payload.ownerGeneration,
3506
+ // Input is persisted with the task in the same create/supersede transaction.
3507
+ // Shared credentials remain references and continue to resolve at fire time.
3508
+ taskInput: payload.taskInput ?? null,
3395
3509
  metadata: payload.metadata || {}
3396
3510
  };
3397
3511
  const serializedTaskData = JSON.stringify(fullTaskData);
@@ -3599,7 +3713,7 @@ async function runScheduledTick(ctx) {
3599
3713
  }
3600
3714
  async function cleanupExpiredClientState(ctx) {
3601
3715
  if (typeof ctx.db.cleanupClientState !== "function") return;
3602
- const targets = planClientStateCleanup(ctx.clientStateTtl, Date.now());
3716
+ const targets = planClientStateCleanup({ [ATTACHMENT_NAMESPACE]: ATTACHMENT_RETENTION_DAYS, ...ctx.clientStateTtl }, Date.now());
3603
3717
  if (targets.length === 0) return;
3604
3718
  try {
3605
3719
  await ctx.db.cleanupClientState(targets);
@@ -4259,6 +4373,9 @@ function createUpdateMessageHandler(ctx) {
4259
4373
  if (!isPlainObject(updates)) {
4260
4374
  return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF" } } };
4261
4375
  }
4376
+ if (Object.prototype.hasOwnProperty.call(updates, "taskInput")) {
4377
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "taskInput \u53EA\u80FD\u5728\u521B\u5EFA\u6216\u66FF\u6362\u4EFB\u52A1\u65F6\u8BBE\u7F6E", details: { invalidFields: ["taskInput"] } } } };
4378
+ }
4262
4379
  if (updates.nextSendAt && !isValidISO8601(updates.nextSendAt)) {
4263
4380
  return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["nextSendAt"] } } } };
4264
4381
  }
@@ -6175,24 +6292,33 @@ var D1Adapter = class {
6175
6292
  return result;
6176
6293
  }
6177
6294
  /**
6178
- * All entries of one namespace (values still encrypted).
6295
+ * Entries of one namespace (values still encrypted); optional exact/prefix selection.
6179
6296
  *
6180
6297
  * @param {string} userId
6181
6298
  * @param {string} namespace
6182
6299
  * @returns {Promise<Array<{ namespace: string, key: string, value: string, updated_at: number }>>}
6183
6300
  */
6184
- async getClientState(userId, namespace) {
6301
+ async getClientState(userId, namespace, options = {}) {
6302
+ const params = [userId, namespace];
6303
+ let selector = "";
6304
+ if (options.key !== void 0) {
6305
+ selector = " AND key = ?";
6306
+ params.push(options.key);
6307
+ } else if (options.keyPrefix !== void 0) {
6308
+ selector = " AND key >= ? AND key < ?";
6309
+ params.push(options.keyPrefix, prefixRangeEnd(options.keyPrefix));
6310
+ }
6185
6311
  const res = await this._db.prepare(
6186
6312
  `SELECT namespace, key, value, updated_at
6187
6313
  FROM client_state
6188
- WHERE user_id = ? AND namespace = ?
6314
+ WHERE user_id = ? AND namespace = ?${selector}
6189
6315
  ORDER BY key ASC`
6190
- ).bind(userId, namespace).all();
6316
+ ).bind(...params).all();
6191
6317
  return res.results || [];
6192
6318
  }
6193
6319
  /**
6194
6320
  * 例行清理:把指定命名空间下太久没更新的条目删掉(run-tick 每跳顺手调,
6195
- * 宿主配了 `clientStateTtl` 才会调)。
6321
+ * 附件默认保留 30 天,宿主 `clientStateTtl` 可覆盖或配置其他命名空间)。
6196
6322
  *
6197
6323
  * 不限用户——「这个命名空间只留最近 N 天」是命名空间级的约定,单用户部署
6198
6324
  * 下也就是这一个用户的行。指令由 lib/client-state-store.js 的
@@ -6924,8 +7050,9 @@ function createClientStateHandler(ctx) {
6924
7050
  if (typeof db.upsertClientState !== "function") {
6925
7051
  return err3(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
6926
7052
  }
6927
- const { upserted, skipped, deleted, skippedEntries } = await writeClientStateEntries({ db, userId, userKey, entries: accepted });
7053
+ const { upserted, skipped, deleted, skippedEntries, unknown } = await writeClientStateEntries({ db, userId, userKey, entries: accepted });
6928
7054
  const data = { upserted, skipped };
7055
+ if (unknown > 0) data.unknown = unknown;
6929
7056
  if (deleted > 0) data.deleted = deleted;
6930
7057
  if (skippedEntries.length > 0) data.skippedEntries = skippedEntries;
6931
7058
  if (rejected.length > 0) data.rejected = rejected;
@@ -6938,7 +7065,12 @@ function createClientStateHandler(ctx) {
6938
7065
  const gate = requireUserId(headers);
6939
7066
  if (gate.error) return gate.error;
6940
7067
  const { userId } = gate;
6941
- const namespace = new URL(url, "https://dummy").searchParams.get("namespace") || "";
7068
+ const query = new URL(url, "https://dummy").searchParams;
7069
+ const namespace = query.get("namespace") || "";
7070
+ const key = query.get("key");
7071
+ if (key !== null && (!key.trim() || key.length > MAX_KEY_CHARS || INTERNAL_STATE_CHAR_RE.test(key))) {
7072
+ return err3(400, "INVALID_STATE_KEY", "key must be non-empty, at most 256 characters, without control characters");
7073
+ }
6942
7074
  if (!namespace.trim()) return err3(400, "NAMESPACE_REQUIRED", "\u5FC5\u987B\u63D0\u4F9B namespace \u67E5\u8BE2\u53C2\u6570");
6943
7075
  if (INTERNAL_STATE_CHAR_RE.test(namespace)) {
6944
7076
  return err3(400, "INVALID_STATE_NAMESPACE", "namespace \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09");
@@ -6947,10 +7079,10 @@ function createClientStateHandler(ctx) {
6947
7079
  return err3(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
6948
7080
  }
6949
7081
  const userKey = await deriveUserEncryptionKey(userId, masterKey);
6950
- const rows = await db.getClientState(userId, namespace);
7082
+ const rows = await db.getClientState(userId, namespace, key === null ? void 0 : { key });
6951
7083
  const decrypted = await resolveClientStateEntries(
6952
- rows,
6953
- () => db.getClientState(userId, chunkNamespaceFor(namespace)),
7084
+ key === null ? rows : rows.filter((row) => row.key === key),
7085
+ () => db.getClientState(userId, chunkNamespaceFor(namespace), key === null ? void 0 : { keyPrefix: chunkKeyPrefixFor(key) }),
6954
7086
  (value) => decryptFromStorage(value, userKey)
6955
7087
  );
6956
7088
  const encryptedResponse = await encryptPayload({ namespace, entries: decrypted }, userKey);
@@ -7028,7 +7160,7 @@ function createClientStateNamespacesHandler(ctx) {
7028
7160
  }
7029
7161
 
7030
7162
  // src/server/lib/version.js
7031
- var SERVER_VERSION = true ? "3.0.0" : "0.0.0-dev";
7163
+ var SERVER_VERSION = true ? "3.1.0-next.0" : "0.0.0-dev";
7032
7164
 
7033
7165
  // src/server/handlers/capabilities.js
7034
7166
  var SERVER_FEATURES = Object.freeze([
@@ -7144,7 +7276,12 @@ var SERVER_FEATURES = Object.freeze([
7144
7276
  "max-generation-retries",
7145
7277
  // onBeforeFire 认 { defer: { afterMs } }:这次不生成,过一会儿再来问(不占重试
7146
7278
  // 次数);onFireSettled 的 status 多一种 'deferred',带 retryAfter。
7147
- "before-fire-defer"
7279
+ "before-fire-defer",
7280
+ "task-input",
7281
+ "client-state-key",
7282
+ "agentic-list-tasks",
7283
+ "agentic-tool-allowlist",
7284
+ "attachments"
7148
7285
  ]);
7149
7286
  function createCapabilitiesHandler(ctx) {
7150
7287
  async function GET(url, headers) {
@@ -8718,6 +8855,7 @@ export {
8718
8855
  timingSafeEqualBytes,
8719
8856
  deriveUserEncryptionKey,
8720
8857
  decryptPayload,
8858
+ encryptPayload,
8721
8859
  encryptForStorage,
8722
8860
  decryptFromStorage,
8723
8861
  CRED_ID_MAX_LENGTH,
@@ -28,7 +28,8 @@
28
28
 
29
29
 
30
30
 
31
- var _chunkYBDQJVFQcjs = require('./chunk-YBDQJVFQ.cjs');
31
+
32
+ var _chunk3NIYNYZYcjs = require('./chunk-3NIYNYZY.cjs');
32
33
  require('./chunk-7NMQFTDJ.cjs');
33
34
 
34
35
 
@@ -60,4 +61,5 @@ require('./chunk-7NMQFTDJ.cjs');
60
61
 
61
62
 
62
63
 
63
- exports.DEFAULT_MAX_DELIVERY_RETRIES = _chunkYBDQJVFQcjs.DEFAULT_MAX_DELIVERY_RETRIES; exports.DEFAULT_MAX_REQUEST_BODY_BYTES = _chunkYBDQJVFQcjs.DEFAULT_MAX_REQUEST_BODY_BYTES; exports.MAX_DEFER_AFTER_MS = _chunkYBDQJVFQcjs.MAX_DEFER_AFTER_MS; exports.MAX_PUSH_PAYLOAD_BYTES = _chunkYBDQJVFQcjs.MAX_PUSH_PAYLOAD_BYTES; exports.NonRetryableError = _chunkYBDQJVFQcjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkYBDQJVFQcjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkYBDQJVFQcjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkYBDQJVFQcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkYBDQJVFQcjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkYBDQJVFQcjs.advanceOccurrence; exports.createD1Adapter = _chunkYBDQJVFQcjs.createD1Adapter; exports.createSingleUserCloudflareWorker = _chunkYBDQJVFQcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkYBDQJVFQcjs.createSingleUserServer; exports.createWebCryptoWebPush = _chunkYBDQJVFQcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkYBDQJVFQcjs.decryptFromStorage; exports.decryptPayload = _chunkYBDQJVFQcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkYBDQJVFQcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkYBDQJVFQcjs.encryptForStorage; exports.ensureSchema = _chunkYBDQJVFQcjs.ensureSchema; exports.getSchemaVersion = _chunkYBDQJVFQcjs.getSchemaVersion; exports.isNonRetryableError = _chunkYBDQJVFQcjs.isNonRetryableError; exports.isValidTimeZoneId = _chunkYBDQJVFQcjs.isValidTimeZoneId; exports.measurePushPayload = _chunkYBDQJVFQcjs.measurePushPayload; exports.nextFutureOccurrence = _chunkYBDQJVFQcjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkYBDQJVFQcjs.planNextOccurrence; exports.readRequestBody = _chunkYBDQJVFQcjs.readRequestBody; exports.runScheduledTick = _chunkYBDQJVFQcjs.runScheduledTick; exports.runTask = _chunkYBDQJVFQcjs.runTask; exports.summarizeErrorCause = _chunkYBDQJVFQcjs.summarizeErrorCause;
64
+
65
+ exports.DEFAULT_MAX_DELIVERY_RETRIES = _chunk3NIYNYZYcjs.DEFAULT_MAX_DELIVERY_RETRIES; exports.DEFAULT_MAX_REQUEST_BODY_BYTES = _chunk3NIYNYZYcjs.DEFAULT_MAX_REQUEST_BODY_BYTES; exports.MAX_DEFER_AFTER_MS = _chunk3NIYNYZYcjs.MAX_DEFER_AFTER_MS; exports.MAX_PUSH_PAYLOAD_BYTES = _chunk3NIYNYZYcjs.MAX_PUSH_PAYLOAD_BYTES; exports.NonRetryableError = _chunk3NIYNYZYcjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunk3NIYNYZYcjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunk3NIYNYZYcjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunk3NIYNYZYcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunk3NIYNYZYcjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunk3NIYNYZYcjs.advanceOccurrence; exports.createD1Adapter = _chunk3NIYNYZYcjs.createD1Adapter; exports.createSingleUserCloudflareWorker = _chunk3NIYNYZYcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunk3NIYNYZYcjs.createSingleUserServer; exports.createWebCryptoWebPush = _chunk3NIYNYZYcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunk3NIYNYZYcjs.decryptFromStorage; exports.decryptPayload = _chunk3NIYNYZYcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunk3NIYNYZYcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunk3NIYNYZYcjs.encryptForStorage; exports.encryptPayload = _chunk3NIYNYZYcjs.encryptPayload; exports.ensureSchema = _chunk3NIYNYZYcjs.ensureSchema; exports.getSchemaVersion = _chunk3NIYNYZYcjs.getSchemaVersion; exports.isNonRetryableError = _chunk3NIYNYZYcjs.isNonRetryableError; exports.isValidTimeZoneId = _chunk3NIYNYZYcjs.isValidTimeZoneId; exports.measurePushPayload = _chunk3NIYNYZYcjs.measurePushPayload; exports.nextFutureOccurrence = _chunk3NIYNYZYcjs.nextFutureOccurrence; exports.planNextOccurrence = _chunk3NIYNYZYcjs.planNextOccurrence; exports.readRequestBody = _chunk3NIYNYZYcjs.readRequestBody; exports.runScheduledTick = _chunk3NIYNYZYcjs.runScheduledTick; exports.runTask = _chunk3NIYNYZYcjs.runTask; exports.summarizeErrorCause = _chunk3NIYNYZYcjs.summarizeErrorCause;
@@ -8,4 +8,4 @@ export { getSchemaVersion, ensureSchema, SCHEMA_VERSION } from "./lib/schema-ver
8
8
  export { summarizeErrorCause, NonRetryableError, isNonRetryableError } from "./lib/errors.js";
9
9
  export { isValidTimeZoneId, advanceOccurrence, nextFutureOccurrence, planNextOccurrence } from "./lib/recurrence.js";
10
10
  export { readRequestBody, DEFAULT_MAX_REQUEST_BODY_BYTES } from "./lib/request.js";
11
- export { deriveUserEncryptionKey, decryptPayload, encryptForStorage, decryptFromStorage } from "./lib/encryption.js";
11
+ export { deriveUserEncryptionKey, decryptPayload, encryptPayload, encryptForStorage, decryptFromStorage } from "./lib/encryption.js";
@@ -17,6 +17,7 @@ import {
17
17
  decryptPayload,
18
18
  deriveUserEncryptionKey,
19
19
  encryptForStorage,
20
+ encryptPayload,
20
21
  ensureSchema,
21
22
  getSchemaVersion,
22
23
  isNonRetryableError,
@@ -28,7 +29,7 @@ import {
28
29
  runScheduledTick,
29
30
  runTask,
30
31
  summarizeErrorCause
31
- } from "./chunk-RL7V7VSS.mjs";
32
+ } from "./chunk-6TCZEACV.mjs";
32
33
  import "./chunk-GN44PST5.mjs";
33
34
  export {
34
35
  DEFAULT_MAX_DELIVERY_RETRIES,
@@ -49,6 +50,7 @@ export {
49
50
  decryptPayload,
50
51
  deriveUserEncryptionKey,
51
52
  encryptForStorage,
53
+ encryptPayload,
52
54
  ensureSchema,
53
55
  getSchemaVersion,
54
56
  isNonRetryableError,