openmausbot 0.1.91 → 0.1.92

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.
Files changed (150) hide show
  1. package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
  3. package/dist/assets/{arc-DsivfZEh.js → arc-DIYc7Ymx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
  7. package/dist/assets/channel-Dtk5Mtic.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-v6bj852W.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
  34. package/dist/assets/index-Cdmjrl28.css +1 -0
  35. package/dist/assets/{index-DKNEM8Q6.js → index-DGBaEXrM.js} +1 -1
  36. package/dist/assets/index-DR7YPMkW.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
  41. package/dist/assets/{linear-yC_29R6e.js → linear-CmOWzX0P.js} +1 -1
  42. package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-CF1zwoQ-.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist/novnc-NOTICE.txt +118 -0
  62. package/dist-server/cloud-home-start.js +112 -14
  63. package/dist-server/container-mcp.js +195 -15
  64. package/dist-server/drivers/agents-proxy.js +5 -4
  65. package/dist-server/enterprise/server/index.js +5 -1
  66. package/dist-server/index.js +9300 -4155
  67. package/dist-server/local-computer-proxy.js +128 -7
  68. package/dist-server/mcp-gate.js +11 -5
  69. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  70. package/dist-server/openmausbot.js +3910 -1182
  71. package/dist-server/pair-cli.js +3910 -1182
  72. package/dist-server/server/boat.js +2 -2
  73. package/dist-server/server/browser-engine.js +10 -1
  74. package/dist-server/server/browser-live.js +46 -2
  75. package/dist-server/server/browser-runtime.js +74 -8
  76. package/dist-server/server/browser-tool-shape.js +6 -4
  77. package/dist-server/server/chief-of-staff.js +1 -1
  78. package/dist-server/server/cloud-home-start.js +118 -26
  79. package/dist-server/server/cloud-home.js +39 -3
  80. package/dist-server/server/cloud-lending.js +74 -10
  81. package/dist-server/server/cloud-owner.js +196 -0
  82. package/dist-server/server/cloud-secrets-boot.js +6 -0
  83. package/dist-server/server/cloud-secrets.js +65 -0
  84. package/dist-server/server/config.js +174 -14
  85. package/dist-server/server/container-computer.js +3 -2
  86. package/dist-server/server/desktop-viewer-targets.js +26 -0
  87. package/dist-server/server/drivers/acp/core.js +167 -21
  88. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  89. package/dist-server/server/drivers/agents-catalog.js +6 -6
  90. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  91. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  92. package/dist-server/server/drivers/antigravity.js +21 -0
  93. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  94. package/dist-server/server/drivers/claude.js +312 -34
  95. package/dist-server/server/drivers/codex.js +228 -18
  96. package/dist-server/server/drivers/grok.js +2 -2
  97. package/dist-server/server/drivers/mistral.js +4 -3
  98. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  99. package/dist-server/server/drivers/openai-chat.js +41 -3
  100. package/dist-server/server/drivers/openai-compat.js +73 -23
  101. package/dist-server/server/engine-install.js +46 -12
  102. package/dist-server/server/env-path.js +15 -3
  103. package/dist-server/server/harness/registry.js +2 -2
  104. package/dist-server/server/index.js +1338 -184
  105. package/dist-server/server/lending-memory.js +479 -0
  106. package/dist-server/server/local-vm-idle.js +24 -6
  107. package/dist-server/server/managed-desktop.js +3 -0
  108. package/dist-server/server/mcp-bridge.js +5 -3
  109. package/dist-server/server/mcp-http.js +6 -3
  110. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  111. package/dist-server/server/mcp-oauth-store.js +70 -0
  112. package/dist-server/server/mcp-oauth.js +550 -0
  113. package/dist-server/server/mcp-probe.js +14 -1
  114. package/dist-server/server/mcp-registry.js +43 -0
  115. package/dist-server/server/memory-upkeep.js +94 -84
  116. package/dist-server/server/model-catalog/catalog.js +374 -0
  117. package/dist-server/server/model-catalog/presets.js +176 -0
  118. package/dist-server/server/model-catalog/trim.js +219 -0
  119. package/dist-server/server/provider-key-check.js +4 -2
  120. package/dist-server/server/recent-work.js +1 -1
  121. package/dist-server/server/request-auth.js +23 -0
  122. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  123. package/dist-server/server/routes/bot-memory.js +31 -6
  124. package/dist-server/server/routes/desktop-viewer.js +194 -0
  125. package/dist-server/server/routine-requests.js +1 -1
  126. package/dist-server/server/routines.js +1 -1
  127. package/dist-server/server/sessions.js +56 -9
  128. package/dist-server/server/skills.js +25 -10
  129. package/dist-server/server/store.js +62 -122
  130. package/dist-server/server/thread-events.js +2 -0
  131. package/dist-server/server/thread-starters.js +33 -0
  132. package/dist-server/server/thread-title.js +46 -0
  133. package/dist-server/server/tts/fish.js +4 -5
  134. package/dist-server/server/tts/index.js +5 -3
  135. package/dist-server/server/vps-computer.js +79 -12
  136. package/dist-server/server/webhooks.js +18 -1
  137. package/dist-server/server/workspace-backup-policy.js +3 -0
  138. package/dist-server/server/workspace-backup.js +3 -0
  139. package/dist-server/server/workspace.js +65 -15
  140. package/dist-server/shared/workspace-backup-client.js +1 -0
  141. package/dist-server/src/lib/citations.js +177 -0
  142. package/dist-server/src/lib/composer-attachments.js +7 -1
  143. package/dist-server/vps-container-mcp.js +196 -15
  144. package/dist-server/workspace-backup.worker.js +73 -9
  145. package/enterprise/server/index.js +5 -1
  146. package/package.json +1 -1
  147. package/dist/assets/channel-DrfGMu7X.js +0 -1
  148. package/dist/assets/index-BlnCEcU6.js +0 -588
  149. package/dist/assets/index-DVxjOvID.css +0 -1
  150. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +0 -8
@@ -51,6 +51,10 @@ const storedWebhookSchema = z.object({
51
51
  updatedAt: z.number().finite().nonnegative(),
52
52
  lastReceivedAt: z.number().finite().nonnegative().optional(),
53
53
  lastRunId: z.string().optional(),
54
+ /** delivery:"post" only: the stable destination thread, resolved once
55
+ * via WebhookManagerOptions.resolvePostThread and reused forever
56
+ * after -- see the `post`/`resolvePostThread` doc comments above. */
57
+ resultsThreadId: z.string().min(1).optional(),
54
58
  deliveryCount: z.number().int().nonnegative(),
55
59
  verificationPending: z.boolean().optional(),
56
60
  verifiedAt: z.number().finite().nonnegative().optional(),
@@ -429,9 +433,22 @@ export class WebhookManager {
429
433
  // without a post sink is a configuration error, not a run request.
430
434
  if (!this.options.post)
431
435
  fail(503, "This server cannot post webhook messages to chat");
436
+ // Resolved on every dispatch, exactly like routines.ts's newRun calls
437
+ // resolveResultsThread on every run -- the resolver owns the
438
+ // reuse-vs-allocate decision from trigger.resultsThreadId, so a host
439
+ // without resolvePostThread wired can only fail closed here, never
440
+ // silently fall back to whatever thread the bot happens to have
441
+ // selected right now (the bug this replaces -- issue #2071).
442
+ const threadId = this.options.resolvePostThread?.(trigger, false);
443
+ if (!threadId)
444
+ fail(503, "This server could not resolve a destination thread for this delivery");
445
+ if (threadId !== trigger.resultsThreadId) {
446
+ trigger.resultsThreadId = threadId;
447
+ trigger.updatedAt = now;
448
+ }
432
449
  const raw = event.payload;
433
450
  const text = raw && typeof raw === "object" && typeof raw.text === "string" && raw.text.trim() ? raw.text : serializePayload(event.payload);
434
- this.options.post(trigger.botId, text.slice(0, 20000));
451
+ this.options.post(trigger.botId, threadId, text.slice(0, 20000));
435
452
  this.deliveries.push({ key: `${trigger.endpointId}:${deliveryId}`, runId: "post", at: now });
436
453
  if (this.deliveries.length > MAX_DELIVERIES)
437
454
  this.deliveries.splice(0, this.deliveries.length - MAX_DELIVERIES);
@@ -41,6 +41,9 @@ export function excludedWorkspaceAuthPath(path) {
41
41
  // Machine/provider-specific execution grants are not portable template data.
42
42
  if (/^command-allowlist\.json(?:$|\.\d+\.[0-9a-f-]+\.tmp$)/.test(path))
43
43
  return true;
44
+ // MCP sign-in tokens belong to this machine's browser sign-ins.
45
+ if (/^mcp-oauth\.json(?:$|\.\d+\.[0-9a-f-]+\.tmp$)/.test(path))
46
+ return true;
44
47
  return /^(?:(?:providers|caddy|chrome-profile|\.agent-browser)(?:\/|$)|workspace-credentials\.json$|external-runtimes\.json$|browser-engine-key$)/.test(path) ||
45
48
  /^(?:config\.json|webhooks\.json|workspace-credentials\.json|external-runtimes\.json|browser-engine-key|sessions\.json|tunnel-account\.json)\.\d+(?:\.[0-9a-f-]+)?\.tmp$/.test(path) ||
46
49
  /^(?:vm-home|vm-homes\/[^/]+)\/\.browser-profiles(?:\/|$)/.test(path);
@@ -29,6 +29,9 @@ const EXCLUDED = new Set([
29
29
  // This machine's decision-model log (server/decider/log.ts): local
30
30
  // measurement of what the classifier picked, not workspace data.
31
31
  "decider-log",
32
+ // A Cloud home's record of who its owner was (server/cloud-owner.ts): this
33
+ // machine's own, kept in place by a restore, which settles what it brought.
34
+ "cloud-owner.json",
32
35
  ]);
33
36
  const EXCLUSION_NOTES = [
34
37
  "Device pairing, server identity, live leases and runtime files (existing destination identities are preserved).",
@@ -8,7 +8,7 @@
8
8
  // memory/ holds topic files the bot reads on demand with its ordinary
9
9
  // file tools. Plain markdown on purpose — the user can open, edit, or
10
10
  // delete anything the bot believes.
11
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
11
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, statSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import { writeFileAtomic } from "./atomic.js";
14
14
  import { isMemoryDate, untilMark, withoutExpired } from "./memory-entries.js";
@@ -58,6 +58,53 @@ export function ensureWorkspace(botId) {
58
58
  export function workspaceDir(botId) {
59
59
  return join(WORKSPACES_DIR, botId);
60
60
  }
61
+ /** On an OMB Cloud home a bot's memory is read only from regular files,
62
+ * never through a link: server/lending-memory.ts judges a link by where it
63
+ * points, not by what is there, so a turn must never read through one.
64
+ * Elsewhere memory reads exactly as it always did. */
65
+ let regularMemoryFilesOnly = false;
66
+ export function readMemoryOnlyFromRegularFiles() {
67
+ regularMemoryFilesOnly = true;
68
+ }
69
+ /** A memory file's text. Throws like readFileSync when it cannot be read,
70
+ * and, on a Cloud home, when it is not a regular file. */
71
+ export function readMemoryText(path) {
72
+ if (!regularMemoryFilesOnly)
73
+ return readFileSync(path, "utf8");
74
+ // O_NOFOLLOW refuses a link at open where there is one; Windows has none
75
+ // (and opens what a link points at), so the entry is looked at first too.
76
+ if (!lstatSync(path).isFile())
77
+ throw Object.assign(new Error(`not a regular file: ${path}`), { code: "EINVAL" });
78
+ const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
79
+ try {
80
+ if (!fstatSync(fd).isFile())
81
+ throw Object.assign(new Error(`not a regular file: ${path}`), { code: "EINVAL" });
82
+ return readFileSync(fd, "utf8");
83
+ }
84
+ finally {
85
+ closeSync(fd);
86
+ }
87
+ }
88
+ /** A memory folder's entries; on a Cloud home, none when the folder itself
89
+ * is a link. */
90
+ function memoryFolderEntries(dir) {
91
+ if (regularMemoryFilesOnly && !lstatSync(dir).isDirectory())
92
+ return [];
93
+ return readdirSync(dir);
94
+ }
95
+ function regularFile(path) {
96
+ try {
97
+ return lstatSync(path).isFile();
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ /** The stat a listing trusts: on a Cloud home, the entry's own (a link is
104
+ * not a file); elsewhere what it points at, as before. */
105
+ function memoryEntryStat(path) {
106
+ return regularMemoryFilesOnly ? lstatSync(path) : statSync(path);
107
+ }
61
108
  /** File locations, not file contents or wider tool permissions. Threads keep
62
109
  * independent working directories; the same bot can find its earlier output
63
110
  * without assuming that a file absent from the current directory was lost. */
@@ -89,7 +136,7 @@ export function memoryOverBudget(text) {
89
136
  export function loadMemory(botId, opts = {}) {
90
137
  let raw;
91
138
  try {
92
- raw = readFileSync(join(workspaceDir(botId), "MEMORY.md"), "utf8");
139
+ raw = readMemoryText(join(workspaceDir(botId), "MEMORY.md"));
93
140
  }
94
141
  catch {
95
142
  return null;
@@ -128,7 +175,7 @@ export const MEMORY_FILE_MAX_BYTES = 256 * 1024;
128
175
  export function readMemoryFile(botId) {
129
176
  let raw;
130
177
  try {
131
- raw = readFileSync(join(workspaceDir(botId), "MEMORY.md"), "utf8");
178
+ raw = readMemoryText(join(workspaceDir(botId), "MEMORY.md"));
132
179
  }
133
180
  catch {
134
181
  return { text: "", truncated: false };
@@ -159,7 +206,7 @@ function indexWrittenMemoryFile(botId, relativePath) {
159
206
  try {
160
207
  const path = join(workspaceDir(botId), relativePath);
161
208
  const stat = statSync(path);
162
- indexMemoryFile(botId, relativePath, searchableMemoryText(relativePath, readFileSync(path, "utf8")), { mtimeMs: stat.mtimeMs, bytes: stat.size });
209
+ indexMemoryFile(botId, relativePath, searchableMemoryText(relativePath, readMemoryText(path)), { mtimeMs: stat.mtimeMs, bytes: stat.size });
163
210
  }
164
211
  catch {
165
212
  // the next search's sync pass picks it up
@@ -176,7 +223,7 @@ function memoryFilesOnDisk(botId) {
176
223
  ];
177
224
  return names.flatMap((relativePath) => {
178
225
  try {
179
- const stat = statSync(join(dir, relativePath));
226
+ const stat = memoryEntryStat(join(dir, relativePath));
180
227
  return stat.isFile() ? [{ path: relativePath, mtimeMs: stat.mtimeMs, bytes: stat.size }] : [];
181
228
  }
182
229
  catch {
@@ -206,7 +253,7 @@ export function syncMemoryIndex(botId) {
206
253
  if (!dateChanged && known && known.bytes === file.bytes && known.mtimeMs === Math.trunc(file.mtimeMs))
207
254
  continue;
208
255
  try {
209
- const text = readFileSync(join(workspaceDir(botId), file.path), "utf8");
256
+ const text = readMemoryText(join(workspaceDir(botId), file.path));
210
257
  indexMemoryFile(botId, file.path, searchableMemoryText(file.path, text), file);
211
258
  }
212
259
  catch {
@@ -318,7 +365,7 @@ export function updateMemory(botId, update, opts = {}) {
318
365
  const dir = ensureWorkspace(botId);
319
366
  // Do not use readMemoryFile's editor-friendly missing/read-error fallback:
320
367
  // a failed read must never turn into a successful overwrite of old notes.
321
- const raw = readFileSync(join(dir, "MEMORY.md"), "utf8");
368
+ const raw = readMemoryText(join(dir, "MEMORY.md"));
322
369
  const current = raw === MEMORY_SEED ? "" : raw;
323
370
  const today = memoryDate(opts.now);
324
371
  // Every appended entry ends its own line; a file the person left without
@@ -419,7 +466,7 @@ export function appendMemoryLog(botId, text, opts = {}) {
419
466
  const path = join(dir, file);
420
467
  let current = "";
421
468
  try {
422
- current = readFileSync(path, "utf8");
469
+ current = readMemoryText(path);
423
470
  }
424
471
  catch {
425
472
  // first line of the day
@@ -442,7 +489,8 @@ export function writeMemoryLog(botId, name, text) {
442
489
  /** The bot's daily log files, oldest first, by day name. */
443
490
  export function listMemoryLogs(botId) {
444
491
  try {
445
- return readdirSync(join(workspaceDir(botId), "memory", MEMORY_LOG_DIR)).filter((name) => LOG_FILE_NAME.test(name)).sort();
492
+ const dir = join(workspaceDir(botId), "memory", MEMORY_LOG_DIR);
493
+ return memoryFolderEntries(dir).filter((name) => LOG_FILE_NAME.test(name) && (!regularMemoryFilesOnly || regularFile(join(dir, name)))).sort();
446
494
  }
447
495
  catch {
448
496
  return [];
@@ -453,7 +501,7 @@ export function readMemoryLog(botId, name) {
453
501
  if (!LOG_FILE_NAME.test(name))
454
502
  return null;
455
503
  try {
456
- return readFileSync(join(workspaceDir(botId), "memory", MEMORY_LOG_DIR, name), "utf8");
504
+ return readMemoryText(join(workspaceDir(botId), "memory", MEMORY_LOG_DIR, name));
457
505
  }
458
506
  catch {
459
507
  return null;
@@ -472,7 +520,7 @@ export function isMemoryTopicName(name) {
472
520
  export function listMemoryTopics(botId) {
473
521
  let entries;
474
522
  try {
475
- entries = readdirSync(join(workspaceDir(botId), "memory"));
523
+ entries = memoryFolderEntries(join(workspaceDir(botId), "memory"));
476
524
  }
477
525
  catch {
478
526
  return [];
@@ -481,7 +529,7 @@ export function listMemoryTopics(botId) {
481
529
  .filter(isMemoryTopicName)
482
530
  .flatMap((name) => {
483
531
  try {
484
- const stat = statSync(join(workspaceDir(botId), "memory", name));
532
+ const stat = memoryEntryStat(join(workspaceDir(botId), "memory", name));
485
533
  return stat.isFile() ? [{ name, bytes: stat.size }] : [];
486
534
  }
487
535
  catch {
@@ -507,7 +555,7 @@ export function readMemoryTopic(botId, name) {
507
555
  if (!isMemoryTopicName(name))
508
556
  return null;
509
557
  try {
510
- return readFileSync(join(workspaceDir(botId), "memory", name), "utf8");
558
+ return readMemoryText(join(workspaceDir(botId), "memory", name));
511
559
  }
512
560
  catch {
513
561
  return null;
@@ -541,12 +589,14 @@ export function memoryTopicIndex(botId) {
541
589
  const dir = join(workspaceDir(botId), "memory");
542
590
  return renderTopicIndex(listMemoryTopics(botId).map((topic) => ({ name: topic.name, header: parseTopicHeader(readTopicHead(join(dir, topic.name))) })));
543
591
  }
544
- /** The memory block appended to a bot's system prompt. Always present for
545
- * bots with a workspace, so the bot knows the mechanism exists even before
592
+ /** The memory block appended to a bot's system prompt when enabled. Present for
593
+ * enabled bots with a workspace, so the bot knows the mechanism exists even before
546
594
  * it has written anything. Content from other bots or imported files must
547
595
  * never be recorded as fact — memory is a prompt-injection persistence
548
596
  * vector the moment a bot copies untrusted text into it. */
549
597
  export function memorySystemPrompt(botId, opts = {}) {
598
+ if (opts.enabled === false)
599
+ return "";
550
600
  const memory = loadMemory(botId);
551
601
  const memoryFile = join(workspaceDir(botId), "MEMORY.md");
552
602
  const topicDir = join(workspaceDir(botId), "memory");
@@ -7,6 +7,7 @@ export const WORKSPACE_BACKUP_CLIENT_KEYS = [
7
7
  "omb-draft-channel-modes",
8
8
  "omb-skin",
9
9
  "omb-show-threads",
10
+ "omb-show-run-card",
10
11
  "openmausbot.sidebarDensity",
11
12
  "openmausbot.sidebarCollapsedSections.v1",
12
13
  "openmausbot.sidebarSectionOrder.v1",
@@ -0,0 +1,177 @@
1
+ // Selection matching is adapted from T3 Code's assistantTextSelection.ts
2
+ // (Copyright 2026 T3 Tools Inc., MIT License; see third_party/t3-code/LICENSE).
3
+ export const CITATION_MAX_QUOTE_LENGTH = 12_000;
4
+ export const CITATION_MAX_COMMENT_LENGTH = 4_000;
5
+ const CITATION_CONTEXT_LENGTH = 64;
6
+ const CITATION_MARKER = "<!--omb-citation-v1:";
7
+ const CITATION_MARKER_RE = /<!--omb-citation-v1:([A-Za-z0-9_-]{1,100000})-->/g;
8
+ function boundedString(value, max, allowEmpty = false) {
9
+ return typeof value === "string" && value.length <= max && (allowEmpty || value.length > 0);
10
+ }
11
+ export function isCitationAttachment(value) {
12
+ if (!value || typeof value !== "object")
13
+ return false;
14
+ const citation = value;
15
+ const source = citation.source;
16
+ return citation.kind === "citation" &&
17
+ citation.version === 1 &&
18
+ boundedString(citation.id, 200) &&
19
+ boundedString(citation.quote, CITATION_MAX_QUOTE_LENGTH) &&
20
+ (citation.comment === undefined || boundedString(citation.comment, CITATION_MAX_COMMENT_LENGTH, true)) &&
21
+ typeof citation.size === "number" && Number.isFinite(citation.size) && citation.size >= 0 &&
22
+ Boolean(source) &&
23
+ (source.ownerType === "bot" || source.ownerType === "group") &&
24
+ boundedString(source.ownerId, 200) &&
25
+ boundedString(source.threadId, 200) &&
26
+ boundedString(source.messageId, 200) &&
27
+ Number.isSafeInteger(source.start) && Number(source.start) >= 0 &&
28
+ Number.isSafeInteger(source.end) && Number(source.end) >= Number(source.start) &&
29
+ boundedString(source.prefix, CITATION_CONTEXT_LENGTH, true) &&
30
+ boundedString(source.suffix, CITATION_CONTEXT_LENGTH, true);
31
+ }
32
+ function newId() {
33
+ return globalThis.crypto?.randomUUID?.() ?? `c${Math.random().toString(36).slice(2)}`;
34
+ }
35
+ function byteLength(text) {
36
+ return new TextEncoder().encode(text).length;
37
+ }
38
+ export function citationAttachment(source, selector, comment = "") {
39
+ const trimmedComment = comment.trim();
40
+ if (!selector.text.trim() || selector.text.length > CITATION_MAX_QUOTE_LENGTH) {
41
+ throw new Error("Citation quote is empty or too long");
42
+ }
43
+ if (trimmedComment.length > CITATION_MAX_COMMENT_LENGTH)
44
+ throw new Error("Citation comment is too long");
45
+ return {
46
+ kind: "citation",
47
+ version: 1,
48
+ id: newId(),
49
+ quote: selector.text,
50
+ ...(trimmedComment ? { comment: trimmedComment } : {}),
51
+ source: { ...source, start: selector.start, end: selector.end, prefix: selector.prefix, suffix: selector.suffix },
52
+ size: byteLength(selector.text) + byteLength(trimmedComment),
53
+ };
54
+ }
55
+ export function withCitationComment(citation, comment) {
56
+ const trimmed = comment.trim();
57
+ if (trimmed.length > CITATION_MAX_COMMENT_LENGTH)
58
+ throw new Error("Citation comment is too long");
59
+ const { comment: _old, ...rest } = citation;
60
+ return { ...rest, ...(trimmed ? { comment: trimmed } : {}), size: byteLength(citation.quote) + byteLength(trimmed) };
61
+ }
62
+ function base64UrlEncode(value) {
63
+ const bytes = new TextEncoder().encode(JSON.stringify(value));
64
+ let binary = "";
65
+ for (const byte of bytes)
66
+ binary += String.fromCharCode(byte);
67
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
68
+ }
69
+ function base64UrlDecode(value) {
70
+ const padded = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
71
+ const binary = atob(padded);
72
+ return JSON.parse(new TextDecoder().decode(Uint8Array.from(binary, (character) => character.charCodeAt(0))));
73
+ }
74
+ /** Readable in clients without citation UI and unambiguous to model providers. */
75
+ export function citationFallback(citation) {
76
+ const quote = citation.quote.split("\n").map((line) => `> ${line}`).join("\n");
77
+ return `> Quoted message:\n${quote}${citation.comment ? `\n\nComment:\n${citation.comment}` : ""}`;
78
+ }
79
+ export function serializeCitation(citation) {
80
+ return `${CITATION_MARKER}${base64UrlEncode(citation)}-->\n${citationFallback(citation)}`;
81
+ }
82
+ export function splitTranscriptCitations(text) {
83
+ const citations = [];
84
+ let display = "";
85
+ let cursor = 0;
86
+ for (const match of text.matchAll(CITATION_MARKER_RE)) {
87
+ if (match.index < cursor)
88
+ continue;
89
+ let citation;
90
+ try {
91
+ const decoded = base64UrlDecode(match[1]);
92
+ if (!isCitationAttachment(decoded))
93
+ continue;
94
+ citation = decoded;
95
+ }
96
+ catch {
97
+ continue;
98
+ }
99
+ const block = `${match[0]}\n${citationFallback(citation)}`;
100
+ if (!text.startsWith(block, match.index))
101
+ continue;
102
+ display += text.slice(cursor, match.index);
103
+ cursor = match.index + block.length;
104
+ citations.push(citation);
105
+ }
106
+ return { display: `${display}${text.slice(cursor)}`.trim(), citations };
107
+ }
108
+ export function citationPreviewText(text) {
109
+ const cited = splitTranscriptCitations(text);
110
+ if (!cited.citations.length)
111
+ return text;
112
+ return [
113
+ cited.display,
114
+ ...cited.citations.map((citation) => citation.comment
115
+ ? `${citation.quote} — ${citation.comment}`
116
+ : citation.quote),
117
+ ].filter(Boolean).join(" ");
118
+ }
119
+ function normalizeWhitespace(text) {
120
+ return text.replace(/\s+/g, " ");
121
+ }
122
+ function splitsSurrogatePair(text, offset) {
123
+ const before = text.charCodeAt(offset - 1);
124
+ const after = text.charCodeAt(offset);
125
+ return before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff;
126
+ }
127
+ export function createCitationTextSelector(text, rawStart, rawEnd) {
128
+ const quote = text.slice(rawStart, rawEnd);
129
+ if (!quote.trim())
130
+ return null;
131
+ const normalized = normalizeWhitespace(text);
132
+ let start = normalizeWhitespace(text.slice(0, rawStart)).length;
133
+ if (rawStart > 0 && /\s/.test(text[rawStart - 1]) && /\s/.test(text[rawStart]))
134
+ start -= 1;
135
+ const end = normalizeWhitespace(text.slice(0, rawEnd)).length;
136
+ let prefixStart = Math.max(0, start - CITATION_CONTEXT_LENGTH);
137
+ let suffixEnd = Math.min(normalized.length, end + CITATION_CONTEXT_LENGTH);
138
+ if (splitsSurrogatePair(normalized, prefixStart))
139
+ prefixStart += 1;
140
+ if (splitsSurrogatePair(normalized, suffixEnd))
141
+ suffixEnd -= 1;
142
+ return {
143
+ text: quote,
144
+ start,
145
+ end,
146
+ prefix: normalized.slice(prefixStart, start),
147
+ suffix: normalized.slice(end, suffixEnd),
148
+ };
149
+ }
150
+ export function findCitationText(text, selector) {
151
+ const normalized = normalizeWhitespace(text);
152
+ const quote = normalizeWhitespace(selector.text);
153
+ if (!quote.trim())
154
+ return null;
155
+ const prefix = normalizeWhitespace(selector.prefix);
156
+ const suffix = normalizeWhitespace(selector.suffix);
157
+ const matchesContext = (start, end) => normalized.slice(Math.max(0, start - prefix.length), start) === prefix &&
158
+ normalized.slice(end, end + suffix.length) === suffix;
159
+ let match = Number.isSafeInteger(selector.start) && Number.isSafeInteger(selector.end) &&
160
+ selector.start >= 0 && selector.end - selector.start === quote.length &&
161
+ normalized.slice(selector.start, selector.end) === quote && matchesContext(selector.start, selector.end)
162
+ ? { start: selector.start, end: selector.end }
163
+ : null;
164
+ let onlyQuote = null;
165
+ let quoteCount = 0;
166
+ for (let start = normalized.indexOf(quote); start !== -1; start = normalized.indexOf(quote, start + 1)) {
167
+ const end = start + quote.length;
168
+ quoteCount += 1;
169
+ onlyQuote = { start, end };
170
+ if (!matchesContext(start, end))
171
+ continue;
172
+ if (match && match.start !== start)
173
+ return null;
174
+ match = { start, end };
175
+ }
176
+ return match ?? (quoteCount === 1 ? onlyQuote : null);
177
+ }
@@ -1,7 +1,10 @@
1
+ import { isCitationAttachment, serializeCitation } from "./citations.js";
1
2
  export function isAttachment(value) {
2
3
  if (!value || typeof value !== "object")
3
4
  return false;
4
5
  const attachment = value;
6
+ if (attachment.kind === "citation")
7
+ return isCitationAttachment(value);
5
8
  if (typeof attachment.id !== "string" || !validSize(attachment.size))
6
9
  return false;
7
10
  if (attachment.kind === "paste") {
@@ -362,7 +365,10 @@ export function formatSize(bytes) {
362
365
  export function composeMessage(text, attachments) {
363
366
  const parts = [text.trim()];
364
367
  attachments.forEach((a, i) => {
365
- if (a.kind === "paste") {
368
+ if (a.kind === "citation") {
369
+ parts.push(serializeCitation(a));
370
+ }
371
+ else if (a.kind === "paste") {
366
372
  parts.push(`<pasted-text index="${i + 1}">\n${a.text}\n</pasted-text>`);
367
373
  }
368
374
  else if (a.kind === "image") {