openmausbot 0.1.89 → 0.1.91

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 (168) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
  2. package/dist/assets/{arc-Be-8GKxd.js → arc-DsivfZEh.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
  6. package/dist/assets/channel-DrfGMu7X.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
  33. package/dist/assets/index-BlnCEcU6.js +588 -0
  34. package/dist/assets/{index-Oxk9x45i.js → index-DKNEM8Q6.js} +1 -1
  35. package/dist/assets/index-DVxjOvID.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
  40. package/dist/assets/{linear-CtJnKe6A.js → linear-yC_29R6e.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-VhEMkSND.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/cloud-home-start.js +14810 -0
  61. package/dist-server/container-mcp.js +136 -36
  62. package/dist-server/drivers/agents-proxy.js +31 -10
  63. package/dist-server/index.js +48251 -44124
  64. package/dist-server/openmausbot.js +628 -292
  65. package/dist-server/pair-cli.js +628 -292
  66. package/dist-server/permission-proxy.js +1 -1
  67. package/dist-server/server/admission.js +46 -0
  68. package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
  69. package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
  70. package/dist-server/server/{box.js → boat.js} +381 -334
  71. package/dist-server/server/bot-overview.js +7 -4
  72. package/dist-server/server/bot-profile.js +7 -1
  73. package/dist-server/server/channel-queue.js +58 -18
  74. package/dist-server/server/checkpoints.js +98 -52
  75. package/dist-server/server/claim-idle.js +33 -0
  76. package/dist-server/server/cloud-backend.js +5 -5
  77. package/dist-server/server/cloud-home-start.js +121 -0
  78. package/dist-server/server/cloud-home.js +241 -0
  79. package/dist-server/server/cloud-lending.js +103 -0
  80. package/dist-server/server/cloud-move-http.js +268 -0
  81. package/dist-server/server/cloud-move.js +418 -0
  82. package/dist-server/server/cloud-overflow.js +167 -0
  83. package/dist-server/server/composio.js +5 -0
  84. package/dist-server/server/computer-observation.js +2 -2
  85. package/dist-server/server/config.js +156 -9
  86. package/dist-server/server/container-computer.js +13 -0
  87. package/dist-server/server/control-client.js +2 -2
  88. package/dist-server/server/decider/index.js +238 -0
  89. package/dist-server/server/decider/jev.js +183 -0
  90. package/dist-server/server/decider/log.js +62 -0
  91. package/dist-server/server/decider/relay.js +52 -0
  92. package/dist-server/server/decider/room-routing.js +88 -0
  93. package/dist-server/server/decider/types.js +5 -0
  94. package/dist-server/server/delegations.js +11 -3
  95. package/dist-server/server/drivers/acp/core.js +5 -4
  96. package/dist-server/server/drivers/agents-call.js +5 -1
  97. package/dist-server/server/drivers/agents-catalog.js +25 -3
  98. package/dist-server/server/drivers/{boxagent.js → boatagent.js} +30 -21
  99. package/dist-server/server/drivers/builtIn.js +2 -2
  100. package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
  101. package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
  102. package/dist-server/server/drivers/claude.js +178 -13
  103. package/dist-server/server/drivers/native.js +1 -1
  104. package/dist-server/server/drivers/openai-chat.js +15 -4
  105. package/dist-server/server/drivers/pi.js +3 -3
  106. package/dist-server/server/group-goal-run.js +6 -4
  107. package/dist-server/server/harness-capabilities.js +2 -0
  108. package/dist-server/server/hosted-models.js +6 -1
  109. package/dist-server/server/included-services.js +70 -0
  110. package/dist-server/server/index.js +1462 -576
  111. package/dist-server/server/local-vm-inventory.js +11 -1
  112. package/dist-server/server/local-vm-seat-pool.js +141 -0
  113. package/dist-server/server/managed-policy.js +4 -1
  114. package/dist-server/server/memory-capture.js +194 -0
  115. package/dist-server/server/memory-entries.js +76 -0
  116. package/dist-server/server/memory-organize.js +102 -0
  117. package/dist-server/server/memory-tidy.js +144 -0
  118. package/dist-server/server/memory-topics.js +169 -0
  119. package/dist-server/server/memory-upkeep.js +452 -0
  120. package/dist-server/server/message-db.js +49 -5
  121. package/dist-server/server/model-context-window.js +4 -4
  122. package/dist-server/server/package-export.js +17 -10
  123. package/dist-server/server/package-import.js +6 -3
  124. package/dist-server/server/package-parts.js +3 -1
  125. package/dist-server/server/permission-proxy.js +3 -3
  126. package/dist-server/server/profile-learned.js +98 -0
  127. package/dist-server/server/profile-versions.js +45 -11
  128. package/dist-server/server/recall.js +172 -0
  129. package/dist-server/server/redact.js +1 -1
  130. package/dist-server/server/request-auth.js +2 -0
  131. package/dist-server/server/room-cwd.js +1 -1
  132. package/dist-server/server/room-handoffs.js +61 -6
  133. package/dist-server/server/routes/bot-memory.js +215 -0
  134. package/dist-server/server/routes/decider.js +19 -0
  135. package/dist-server/server/routine-requests.js +27 -16
  136. package/dist-server/server/routines.js +10 -6
  137. package/dist-server/server/shared-computers.js +116 -30
  138. package/dist-server/server/steer-queue.js +41 -10
  139. package/dist-server/server/store.js +42 -3
  140. package/dist-server/server/surface.js +14 -1
  141. package/dist-server/server/system-prompt.js +16 -5
  142. package/dist-server/server/team-backup.js +6 -2
  143. package/dist-server/server/team-computers.js +2 -2
  144. package/dist-server/server/team-manifest.js +11 -3
  145. package/dist-server/server/tightening-requests.js +34 -4
  146. package/dist-server/server/tool-summary.js +2 -2
  147. package/dist-server/server/tts/elevenlabs.js +12 -7
  148. package/dist-server/server/tts/index.js +17 -10
  149. package/dist-server/server/turn-resources.js +87 -11
  150. package/dist-server/server/vps-computer.js +1 -1
  151. package/dist-server/server/workspace-backup-policy.js +22 -3
  152. package/dist-server/server/workspace-backup.js +25 -8
  153. package/dist-server/server/workspace.js +57 -22
  154. package/dist-server/shared/ask-question.js +7 -5
  155. package/dist-server/shared/bot-avatar.js +19 -0
  156. package/dist-server/shared/cloud-home.js +4 -0
  157. package/dist-server/shared/credential-request.js +3 -3
  158. package/dist-server/shared/package-format.js +30 -4
  159. package/dist-server/shared/team-backup.js +20 -3
  160. package/dist-server/shared/tool-surface.js +1 -1
  161. package/dist-server/src/lib/composer-attachments.js +9 -0
  162. package/dist-server/vps-container-mcp.js +136 -36
  163. package/dist-server/workspace-backup.worker.js +165 -35
  164. package/package.json +1 -1
  165. package/dist/assets/channel-DqY_gMLg.js +0 -1
  166. package/dist/assets/index-BIMdIbaw.css +0 -1
  167. package/dist/assets/index-BVPdbq2r.js +0 -583
  168. package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
@@ -0,0 +1,452 @@
1
+ // Memory upkeep: the background half of a bot's memory. On for every bot
2
+ // unless switched off (BotRecord.memoryUpkeep === false). The main agent
3
+ // keeps reading and writing its notes as before; this adds what happens
4
+ // without it choosing:
5
+ // - capture: finished 1:1 turns are read after the chat goes quiet; core
6
+ // facts go to MEMORY.md, detail to a topic file per subject that upkeep
7
+ // creates and names, with other words for it (server/memory-capture.ts);
8
+ // - About me: a durable fact about the person, from their own words, is
9
+ // added to the shared About me and listed in Settings with Remove
10
+ // (server/profile-learned.ts);
11
+ // - tidy-up: nightly, and on demand, expired entries are archived, exact
12
+ // duplicates merged (MEMORY.md and topic files) and contradictions in
13
+ // MEMORY.md struck (server/memory-tidy.ts).
14
+ // Every memory write is a journal row with actor "upkeep", so the Memory
15
+ // panel shows it and Undo works. The model steps need a one-shot text call
16
+ // (`generateText`: Claude and the chat-completion engines); on any other
17
+ // engine they are skipped and only the deterministic tidy steps run.
18
+ import { mkdirSync, readFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { writeFileAtomic } from "./atomic.js";
21
+ import { DATA_DIR } from "./config.js";
22
+ import { CaptureBuffer, capturePrompt, newCandidates, parseCandidates } from "./memory-capture.js";
23
+ import { factIdentity, lineFactIdentity, notebookIdentities } from "./memory-entries.js";
24
+ import { mergeTopicText } from "./memory-topics.js";
25
+ import { applyMoves, MAX_MOVES, organizeCandidates, organizePrompt, parseMoves } from "./memory-organize.js";
26
+ import { recordMemoryChange } from "./memory-journal.js";
27
+ import { applyTidy, contradictionCandidates, contradictionPrompt, parseContradictions, planChanges, planTidy } from "./memory-tidy.js";
28
+ import { ensureWorkspace, listMemoryTopics, memoryDate, memoryEntry, memoryTopicIndex, updateMemory, workspaceDir, writeMemoryFile, writeMemoryTopic } from "./workspace.js";
29
+ export const CAPTURE_MAX_TURNS = 6;
30
+ export const MODEL_TIMEOUT_MS = 60_000;
31
+ export const TIDY_CHECK_MS = 10 * 60_000;
32
+ /** Below this many live entries there is nothing a contradiction pass may change. */
33
+ export const MIN_ENTRIES_FOR_CONTRADICTIONS = 5;
34
+ export const ARCHIVE_TOPIC = "archive.md";
35
+ /** A topic file past this size gains nothing more from capture. */
36
+ export const TOPIC_MAX_BYTES = 64 * 1024;
37
+ /** On unless switched off: every bot keeps its memory in shape by default. */
38
+ export function upkeepEnabled(bot) {
39
+ return Boolean(bot) && bot.memoryUpkeep !== false;
40
+ }
41
+ /** How many "this entry is core" judgements are remembered per bot. */
42
+ const MAX_CORE = 500;
43
+ function statePath() {
44
+ return join(DATA_DIR, "memory-upkeep.json");
45
+ }
46
+ function loadState() {
47
+ try {
48
+ const parsed = JSON.parse(readFileSync(statePath(), "utf8"));
49
+ return parsed && typeof parsed.bots === "object" && parsed.bots ? parsed : { bots: {} };
50
+ }
51
+ catch {
52
+ return { bots: {} };
53
+ }
54
+ }
55
+ function saveState(state) {
56
+ try {
57
+ mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
58
+ writeFileAtomic(statePath(), `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
59
+ }
60
+ catch {
61
+ // status is a convenience; losing it only means the tidy may run again
62
+ }
63
+ }
64
+ function readRaw(botId, relative) {
65
+ try {
66
+ return readFileSync(join(workspaceDir(botId), relative), "utf8");
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ async function askModel(engine, prompt) {
73
+ if (!engine.generateText)
74
+ return null;
75
+ const expiry = AbortSignal.timeout(MODEL_TIMEOUT_MS);
76
+ try {
77
+ const answer = await Promise.race([
78
+ engine.generateText(prompt, { signal: expiry }),
79
+ new Promise((_, reject) => expiry.addEventListener("abort", () => reject(new Error("timed out")), { once: true })),
80
+ ]);
81
+ return typeof answer === "string" ? answer : null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ export const NO_TEXT_ENGINE = "This bot's engine cannot make the quick background model call upkeep uses, so only expired notes and exact duplicates are tidied.";
88
+ export function createMemoryUpkeep(deps) {
89
+ const now = () => deps.now?.() ?? new Date();
90
+ const log = (line) => deps.log?.(line);
91
+ const inflight = new Set();
92
+ const tidying = new Map();
93
+ let timer = null;
94
+ let paused = false;
95
+ const deferred = [];
96
+ const record = (botId, patch) => {
97
+ const state = loadState();
98
+ state.bots[botId] = { ...state.bots[botId], ...patch };
99
+ saveState(state);
100
+ };
101
+ const track = (promise) => {
102
+ inflight.add(promise);
103
+ void promise.finally(() => inflight.delete(promise)).catch(() => undefined);
104
+ return promise;
105
+ };
106
+ async function capture(batch) {
107
+ const bot = deps.bot(batch.botId);
108
+ const at = now().getTime();
109
+ const off = { at, added: 0, topics: 0, aboutMe: 0, note: "upkeep is off" };
110
+ if (!upkeepEnabled(bot))
111
+ return off;
112
+ const engine = deps.engine(bot.id);
113
+ if (!engine?.generateText) {
114
+ const report = { at, added: 0, topics: 0, aboutMe: 0, note: NO_TEXT_ENGINE };
115
+ record(bot.id, { lastCapture: report });
116
+ return report;
117
+ }
118
+ ensureWorkspace(bot.id);
119
+ const today = memoryDate(now());
120
+ const answer = await askModel(engine, capturePrompt({
121
+ botName: bot.name,
122
+ turns: batch.turns,
123
+ notebook: readRaw(bot.id, "MEMORY.md") ?? "",
124
+ today,
125
+ topics: memoryTopicIndex(bot.id),
126
+ }));
127
+ if (answer === null) {
128
+ const report = { at, added: 0, topics: 0, aboutMe: 0, note: "The capture call did not answer in time." };
129
+ record(bot.id, { lastCapture: report });
130
+ return report;
131
+ }
132
+ if (paused) {
133
+ // a backup began while the model answered: run it again after
134
+ deferred.push(batch);
135
+ return { at, added: 0, topics: 0, aboutMe: 0, note: "deferred until the backup finishes" };
136
+ }
137
+ // Re-read after the await: the bot or the person may have written since.
138
+ // From here to the journal rows there is no await, so no write interleaves.
139
+ if (!upkeepEnabled(deps.bot(bot.id)))
140
+ return off;
141
+ const before = readRaw(bot.id, "MEMORY.md");
142
+ const parsed = parseCandidates(answer, today);
143
+ const fresh = newCandidates(parsed, before ?? "");
144
+ const source = `${deps.sourceLabel(bot.id, batch.threadId)} (noticed)`;
145
+ let added = 0;
146
+ let full = false;
147
+ const byTopic = new Map();
148
+ for (const candidate of fresh) {
149
+ if (candidate.topic) {
150
+ byTopic.set(candidate.topic, [...(byTopic.get(candidate.topic) ?? []), candidate]);
151
+ continue;
152
+ }
153
+ const result = updateMemory(bot.id, { action: "append", text: candidate.text, ...(candidate.until ? { until: candidate.until } : {}) }, { source, now: now() });
154
+ if (result.ok)
155
+ added += 1;
156
+ else if (result.code === "over-budget")
157
+ full = true;
158
+ }
159
+ if (added)
160
+ recordMemoryChange(bot.id, { path: "MEMORY.md", actor: "upkeep", via: "capture", threadId: batch.threadId, before, after: readRaw(bot.id, "MEMORY.md") });
161
+ // Detail goes to a topic file the bot keeps for that subject, created
162
+ // with a header (title and other words for it) the first time.
163
+ const existing = new Map(listMemoryTopics(bot.id).map((topic) => [topic.name.toLowerCase(), topic.name]));
164
+ let topics = 0;
165
+ for (const [wanted, candidates] of byTopic) {
166
+ const name = existing.get(wanted) ?? wanted;
167
+ const path = `memory/${name}`;
168
+ const current = readRaw(bot.id, path);
169
+ if (current !== null && Buffer.byteLength(current, "utf8") > TOPIC_MAX_BYTES)
170
+ continue;
171
+ const seen = notebookIdentities(current ?? "");
172
+ const lines = candidates
173
+ .filter((candidate) => !seen.has(factIdentity(candidate.text)))
174
+ .map((candidate) => memoryEntry(candidate.text, { source, now: now(), ...(candidate.until ? { until: candidate.until } : {}) }));
175
+ if (!lines.length)
176
+ continue;
177
+ const title = name.replace(/\.md$/, "").replace(/-/g, " ");
178
+ const aliases = [...new Set(candidates.flatMap((candidate) => candidate.topicAliases ?? []))];
179
+ try {
180
+ writeMemoryTopic(bot.id, name, mergeTopicText(current, { title, aliases, lines }));
181
+ }
182
+ catch {
183
+ continue;
184
+ }
185
+ recordMemoryChange(bot.id, { path, actor: "upkeep", via: "capture", threadId: batch.threadId, before: current, after: readRaw(bot.id, path) });
186
+ existing.set(name.toLowerCase(), name);
187
+ topics += lines.length;
188
+ }
189
+ // Only the owner's own words reach About me: on a shared workspace
190
+ // another person's facts are not the owner's profile. Every parsed fact
191
+ // counts, not only new ones — the bot may already have noted it in its
192
+ // own memory, and About me is where every other bot learns it.
193
+ const owner = batch.turns.every((turn) => turn.owner !== false);
194
+ const aboutMe = owner ? deps.addToAboutMe({ botId: bot.id, botName: bot.name }, parsed.filter((c) => c.aboutUser).map((c) => c.text)) : 0;
195
+ // whoever wrote MEMORY.md — the bot, the person or this capture — detail
196
+ // that is not core moves to its topic now, not only at night
197
+ const organized = await organize(bot.id);
198
+ const report = { at, added, topics, aboutMe, organized, ...(full ? { note: "MEMORY.md is full; the tidy-up or a person needs to make room." } : {}) };
199
+ record(bot.id, { lastCapture: report });
200
+ if (added || topics || aboutMe)
201
+ log(`memory upkeep: noticed ${added} fact(s) for MEMORY.md, ${topics} for topic files and ${aboutMe} for About me — ${bot.name} (${bot.id}) from ${batch.threadId}`);
202
+ return report;
203
+ }
204
+ const buffer = new CaptureBuffer({
205
+ quietMs: deps.quietMs,
206
+ maxTurns: CAPTURE_MAX_TURNS,
207
+ onFlush: (batch) => {
208
+ if (paused) {
209
+ deferred.push(batch);
210
+ return;
211
+ }
212
+ void track(capture(batch)).catch((error) => log(`memory upkeep: capture failed for ${batch.botId}: ${error.message}`));
213
+ },
214
+ });
215
+ /** Move MEMORY.md entries that are not core into their topic files. One
216
+ * model call over entries not judged before; lines move unchanged, topics
217
+ * first so undoing the newest row (MEMORY.md) never loses one. */
218
+ async function organize(botId) {
219
+ const engine = deps.engine(botId);
220
+ if (!engine?.generateText)
221
+ return 0;
222
+ const today = memoryDate(now());
223
+ const state = loadState();
224
+ const core = new Set(state.bots[botId]?.core ?? []);
225
+ const first = readRaw(botId, "MEMORY.md") ?? "";
226
+ const candidates = organizeCandidates(first, today, core);
227
+ if (!candidates.length)
228
+ return 0;
229
+ const answer = await askModel(engine, organizePrompt(candidates, memoryTopicIndex(botId)));
230
+ if (answer === null || paused || !upkeepEnabled(deps.bot(botId)))
231
+ return 0;
232
+ // the line numbers are the file's as read; a write since means ask again later
233
+ const before = readRaw(botId, "MEMORY.md") ?? "";
234
+ if (before !== first)
235
+ return 0;
236
+ const proposed = parseMoves(answer, candidates);
237
+ if (proposed === null)
238
+ return 0;
239
+ // Deferred moves are not core: let the next pass reconsider them.
240
+ const moved = new Set(proposed.map((move) => move.entry.line));
241
+ const moves = proposed.slice(0, MAX_MOVES);
242
+ const judgedCore = candidates.filter((entry) => !moved.has(entry.line)).map((entry) => factIdentity(entry.body));
243
+ const saved = loadState();
244
+ saved.bots[botId] = { ...saved.bots[botId], core: [...new Set([...(saved.bots[botId]?.core ?? []), ...judgedCore])].slice(-MAX_CORE) };
245
+ saveState(saved);
246
+ if (!moves.length)
247
+ return 0;
248
+ const { byTopic } = applyMoves(before, moves);
249
+ const written = new Set();
250
+ const existing = new Map(listMemoryTopics(botId).map((topic) => [topic.name.toLowerCase(), topic.name]));
251
+ let count = 0;
252
+ for (const [wanted, group] of byTopic) {
253
+ const name = existing.get(wanted) ?? wanted;
254
+ const path = `memory/${name}`;
255
+ const current = readRaw(botId, path);
256
+ const seen = notebookIdentities(current ?? "");
257
+ const lines = group.lines.filter((line) => !seen.has(lineFactIdentity(line)));
258
+ if (lines.length) {
259
+ try {
260
+ writeMemoryTopic(botId, name, mergeTopicText(current, { title: name.replace(/\.md$/, "").replace(/-/g, " "), aliases: group.aliases, lines }));
261
+ }
262
+ catch (error) {
263
+ log(`memory upkeep: could not write ${path}; kept its entries in MEMORY.md: ${error.message}`);
264
+ continue;
265
+ }
266
+ recordMemoryChange(botId, { path, actor: "upkeep", via: "organize", before: current, after: readRaw(botId, path) });
267
+ }
268
+ existing.set(name.toLowerCase(), name);
269
+ written.add(wanted);
270
+ count += group.lines.length;
271
+ }
272
+ if (!written.size)
273
+ return 0;
274
+ const { text } = applyMoves(before, moves.filter((move) => written.has(move.topic)));
275
+ writeMemoryFile(botId, text);
276
+ recordMemoryChange(botId, { path: "MEMORY.md", actor: "upkeep", via: "organize", before, after: readRaw(botId, "MEMORY.md") });
277
+ log(`memory upkeep: moved ${count} entr${count === 1 ? "y" : "ies"} from MEMORY.md into topic files for ${deps.bot(botId)?.name ?? botId}`);
278
+ return count;
279
+ }
280
+ function appendArchive(botId, archived) {
281
+ const archivePath = `memory/${ARCHIVE_TOPIC}`;
282
+ const archiveBefore = readRaw(botId, archivePath);
283
+ const head = archiveBefore ?? "---\ntitle: Archive\ndescription: expired notes moved out by the tidy-up\n---\n";
284
+ writeMemoryTopic(botId, ARCHIVE_TOPIC, `${head}${head.endsWith("\n") ? "" : "\n"}${archived.join("\n")}\n`);
285
+ recordMemoryChange(botId, { path: archivePath, actor: "upkeep", via: "tidy", before: archiveBefore, after: readRaw(botId, archivePath) });
286
+ }
287
+ async function runTidy(botId) {
288
+ const bot = deps.bot(botId);
289
+ const at = now().getTime();
290
+ const today = memoryDate(now());
291
+ const stopped = { at, expired: 0, duplicates: 0, superseded: 0, deferred: 0, contradictionsChecked: false, note: "Memory upkeep is off or paused; no further changes were made." };
292
+ if (paused || !upkeepEnabled(bot))
293
+ return stopped;
294
+ ensureWorkspace(botId);
295
+ const organized = await organize(botId);
296
+ if (paused || !upkeepEnabled(deps.bot(botId)))
297
+ return stopped;
298
+ const first = readRaw(botId, "MEMORY.md") ?? "";
299
+ const engine = deps.engine(botId);
300
+ const candidates = contradictionCandidates(first, today);
301
+ let contradictions = [];
302
+ let contradictionsChecked = false;
303
+ let note;
304
+ if (!engine?.generateText)
305
+ note = NO_TEXT_ENGINE;
306
+ else if (candidates.length < MIN_ENTRIES_FOR_CONTRADICTIONS)
307
+ note = `Contradictions are checked from ${MIN_ENTRIES_FOR_CONTRADICTIONS} notes on.`;
308
+ else {
309
+ const answer = await askModel(engine, contradictionPrompt(candidates));
310
+ if (answer === null)
311
+ note = "The contradiction check did not answer in time.";
312
+ else {
313
+ contradictions = parseContradictions(answer, candidates.length);
314
+ contradictionsChecked = true;
315
+ }
316
+ }
317
+ if (paused || !upkeepEnabled(deps.bot(botId)))
318
+ return stopped;
319
+ // Re-read after the await. If the notes changed meanwhile, the model's
320
+ // line numbers no longer point at the same facts: keep only the
321
+ // deterministic steps this time.
322
+ const before = readRaw(botId, "MEMORY.md") ?? "";
323
+ if (before !== first && contradictions.length) {
324
+ contradictions = [];
325
+ note = "The notes changed during the check; contradictions wait for the next tidy-up.";
326
+ }
327
+ const plan = planTidy(before, today, contradictions);
328
+ if (planChanges(plan)) {
329
+ const { text, archived } = applyTidy(before, plan, today);
330
+ // The archive is written first: a line is never out of MEMORY.md
331
+ // before it is in the archive, and the newest journal row — the one a
332
+ // person undoes — is MEMORY.md, whose undo brings the line back.
333
+ if (archived.length)
334
+ appendArchive(botId, archived);
335
+ writeMemoryFile(botId, text);
336
+ recordMemoryChange(botId, { path: "MEMORY.md", actor: "upkeep", via: "tidy", before, after: readRaw(botId, "MEMORY.md") });
337
+ }
338
+ // Topic files get the two steps that need no model: expired lines to the
339
+ // archive (written first, as above) and exact duplicates merged.
340
+ let topicExpired = 0;
341
+ let topicDuplicates = 0;
342
+ for (const topic of listMemoryTopics(botId)) {
343
+ if (topic.name === ARCHIVE_TOPIC)
344
+ continue;
345
+ const path = `memory/${topic.name}`;
346
+ const topicBefore = readRaw(botId, path);
347
+ if (topicBefore === null)
348
+ continue;
349
+ const topicPlan = planTidy(topicBefore, today);
350
+ if (!planChanges(topicPlan))
351
+ continue;
352
+ const tidied = applyTidy(topicBefore, topicPlan, today);
353
+ if (tidied.archived.length)
354
+ appendArchive(botId, tidied.archived);
355
+ writeMemoryTopic(botId, topic.name, tidied.text);
356
+ recordMemoryChange(botId, { path, actor: "upkeep", via: "tidy", before: topicBefore, after: readRaw(botId, path) });
357
+ topicExpired += topicPlan.expired.length;
358
+ topicDuplicates += topicPlan.duplicates.length;
359
+ }
360
+ const report = {
361
+ at,
362
+ expired: plan.expired.length + topicExpired,
363
+ duplicates: plan.duplicates.length + topicDuplicates,
364
+ superseded: plan.superseded.length,
365
+ deferred: plan.deferred,
366
+ ...(organized ? { organized } : {}),
367
+ contradictionsChecked,
368
+ ...(note ? { note } : {}),
369
+ };
370
+ record(botId, { lastTidy: report });
371
+ if (report.expired || report.duplicates || report.superseded || organized)
372
+ log(`memory upkeep: tidied ${bot?.name ?? botId} — ${report.expired} expired, ${report.duplicates} duplicate(s), ${report.superseded} contradicted`);
373
+ return report;
374
+ }
375
+ function tidy(botId) {
376
+ const running = tidying.get(botId);
377
+ if (running)
378
+ return running;
379
+ const promise = track(runTidy(botId)).finally(() => tidying.delete(botId));
380
+ tidying.set(botId, promise);
381
+ return promise;
382
+ }
383
+ function due(botId) {
384
+ const current = now();
385
+ if (current.getHours() < deps.tidyHour())
386
+ return false;
387
+ const last = loadState().bots[botId]?.lastTidy?.at;
388
+ return !last || memoryDate(new Date(last)) < memoryDate(current);
389
+ }
390
+ async function tick() {
391
+ if (paused)
392
+ return;
393
+ for (const bot of deps.bots()) {
394
+ if (paused)
395
+ return;
396
+ if (!upkeepEnabled(bot) || deps.busy(bot.id) || !due(bot.id))
397
+ continue;
398
+ try {
399
+ await tidy(bot.id);
400
+ }
401
+ catch (error) {
402
+ log(`memory upkeep: tidy failed for ${bot.id}: ${error.message}`);
403
+ }
404
+ }
405
+ }
406
+ return {
407
+ noteTurn(botId, threadId, turn) {
408
+ if (!upkeepEnabled(deps.bot(botId)))
409
+ return;
410
+ if (!turn.person.trim() && !turn.bot.trim())
411
+ return;
412
+ buffer.add(botId, threadId, turn);
413
+ },
414
+ flushThread: (threadId) => buffer.flush(threadId),
415
+ dropBot: (botId) => buffer.dropBot(botId),
416
+ capture: (batch) => track(capture(batch)),
417
+ tidy,
418
+ status(botId) {
419
+ // the core judgements are upkeep's own bookkeeping, not status
420
+ const { lastTidy, lastCapture } = loadState().bots[botId] ?? {};
421
+ return { ...(lastTidy ? { lastTidy } : {}), ...(lastCapture ? { lastCapture } : {}), modelSteps: Boolean(deps.engine(botId)?.generateText) };
422
+ },
423
+ tick,
424
+ start() {
425
+ if (timer)
426
+ return;
427
+ timer = setInterval(() => void tick(), TIDY_CHECK_MS);
428
+ timer.unref?.();
429
+ // a first pass shortly after start catches a night the computer slept through
430
+ const first = setTimeout(() => void tick(), 60_000);
431
+ first.unref?.();
432
+ },
433
+ stop() {
434
+ if (timer)
435
+ clearInterval(timer);
436
+ timer = null;
437
+ },
438
+ pause() {
439
+ paused = true;
440
+ },
441
+ resume() {
442
+ paused = false;
443
+ for (const batch of deferred.splice(0)) {
444
+ void track(capture(batch)).catch((error) => log(`memory upkeep: capture failed for ${batch.botId}: ${error.message}`));
445
+ }
446
+ },
447
+ async idle() {
448
+ while (inflight.size)
449
+ await Promise.allSettled(inflight);
450
+ },
451
+ };
452
+ }
@@ -475,7 +475,7 @@ const STOP_WORDS = new Set("a an and are as at be by did do for from had has hav
475
475
  * whitespace-separated token becomes a quoted string, so `AND`, `NOT`,
476
476
  * `*`, `:`, and stray quotes are searched for rather than interpreted.
477
477
  * Tokens are ANDed — FTS5's default — so a hit contains all of them. */
478
- function ftsQuery(query) {
478
+ function ftsQuery(query, mode = "all") {
479
479
  const tokens = query
480
480
  .split(/\s+/)
481
481
  .map((token) => token.replace(/"/g, "").trim())
@@ -483,8 +483,49 @@ function ftsQuery(query) {
483
483
  if (!tokens.length)
484
484
  return null;
485
485
  const content = tokens.filter((token) => !STOP_WORDS.has(token.toLowerCase()));
486
+ if (mode === "any") {
487
+ // Automatic recall searches with a whole message: any content word may
488
+ // match, ranked by bm25. Punctuation-only tokens are dropped, since a
489
+ // lone "?" or "—" would match nothing useful. No content word, no query.
490
+ const terms = recallTerms(query).map(recallMatchTerm).filter(Boolean);
491
+ return terms.length ? terms.join(" OR ") : null;
492
+ }
486
493
  return (content.length ? content : tokens).map((token) => `"${token}"`).join(" ");
487
494
  }
495
+ /** One recall term as FTS5 syntax. The index does no stemming, so a word
496
+ * of four or more letters matches as a prefix, with a plural -s taken off
497
+ * first: "restaurant" and "restaurants" find each other. Skip punctuation
498
+ * terms: unicode61 cannot distinguish "-10" from "10" or "c++" from "c",
499
+ * even in a quoted FTS phrase. Explicit all-term search stays unchanged. */
500
+ export function recallMatchTerm(term) {
501
+ if (!/^[\p{L}\p{N}]+$/u.test(term))
502
+ return null;
503
+ if (!/^\p{L}{4,}$/u.test(term))
504
+ return `"${term}"`;
505
+ const stem = term.length > 4 && term.endsWith("s") && !term.endsWith("ss") ? term.slice(0, -1) : term;
506
+ return `"${stem}"*`;
507
+ }
508
+ /** Words that go at the end of a question and say nothing about its topic. */
509
+ const RECALL_FILLER = new Set("about again also any anything can could does don't know me my please remember remind should tell there they us would hi hello hey thanks thank ok okay yes yeah no i'm im i've i'll i'd you're quick just".split(" "));
510
+ /** The content words of a message, for an any-term recall: lower-cased,
511
+ * de-duplicated, stop words and filler dropped. Ordinary separators between
512
+ * letters split words; signs, language symbols and versions (`-10`, `c++`,
513
+ * `v2.1`) survive as written rather than matching a different fact. */
514
+ export function recallTerms(query) {
515
+ const out = [];
516
+ for (const raw of query.split(/\s+|(?<=\p{L})[/,;:.—–-]+(?=\p{L})/u)) {
517
+ const token = raw.replace(/"/g, "").replace(/^[\s.,;:!?()[\]{}'`“”‘’]+|[\s.,;:!?()[\]{}'`“”‘’]+$/g, "").toLowerCase();
518
+ if (token.length < 2 && !/\d/.test(token))
519
+ continue;
520
+ if (!/[\p{L}\p{N}]/u.test(token))
521
+ continue;
522
+ if (STOP_WORDS.has(token) || RECALL_FILLER.has(token))
523
+ continue;
524
+ if (!out.includes(token))
525
+ out.push(token);
526
+ }
527
+ return out.slice(0, 16);
528
+ }
488
529
  /** How much of a matched message rides back in a hit. Wide enough that a
489
530
  * short report reads whole; a longer one is fetched with readMessageText. */
490
531
  const SNIPPET_TOKENS = 48;
@@ -523,8 +564,8 @@ function rangeClause(range, column) {
523
564
  }
524
565
  return { sql: parts.map((part) => ` AND ${part}`).join(""), params };
525
566
  }
526
- export function recallMessages(query, threadIds, limit = 12, range) {
527
- const match = ftsQuery(query);
567
+ export function recallMessages(query, threadIds, limit = 12, range, mode = "all") {
568
+ const match = ftsQuery(query, mode);
528
569
  if (!match || !threadIds.length)
529
570
  return [];
530
571
  const placeholders = threadIds.map(() => "?").join(", ");
@@ -632,8 +673,8 @@ export function indexedMemoryFiles(botId) {
632
673
  /** Relevance-ranked recall over ONE bot's memory files. Scoped by bot id
633
674
  * in SQL, the same way recallMessages scopes by thread: another bot's
634
675
  * memory is not a lower-ranked result, it is not a result. */
635
- export function recallMemory(query, botId, limit = 12) {
636
- const match = ftsQuery(query);
676
+ export function recallMemory(query, botId, limit = 12, mode = "all") {
677
+ const match = ftsQuery(query, mode);
637
678
  if (!match)
638
679
  return [];
639
680
  const rows = db()
@@ -641,6 +682,9 @@ export function recallMemory(query, botId, limit = 12) {
641
682
  `snippet(memory_fts, 0, '[', ']', '…', ${SNIPPET_TOKENS}) AS snippet ` +
642
683
  "FROM memory_fts JOIN memory_files f ON f.rowid = memory_fts.rowid " +
643
684
  "WHERE memory_fts MATCH ? AND f.bot_id = ? " +
685
+ // Automatic recall must filter before LIMIT; historical logs can
686
+ // otherwise crowd all current topic files out of the result set.
687
+ (mode === "any" ? "AND f.path NOT IN ('MEMORY.md', 'memory/archive.md') AND f.path NOT LIKE 'memory/log/%' " : "") +
644
688
  "ORDER BY bm25(memory_fts), f.mtime_ms DESC LIMIT ?")
645
689
  // SAFETY: the SELECT names exactly these three columns; snippet() is never null
646
690
  .all(match, botId, limit);
@@ -6,10 +6,10 @@ export function modelContextWindow(model) {
6
6
  if (!model)
7
7
  return undefined;
8
8
  const id = model.toLowerCase();
9
- // Opus 5.5 is 1M without a `[1m]` suffix. Only the exact slug, or that
10
- // slug after a slash or a provider dot. A longer suffix such as
11
- // `claude-opus-5-5-local` stays on the generic Claude rule, as does `host::`.
12
- if (/(?:^|[/.])claude-opus-5(?:-5|\.5)$/.test(id))
9
+ // Opus 5.5 and Sonnet 5.5 are 1M without a `[1m]` suffix. Only the exact
10
+ // slug, or that slug after a slash or a provider dot. A longer suffix such
11
+ // as `claude-sonnet-5-5-local` stays on the generic Claude rule, as does `host::`.
12
+ if (/(?:^|[/.])claude-(?:opus|sonnet)-5(?:-5|\.5)$/.test(id))
13
13
  return 1_000_000;
14
14
  if (/claude/.test(id) || /^(opus|sonnet|haiku|fable)\b/.test(id))
15
15
  return /\[1m\]|-1m\b|1m-context/.test(id) ? 1_000_000 : 200_000;
@@ -15,6 +15,21 @@ function portableKey(value, fallback, used) {
15
15
  used.add(key);
16
16
  return key;
17
17
  }
18
+ /** A room's responder in package terms. An Auto room is written as its lead
19
+ * (its fallback, else its first exported member): that is how it behaves on
20
+ * any install without the decision model, and every older app can read it,
21
+ * where a new `auto` kind would make them refuse the whole package. */
22
+ function packageRoomResponder(responder, keys, members) {
23
+ if (responder.kind === "member" && keys.has(responder.botId))
24
+ return { kind: "agent", agent: keys.get(responder.botId) };
25
+ if (responder.kind === "everyone")
26
+ return { kind: "everyone" };
27
+ if (responder.kind === "auto") {
28
+ const lead = responder.fallbackBotId ? keys.get(responder.fallbackBotId) : undefined;
29
+ return { kind: "agent", agent: lead ?? members[0] };
30
+ }
31
+ return { kind: "mentions" };
32
+ }
18
33
  function samePlaybook(a, b) {
19
34
  return a.name === b.name && a.summary === b.summary && a.instructions === b.instructions &&
20
35
  a.triggers.join("\n") === b.triggers.join("\n");
@@ -122,11 +137,7 @@ export function createBotPackageExport(input) {
122
137
  const members = group.memberIds.flatMap((id) => idToKey.has(id) ? [idToKey.get(id)] : []);
123
138
  if (!members.length)
124
139
  continue;
125
- const defaultResponder = group.defaultResponder.kind === "member" && idToKey.has(group.defaultResponder.botId)
126
- ? { kind: "agent", agent: idToKey.get(group.defaultResponder.botId) }
127
- : group.defaultResponder.kind === "everyone"
128
- ? { kind: "everyone" }
129
- : { kind: "mentions" };
140
+ const defaultResponder = packageRoomResponder(group.defaultResponder, idToKey, members);
130
141
  rooms.push({
131
142
  key: portableKey(group.name, `room-${index + 1}`, roomKeys),
132
143
  name: group.name,
@@ -587,11 +598,7 @@ export function createTeamPackageExport(input) {
587
598
  const members = group.memberIds.flatMap((id) => botKeys.has(id) ? [botKeys.get(id)] : []);
588
599
  if (!members.length)
589
600
  continue;
590
- const defaultResponder = group.defaultResponder.kind === "member" && botKeys.has(group.defaultResponder.botId)
591
- ? { kind: "agent", agent: botKeys.get(group.defaultResponder.botId) }
592
- : group.defaultResponder.kind === "everyone"
593
- ? { kind: "everyone" }
594
- : { kind: "mentions" };
601
+ const defaultResponder = packageRoomResponder(group.defaultResponder, botKeys, members);
595
602
  const key = roomKeys.get(group.id);
596
603
  rooms.push({ key, name: group.name, members, ...(group.bulletin ? { bulletin: group.bulletin } : {}), defaultResponder });
597
604
  roomMembers.set(group.id, new Set(members));
@@ -318,9 +318,12 @@ function runImport(source, options, deps) {
318
318
  const ids = room.members.map((key) => botIds.get(key));
319
319
  let created = store.createGroup(room.name, ids, false, section);
320
320
  createdGroups.push(created);
321
- const defaultResponder = room.defaultResponder.kind === "agent"
322
- ? { kind: "member", botId: botIds.get(room.defaultResponder.agent) }
323
- : { kind: room.defaultResponder.kind };
321
+ const responder = room.defaultResponder;
322
+ const defaultResponder = responder.kind === "agent"
323
+ ? { kind: "member", botId: botIds.get(responder.agent) }
324
+ : responder.kind === "auto"
325
+ ? { kind: "auto", ...(responder.agent ? { fallbackBotId: botIds.get(responder.agent) } : {}) }
326
+ : { kind: responder.kind };
324
327
  created = store.patchGroup(created.id, {
325
328
  bulletin: room.bulletin ?? "",
326
329
  defaultResponder,
@@ -53,7 +53,9 @@ export const roomReleaseValues = (room) => ({
53
53
  members: [...room.members].sort(),
54
54
  defaultResponder: room.defaultResponder.kind === "agent"
55
55
  ? { kind: "agent", agent: room.defaultResponder.agent }
56
- : { kind: room.defaultResponder.kind },
56
+ : room.defaultResponder.kind === "auto" && room.defaultResponder.agent
57
+ ? { kind: "auto", agent: room.defaultResponder.agent }
58
+ : { kind: room.defaultResponder.kind },
57
59
  });
58
60
  /** One normal form for a routine's timing on both sides, so an untouched
59
61
  * routine compares equal: absent cap → null, absent overlap → "skip". */
@@ -14,7 +14,7 @@
14
14
  // CLI's own AskUserQuestion. It is the tool the model actually reaches for
15
15
  // when it wants a person to choose, and acceptEdits will not run it unasked,
16
16
  // so it lands here looking like "may I run a tool?" — which is how a question
17
- // ended up on screen as an Allow/Deny box over a JSON blob. It is intercepted
17
+ // ended up on screen as an Allow/Deny dialog over a JSON blob. It is intercepted
18
18
  // below and asked as what it is: one card carrying every question it posed,
19
19
  // answered through the tool's own `answers` field.
20
20
  //
@@ -89,8 +89,8 @@ const asRecord = (value) => typeof value === "object" && value !== null && !Arra
89
89
  *
90
90
  * There is deliberately no "fall through to the permission path" here. That
91
91
  * path cannot answer this tool — allowing it throws the click away, as
92
- * above — so the fallback offered a person an Allow/Deny box over raw JSON.
93
- * That box is the bug this file exists to remove.
92
+ * above — so the fallback offered a person an Allow/Deny dialog over raw JSON.
93
+ * That dialog is the bug this file exists to remove.
94
94
  */
95
95
  async function answerNativeQuestions(input) {
96
96
  // Unanswerable entries are SKIPPED by the parser, not fatal: one bad entry