@rei-standard/amsg-server 2.6.0-next.3 → 2.6.0-next.5

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.
@@ -485,6 +485,81 @@ import {
485
485
  extractAssistantMessage,
486
486
  extractToolCallsFromDecision
487
487
  } from "@rei-standard/amsg-shared";
488
+
489
+ // src/server/lib/state-chunks.js
490
+ import { chunkReasoningByUtf8Bytes } from "@rei-standard/amsg-shared";
491
+ var STATE_CHUNK_SLICE_BYTES = 200 * 1024;
492
+ var DEFAULT_MAX_STATE_VALUE_BYTES = 5 * 1024 * 1024;
493
+ var INTERNAL_STATE_CHAR_RE = /[\u0000-\u001f]/;
494
+ var SEP = "";
495
+ var CHUNK_NS_PREFIX = `${SEP}amsg-chunks${SEP}`;
496
+ var ROOT_MARKER_PREFIX = `${SEP}amsg-chunked${SEP}v1${SEP}`;
497
+ function chunkNamespaceFor(namespace) {
498
+ return CHUNK_NS_PREFIX + namespace;
499
+ }
500
+ function chunkKeyFor(key, index) {
501
+ return `${key}${SEP}${index}`;
502
+ }
503
+ function chunkKeyPrefixFor(key) {
504
+ return `${key}${SEP}`;
505
+ }
506
+ function buildChunkedRootValue(chunkCount) {
507
+ return `${ROOT_MARKER_PREFIX}${chunkCount}`;
508
+ }
509
+ function parseChunkedRootCount(value) {
510
+ if (typeof value !== "string" || !value.startsWith(ROOT_MARKER_PREFIX)) return null;
511
+ const raw = value.slice(ROOT_MARKER_PREFIX.length);
512
+ if (!/^[1-9][0-9]*$/.test(raw)) return null;
513
+ return Number(raw);
514
+ }
515
+ function splitStateValue(value) {
516
+ return chunkReasoningByUtf8Bytes(value, STATE_CHUNK_SLICE_BYTES);
517
+ }
518
+ async function resolveClientStateEntries(rows, fetchChunkRows, decryptValue) {
519
+ let chunkMap = null;
520
+ const loadChunks = async () => {
521
+ if (chunkMap === null) {
522
+ const chunkRows = await fetchChunkRows();
523
+ chunkMap = new Map(chunkRows.map((row) => [row.key, row]));
524
+ }
525
+ return chunkMap;
526
+ };
527
+ const entries = [];
528
+ for (const row of rows) {
529
+ const count = parseChunkedRootCount(row.value);
530
+ if (count === null) {
531
+ entries.push({
532
+ namespace: row.namespace,
533
+ key: row.key,
534
+ value: await decryptValue(row.value),
535
+ updatedAt: row.updated_at
536
+ });
537
+ continue;
538
+ }
539
+ const map = await loadChunks();
540
+ const chunkRows = [];
541
+ let intact = true;
542
+ for (let i = 0; i < count; i++) {
543
+ const chunk = map.get(chunkKeyFor(row.key, i));
544
+ if (!chunk || chunk.updated_at !== row.updated_at) {
545
+ intact = false;
546
+ break;
547
+ }
548
+ chunkRows.push(chunk);
549
+ }
550
+ if (!intact) continue;
551
+ const parts = await Promise.all(chunkRows.map((chunk) => decryptValue(chunk.value)));
552
+ entries.push({
553
+ namespace: row.namespace,
554
+ key: row.key,
555
+ value: parts.join(""),
556
+ updatedAt: row.updated_at
557
+ });
558
+ }
559
+ return entries;
560
+ }
561
+
562
+ // src/server/lib/agentic-fire.js
488
563
  var DEFAULT_MAX_TOOL_ITERATIONS = 5;
489
564
  var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
490
565
  var SLEEP_BETWEEN_MESSAGES_MS = 1500;
@@ -523,7 +598,7 @@ function normalizeBeforeFireResult(result) {
523
598
  };
524
599
  }
525
600
  throw new TypeError(
526
- "AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs? } | null"
601
+ "AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs? } | { skip: true } | null"
527
602
  );
528
603
  }
529
604
  function firstPositiveInt(values, fallback) {
@@ -551,21 +626,25 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
551
626
  }
552
627
  if (!ctx.db || typeof ctx.db.getClientState !== "function") return [];
553
628
  const rows = await ctx.db.getClientState(task.user_id, namespace);
554
- return Promise.all(rows.map(async (row) => ({
555
- namespace: row.namespace,
556
- key: row.key,
557
- value: await decryptFromStorage(row.value, userKey),
558
- updatedAt: row.updated_at
559
- })));
629
+ return resolveClientStateEntries(
630
+ rows,
631
+ () => ctx.db.getClientState(task.user_id, chunkNamespaceFor(namespace)),
632
+ (value) => decryptFromStorage(value, userKey)
633
+ );
560
634
  };
635
+ const scratch = {};
561
636
  const fireCtx = Object.freeze({
562
637
  task: buildHookTask(task, decryptedPayload),
563
638
  userId: task.user_id,
564
639
  readState,
565
- now: new Date(nowFn())
640
+ now: new Date(nowFn()),
641
+ scratch
566
642
  });
567
643
  const before = await hooks.onBeforeFire(fireCtx);
568
644
  if (before == null) return { handled: false };
645
+ if (typeof before === "object" && before.skip === true) {
646
+ return { handled: true, result: { success: true, messagesSent: 0, status: "skipped", iterations: 0 } };
647
+ }
569
648
  const normalized = normalizeBeforeFireResult(before);
570
649
  const maxToolIterations = firstPositiveInt(
571
650
  [normalized.maxToolIterations, ctx.maxToolIterations],
@@ -597,7 +676,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
597
676
  contactName: decryptedPayload.contactName,
598
677
  avatarUrl: decryptedPayload.avatarUrl || void 0,
599
678
  charId: decryptedPayload.charId,
600
- metadata: decryptedPayload.metadata
679
+ metadata: decryptedPayload.metadata,
680
+ scratch
601
681
  });
602
682
  const decision = await hooks.onLLMOutput(sessionCtx);
603
683
  assertValidDecision(decision, { inlineToolCalls: true });
@@ -1339,7 +1419,14 @@ function createMessagesHandler(ctx) {
1339
1419
  status: task.status,
1340
1420
  retryCount: task.retry_count,
1341
1421
  createdAt: task.created_at,
1342
- updatedAt: task.updated_at
1422
+ updatedAt: task.updated_at,
1423
+ // Character ownership / client-side task identity, pulled from the
1424
+ // scheduling host's metadata so it can filter tasks by character
1425
+ // (contactName can collide across characters). Only these two
1426
+ // metadata fields are surfaced — the rest of metadata may hold
1427
+ // host-private data and stays server-side. Absent → null.
1428
+ charId: decrypted.metadata?.charId ?? null,
1429
+ clientTaskId: decrypted.metadata?.amsgClientTaskId ?? null
1343
1430
  };
1344
1431
  }));
1345
1432
  const responsePayload = {
@@ -1439,6 +1526,9 @@ var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
1439
1526
  "created_at",
1440
1527
  "updated_at"
1441
1528
  ]);
1529
+ function escapeLikePrefix(prefix) {
1530
+ return prefix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
1531
+ }
1442
1532
  var D1Adapter = class {
1443
1533
  /** @param {{ prepare: (sql: string) => any }} db - Cloudflare D1 binding */
1444
1534
  constructor(db) {
@@ -1620,6 +1710,10 @@ var D1Adapter = class {
1620
1710
  * than the stored row (updatedAt strictly lower) is skipped; equal or
1621
1711
  * newer overwrites. Values arrive pre-encrypted (the handler encrypts).
1622
1712
  *
1713
+ * `cleanups` 是分块存储的清理项(见 lib/state-chunks.js):在同一 batch 里
1714
+ * 先于 upsert 执行,按 (namespace, key 前缀) 删掉旧写入留下的切片行;
1715
+ * `updated_at <= ?` 条件保证陈旧批次删不动更新写入的行。
1716
+ *
1623
1717
  * Uses D1's batch() — one network round trip for the whole set (implicit
1624
1718
  * transaction). The client calls this endpoint inside its few-seconds
1625
1719
  * background window, so N sequential round trips could eat the whole
@@ -1628,36 +1722,44 @@ var D1Adapter = class {
1628
1722
  *
1629
1723
  * @param {string} userId
1630
1724
  * @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
1631
- * @returns {Promise<{ upserted: number, skipped: number }>}
1725
+ * @param {Array<{ namespace: string, keyPrefix: string, updatedAt: number }>} [cleanups]
1726
+ * @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[] }>}
1727
+ * `outcomes[i]` 对应 entries[i] 是否真的写入(changes > 0)。
1632
1728
  */
1633
- async upsertClientState(userId, entries) {
1729
+ async upsertClientState(userId, entries, cleanups = []) {
1634
1730
  const UPSERT_SQL = `INSERT INTO client_state (user_id, namespace, key, value, updated_at)
1635
1731
  VALUES (?, ?, ?, ?, ?)
1636
1732
  ON CONFLICT (user_id, namespace, key) DO UPDATE SET
1637
1733
  value = excluded.value,
1638
1734
  updated_at = excluded.updated_at
1639
1735
  WHERE excluded.updated_at >= client_state.updated_at`;
1736
+ const CLEANUP_SQL = `DELETE FROM client_state
1737
+ WHERE user_id = ? AND namespace = ? AND key LIKE ? ESCAPE '\\' AND updated_at <= ?`;
1738
+ const buildStatements = () => [
1739
+ ...cleanups.map(
1740
+ (c) => this._db.prepare(CLEANUP_SQL).bind(userId, c.namespace, `${escapeLikePrefix(c.keyPrefix)}%`, c.updatedAt)
1741
+ ),
1742
+ ...entries.map(
1743
+ (entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt)
1744
+ )
1745
+ ];
1640
1746
  let results;
1641
1747
  if (typeof this._db.batch === "function") {
1642
- const statements = entries.map(
1643
- (entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt)
1644
- );
1645
- results = await this._db.batch(statements);
1748
+ results = await this._db.batch(buildStatements());
1646
1749
  } else {
1647
1750
  results = [];
1648
- for (const entry of entries) {
1649
- results.push(
1650
- await this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt).run()
1651
- );
1751
+ for (const stmt of buildStatements()) {
1752
+ results.push(await stmt.run());
1652
1753
  }
1653
1754
  }
1755
+ const outcomes = results.slice(cleanups.length).map((res) => res.meta.changes > 0);
1654
1756
  let upserted = 0;
1655
1757
  let skipped = 0;
1656
- for (const res of results) {
1657
- if (res.meta.changes > 0) upserted++;
1758
+ for (const wrote of outcomes) {
1759
+ if (wrote) upserted++;
1658
1760
  else skipped++;
1659
1761
  }
1660
- return { upserted, skipped };
1762
+ return { upserted, skipped, outcomes };
1661
1763
  }
1662
1764
  /**
1663
1765
  * All entries of one namespace (values still encrypted).
@@ -1799,7 +1901,6 @@ function createVapidPublicKeyHandler(ctx) {
1799
1901
  }
1800
1902
 
1801
1903
  // src/server/handlers/client-state.js
1802
- var MAX_STATE_VALUE_BYTES = 200 * 1024;
1803
1904
  var MAX_STATE_ENTRIES_PER_REQUEST = 200;
1804
1905
  var MAX_NAMESPACE_CHARS = 128;
1805
1906
  var MAX_KEY_CHARS = 256;
@@ -1814,23 +1915,39 @@ function requireUserId(headers) {
1814
1915
  if (!isValidUUIDv4(userId)) return { error: err(400, "INVALID_USER_ID_FORMAT", "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F") };
1815
1916
  return { userId };
1816
1917
  }
1817
- function validateEntry(entry, index) {
1818
- if (!isPlainObject(entry)) return err(400, "INVALID_STATE_ENTRY", `entries[${index}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
1918
+ function rejectEntry(entry, index, code, message, extra) {
1919
+ const rejection = { index, code, message, ...extra || {} };
1920
+ if (entry && typeof entry === "object") {
1921
+ if (typeof entry.namespace === "string") rejection.namespace = entry.namespace;
1922
+ if (typeof entry.key === "string") rejection.key = entry.key;
1923
+ }
1924
+ return rejection;
1925
+ }
1926
+ function validateEntry(entry, index, maxValueBytes) {
1927
+ if (!isPlainObject(entry)) {
1928
+ return rejectEntry(entry, index, "INVALID_STATE_ENTRY", `entries[${index}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
1929
+ }
1819
1930
  if (typeof entry.namespace !== "string" || !entry.namespace.trim() || entry.namespace.length > MAX_NAMESPACE_CHARS) {
1820
- return err(400, "INVALID_STATE_NAMESPACE", `entries[${index}].namespace \u5FC5\u987B\u662F 1-${MAX_NAMESPACE_CHARS} \u5B57\u7B26\u7684\u5B57\u7B26\u4E32`);
1931
+ return rejectEntry(entry, index, "INVALID_STATE_NAMESPACE", `entries[${index}].namespace \u5FC5\u987B\u662F 1-${MAX_NAMESPACE_CHARS} \u5B57\u7B26\u7684\u5B57\u7B26\u4E32`);
1932
+ }
1933
+ if (INTERNAL_STATE_CHAR_RE.test(entry.namespace)) {
1934
+ return rejectEntry(entry, index, "INVALID_STATE_NAMESPACE", `entries[${index}].namespace \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09`);
1821
1935
  }
1822
1936
  if (typeof entry.key !== "string" || !entry.key.trim() || entry.key.length > MAX_KEY_CHARS) {
1823
- return err(400, "INVALID_STATE_KEY", `entries[${index}].key \u5FC5\u987B\u662F 1-${MAX_KEY_CHARS} \u5B57\u7B26\u7684\u5B57\u7B26\u4E32`);
1937
+ return rejectEntry(entry, index, "INVALID_STATE_KEY", `entries[${index}].key \u5FC5\u987B\u662F 1-${MAX_KEY_CHARS} \u5B57\u7B26\u7684\u5B57\u7B26\u4E32`);
1938
+ }
1939
+ if (INTERNAL_STATE_CHAR_RE.test(entry.key)) {
1940
+ return rejectEntry(entry, index, "INVALID_STATE_KEY", `entries[${index}].key \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09`);
1824
1941
  }
1825
1942
  if (typeof entry.value !== "string") {
1826
- return err(400, "INVALID_STATE_VALUE", `entries[${index}].value \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u5BBF\u4E3B\u81EA\u884C\u5E8F\u5217\u5316\uFF09`);
1943
+ 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`);
1827
1944
  }
1828
1945
  const bytes = utf82.encode(entry.value).length;
1829
- if (bytes > MAX_STATE_VALUE_BYTES) {
1830
- return err(413, "STATE_VALUE_TOO_LARGE", `entries[${index}].value \u8D85\u8FC7\u5355\u6761\u4E0A\u9650`, { index, key: entry.key, bytes, maxBytes: MAX_STATE_VALUE_BYTES });
1946
+ if (bytes > maxValueBytes) {
1947
+ return rejectEntry(entry, index, "STATE_VALUE_TOO_LARGE", `entries[${index}].value \u8D85\u8FC7\u5355\u6761\u603B\u4E0A\u9650`, { bytes, maxBytes: maxValueBytes });
1831
1948
  }
1832
1949
  if (!Number.isInteger(entry.updatedAt) || entry.updatedAt <= 0) {
1833
- return err(400, "INVALID_STATE_UPDATED_AT", `entries[${index}].updatedAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF08epoch \u6BEB\u79D2\uFF09`);
1950
+ return rejectEntry(entry, index, "INVALID_STATE_UPDATED_AT", `entries[${index}].updatedAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF08epoch \u6BEB\u79D2\uFF09`);
1834
1951
  }
1835
1952
  return null;
1836
1953
  }
@@ -1868,21 +1985,70 @@ function createClientStateHandler(ctx) {
1868
1985
  if (entries.length > MAX_STATE_ENTRIES_PER_REQUEST) {
1869
1986
  return err(400, "TOO_MANY_STATE_ENTRIES", `\u5355\u6B21\u6700\u591A ${MAX_STATE_ENTRIES_PER_REQUEST} \u6761`, { count: entries.length });
1870
1987
  }
1988
+ const maxValueBytes = Number.isInteger(ctx.maxStateValueBytes) && ctx.maxStateValueBytes > 0 ? ctx.maxStateValueBytes : DEFAULT_MAX_STATE_VALUE_BYTES;
1989
+ const accepted = [];
1990
+ const rejected = [];
1871
1991
  for (let i = 0; i < entries.length; i++) {
1872
- const invalid = validateEntry(entries[i], i);
1873
- if (invalid) return invalid;
1992
+ const rejection = validateEntry(entries[i], i, maxValueBytes);
1993
+ if (rejection) rejected.push(rejection);
1994
+ else accepted.push(entries[i]);
1874
1995
  }
1875
1996
  if (typeof db.upsertClientState !== "function") {
1876
1997
  return err(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
1877
1998
  }
1878
- const encryptedEntries = await Promise.all(entries.map(async (entry) => ({
1879
- namespace: entry.namespace,
1880
- key: entry.key,
1881
- value: await encryptForStorage(entry.value, userKey),
1882
- updatedAt: entry.updatedAt
1883
- })));
1884
- const { upserted, skipped } = await db.upsertClientState(userId, encryptedEntries);
1885
- return { status: 200, body: { success: true, data: { upserted, skipped } } };
1999
+ const physicalRows = [];
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
+ }
2049
+ const data = { upserted, skipped };
2050
+ if (rejected.length > 0) data.rejected = rejected;
2051
+ return { status: 200, body: { success: true, data } };
1886
2052
  }
1887
2053
  async function GET(url, headers) {
1888
2054
  const tenantResult = await ctx.tenantManager.resolveTenant(headers);
@@ -1893,17 +2059,19 @@ function createClientStateHandler(ctx) {
1893
2059
  const { userId } = gate;
1894
2060
  const namespace = new URL(url, "https://dummy").searchParams.get("namespace") || "";
1895
2061
  if (!namespace.trim()) return err(400, "NAMESPACE_REQUIRED", "\u5FC5\u987B\u63D0\u4F9B namespace \u67E5\u8BE2\u53C2\u6570");
2062
+ if (INTERNAL_STATE_CHAR_RE.test(namespace)) {
2063
+ return err(400, "INVALID_STATE_NAMESPACE", "namespace \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09");
2064
+ }
1896
2065
  if (typeof db.getClientState !== "function") {
1897
2066
  return err(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
1898
2067
  }
1899
2068
  const userKey = await deriveUserEncryptionKey(userId, masterKey);
1900
2069
  const rows = await db.getClientState(userId, namespace);
1901
- const decrypted = await Promise.all(rows.map(async (row) => ({
1902
- namespace: row.namespace,
1903
- key: row.key,
1904
- value: await decryptFromStorage(row.value, userKey),
1905
- updatedAt: row.updated_at
1906
- })));
2070
+ const decrypted = await resolveClientStateEntries(
2071
+ rows,
2072
+ () => db.getClientState(userId, chunkNamespaceFor(namespace)),
2073
+ (value) => decryptFromStorage(value, userKey)
2074
+ );
1907
2075
  const encryptedResponse = await encryptPayload({ namespace, entries: decrypted }, userKey);
1908
2076
  return { status: 200, body: { success: true, encrypted: true, version: 1, data: encryptedResponse } };
1909
2077
  }
@@ -1923,6 +2091,33 @@ function createClientStateHandler(ctx) {
1923
2091
  return { PUT, GET, DELETE };
1924
2092
  }
1925
2093
 
2094
+ // src/server/lib/version.js
2095
+ var SERVER_VERSION = true ? "2.6.0-next.5" : "0.0.0-dev";
2096
+
2097
+ // src/server/handlers/capabilities.js
2098
+ var SERVER_FEATURES = Object.freeze([
2099
+ "client-state",
2100
+ "client-state-chunking",
2101
+ "client-state-partial-failure",
2102
+ "agentic-hooks",
2103
+ "agentic-scratch",
2104
+ "vapid-public-key"
2105
+ ]);
2106
+ function createCapabilitiesHandler(ctx) {
2107
+ async function GET(url, headers) {
2108
+ const effectiveHeaders = headers || url || {};
2109
+ const tenantResult = await ctx.tenantManager.resolveTenant(effectiveHeaders);
2110
+ if (!tenantResult.ok) {
2111
+ return tenantResult.error;
2112
+ }
2113
+ return {
2114
+ status: 200,
2115
+ body: { success: true, serverVersion: SERVER_VERSION, features: [...SERVER_FEATURES] }
2116
+ };
2117
+ }
2118
+ return { GET };
2119
+ }
2120
+
1926
2121
  // src/server/single-user.js
1927
2122
  function createSingleUserServer(config) {
1928
2123
  if (!config || !config.db) throw new Error("[amsg-server single-user] config.db is required");
@@ -1946,7 +2141,8 @@ function createSingleUserServer(config) {
1946
2141
  // the agentic path too.
1947
2142
  hooks: config.hooks || null,
1948
2143
  maxToolIterations: config.maxToolIterations,
1949
- totalTimeoutMs: config.totalTimeoutMs
2144
+ totalTimeoutMs: config.totalTimeoutMs,
2145
+ maxStateValueBytes: config.maxStateValueBytes
1950
2146
  };
1951
2147
  return {
1952
2148
  ctx,
@@ -1958,7 +2154,8 @@ function createSingleUserServer(config) {
1958
2154
  cancelMessage: createCancelMessageHandler(ctx),
1959
2155
  messages: createMessagesHandler(ctx),
1960
2156
  vapidPublicKey: createVapidPublicKeyHandler(ctx),
1961
- clientState: createClientStateHandler(ctx)
2157
+ clientState: createClientStateHandler(ctx),
2158
+ capabilities: createCapabilitiesHandler(ctx)
1962
2159
  }
1963
2160
  };
1964
2161
  }
@@ -2224,6 +2421,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
2224
2421
  result = await server.handlers.cancelMessage.DELETE(url, headers);
2225
2422
  } else if (method === "GET" && pathname.endsWith("/vapid-public-key")) {
2226
2423
  result = await server.handlers.vapidPublicKey.GET(url, headers);
2424
+ } else if (method === "GET" && pathname.endsWith("/capabilities")) {
2425
+ result = await server.handlers.capabilities.GET(url, headers);
2227
2426
  } else if (method === "PUT" && pathname.endsWith("/client-state")) {
2228
2427
  result = await server.handlers.clientState.PUT(headers, await request.text());
2229
2428
  } else if (method === "GET" && pathname.endsWith("/client-state")) {