openmausbot 0.1.94 → 0.1.96

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 (188) hide show
  1. package/dist/assets/{DesktopViewer-bxM7NV3N.js → DesktopViewer-DsnA23KM.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-BhMdJhRJ.js → abnfDiagram-O67JEVCF-Dkbd2hcj.js} +1 -1
  3. package/dist/assets/{arc-B5itKDcx.js → arc-Duj5Coy2.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-C5d7UY6U.js → architectureDiagram-NJMV4G6O-uYUsDawP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-BrZIhMmu.js → blockDiagram-BEXU5L5S-CWDlTpXe.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-7I0ArfbD.js → c4Diagram-YGBWAQC7-B0t15PDc.js} +1 -1
  7. package/dist/assets/channel-BZxwC-Gw.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BnEsyN36.js → chunk-2Q5K7J3B-BJSCL-HV.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BXATQ52s.js → chunk-3FUC2YCW-D3_ILccd.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-B0BpTkEx.js → chunk-5VM5RSS4-D3dmwT_S.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-BtLTz6FR.js → chunk-GTNCS2PH-Br73HFPD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-D6gcZOYt.js → chunk-GWA4HPMP-uSsjxubC.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DCLrosvf.js → chunk-JWPE2WC7-C40VWFyr.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-B9JsknV9.js → chunk-NETBCI7D-D6WNNYwU.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CsoTxw7z.js → chunk-WEXAMYUT-DOEhq64P.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-0F_frB0c.js → chunk-XXDRQBXY-BqJEi6Pi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DkrpnflX.js → classDiagram-v2-NBCMYWYE-ByU_GnxT.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BNDwYBt9.js → cose-bilkent-JH36ORCC-D-Uglxqq.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-JUkNr1Mk.js → cynefin-OW5HDTMX-CZWO7aYj.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-DKeh9N4M.js → cynefinDiagram-VND7K2PF-C70-w0la.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-ve-6T_wX.js → dagre-6A5THRUB-CnhjhExB.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-BuJ2d8rT.js → diagram-22UHCM2B-bnjWCNb4.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BgOs_Ni-.js → diagram-3UASUU5V-A4uTWoFV.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-DMK6VmcP.js → diagram-ATOU4E4O-Zi6m3NUl.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DksASlcY.js → diagram-CDSNMT55-DtCcmd5k.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-Bgn8I5k6.js → diagram-MLGK6HIB-DofuETS3.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-zK0jqO8N.js → diagram-MPIPVDR6-Dw2T9PuU.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-vKrbzTwr.js → ebnfDiagram-ZINNZB2B-DbrwPiem.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-Bzv2mPNf.js → elk-276RUBZZ-BvCt1dCb.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CvOyYuxu.js → erDiagram-OPXOYQCR-Y-QKqPnU.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CtbnDbc0.js → flowDiagram-KWPJA3E3-CDkL1B5w.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-c4dMRYyG.js → ganttDiagram-FUAMR5RP-CKV5D2F6.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-B34uaQho.js → gitGraphDiagram-X574FWY7-nL9iFTCv.js} +1 -1
  34. package/dist/assets/{index-CPiqc_DN.js → index-DreDnjOu.js} +1 -1
  35. package/dist/assets/index-Du7o6Dsf.js +610 -0
  36. package/dist/assets/index-KxG4bWSr.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-o64ma4CI.js → infoDiagram-VRGFBTTK-C7aHjzsf.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-DV4C9BAm.js → ishikawaDiagram-OU5B5YK6-KUZsNBTh.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-Bf0AHk1H.js → journeyDiagram-ZHPQQLJL-CGUuodFl.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-B1UMSKmE.js → kanban-definition-PNTS6WVX-Cy6r868Z.js} +1 -1
  41. package/dist/assets/{linear-BpKhuyLm.js → linear-Bl669148.js} +1 -1
  42. package/dist/assets/{mermaid.core-B_BKNCw5.js → mermaid.core-86Xqz5Mr.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DeARruT1.js → mindmap-definition-NLK3R4M7-G4DzHLrd.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BOQEk_1D.js → pegDiagram-GJSIUBJH-K4eruzY0.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-EIfoiTZo.js → pieDiagram-5QR66LMP-DYdR9PPq.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-GgZwT-fi.js → quadrantDiagram-O4NWA36T-BCAh0nEy.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-D9rDQomj.js → railroadDiagram-XR7U4H2S-p0dGRc4g.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DAh1MIeo.js → requirementDiagram-PLB6GJNP-tLeBG0aL.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-CPr6Uz64.js → sankeyDiagram-IPEJSGJF-QRuN16rV.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-BU6VIdcm.js → sequenceDiagram-PO4LG4MO-ueT1nVoa.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-qF2HO41r.js → sizeCapture-INFHLROL-wRS2zWg2.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-DlJ8G4MD.js → stateDiagram-v2-GCMORJYK-CYzA_hE1.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DzYE8I54.js → swimlanes-2SLR337P-kDnv6LMc.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-B7wzEQ0A.js → timeline-definition-EJHVYXUP-D1y2UbbH.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-BvGgrxIP.js → usecaseDiagram-POWQR4AR-IqB4v9HX.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-CEECdgQg.js → vennDiagram-UO4OBE2U-BEi7KYVF.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-XQHA0P_P.js → wardleyDiagram-VNRHLVJA-Dkybo-Yf.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-B5mLcZI2.js → xychartDiagram-PMCCYNJV-BAPFJLpg.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +27 -13
  62. package/dist-server/companion/src/listener.js +39 -18
  63. package/dist-server/companion/src/routes.js +39 -0
  64. package/dist-server/container-mcp.js +16 -29
  65. package/dist-server/drivers/agents-proxy.js +161 -82
  66. package/dist-server/enterprise/server/index.js +2 -1
  67. package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +34 -19
  68. package/dist-server/index.js +4991 -4908
  69. package/dist-server/local-computer.js +2 -1
  70. package/dist-server/openmausbot.js +906 -1785
  71. package/dist-server/pair-cli.js +906 -1785
  72. package/dist-server/proxy-paths.js +2 -1
  73. package/dist-server/server/account-signin.js +0 -2
  74. package/dist-server/server/activity.js +201 -0
  75. package/dist-server/server/admin-activity.js +1 -4
  76. package/dist-server/server/auto-approve.js +25 -4
  77. package/dist-server/server/automatic-recovery.js +1 -1
  78. package/dist-server/server/boat-create-idempotency.js +0 -7
  79. package/dist-server/server/boat-delete-journal.js +0 -4
  80. package/dist-server/server/boat.js +4 -4
  81. package/dist-server/server/bot-package.js +1 -19
  82. package/dist-server/server/browser-runtime.js +1 -2
  83. package/dist-server/server/checkpoints.js +90 -148
  84. package/dist-server/server/chief-of-staff.js +9 -9
  85. package/dist-server/server/cli.js +33 -26
  86. package/dist-server/server/cloud-backend.js +0 -4
  87. package/dist-server/server/cloud-computer-tools.js +110 -0
  88. package/dist-server/server/cloud-home-start.js +12 -15
  89. package/dist-server/server/cloud-home.js +0 -5
  90. package/dist-server/server/cloud-lending.js +0 -3
  91. package/dist-server/server/cloud-move-http.js +44 -30
  92. package/dist-server/server/cloud-move.js +20 -18
  93. package/dist-server/server/composio.js +41 -5
  94. package/dist-server/server/computer-control.js +0 -15
  95. package/dist-server/server/computer-observation.js +0 -74
  96. package/dist-server/server/config.js +121 -65
  97. package/dist-server/server/container-computer.js +23 -24
  98. package/dist-server/server/control-client.js +4 -10
  99. package/dist-server/server/decider/jev.js +1 -4
  100. package/dist-server/server/decider/log.js +1 -3
  101. package/dist-server/server/decision-log.js +1 -4
  102. package/dist-server/server/delegations.js +3 -2
  103. package/dist-server/server/drivers/acp/core.js +11 -8
  104. package/dist-server/server/drivers/acp/opencode-go.js +17 -16
  105. package/dist-server/server/drivers/agents-call.js +88 -92
  106. package/dist-server/server/drivers/agents-catalog.js +90 -11
  107. package/dist-server/server/drivers/agents-proxy.js +10 -31
  108. package/dist-server/server/drivers/antigravity.js +1 -4
  109. package/dist-server/server/drivers/boatagent.js +23 -4
  110. package/dist-server/server/drivers/cerebras.js +3 -3
  111. package/dist-server/server/drivers/chat-mcp-tools.js +4 -7
  112. package/dist-server/server/drivers/claude-login-auth.js +2 -11
  113. package/dist-server/server/drivers/claude.js +16 -14
  114. package/dist-server/server/drivers/codex-device-auth.js +3 -1
  115. package/dist-server/server/drivers/codex.js +5 -4
  116. package/dist-server/server/drivers/grok.js +1 -1
  117. package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
  118. package/dist-server/server/drivers/openai-chat.js +18 -11
  119. package/dist-server/server/drivers/prompt-split.js +4 -4
  120. package/dist-server/server/drivers/retry.js +8 -4
  121. package/dist-server/server/engine-install.js +1 -5
  122. package/dist-server/server/harness/bus.js +49 -0
  123. package/dist-server/server/harness/registry.js +0 -1
  124. package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
  125. package/dist-server/server/index.js +1190 -971
  126. package/dist-server/server/lending-memory.js +12 -8
  127. package/dist-server/server/mcp-registry.js +35 -0
  128. package/dist-server/server/memory-entries.js +38 -8
  129. package/dist-server/server/memory-organize.js +1 -8
  130. package/dist-server/server/memory-topics.js +4 -4
  131. package/dist-server/server/memory-upkeep.js +13 -15
  132. package/dist-server/server/message-file.js +9 -13
  133. package/dist-server/server/outbound-counts.js +79 -0
  134. package/dist-server/server/outbound-requests.js +60 -0
  135. package/dist-server/server/package-import.js +1 -1
  136. package/dist-server/server/peer-delivery.js +5 -19
  137. package/dist-server/server/peer-roster.js +30 -4
  138. package/dist-server/server/profile-versions.js +0 -31
  139. package/dist-server/server/provider-key-check.js +2 -1
  140. package/dist-server/server/proxy-paths.js +2 -1
  141. package/dist-server/server/restart.js +40 -0
  142. package/dist-server/server/room-handoffs.js +17 -13
  143. package/dist-server/server/routes/bot-memory.js +0 -24
  144. package/dist-server/server/routine-requests.js +2 -5
  145. package/dist-server/server/routines.js +10 -5
  146. package/dist-server/server/server-launcher.js +53 -0
  147. package/dist-server/server/setup-mode.js +0 -2
  148. package/dist-server/server/skill-learn.js +0 -1
  149. package/dist-server/server/skill-library.js +0 -15
  150. package/dist-server/server/store.js +10 -2
  151. package/dist-server/server/surface.js +69 -28
  152. package/dist-server/server/system-prompt.js +17 -7
  153. package/dist-server/server/team-manifest.js +1 -6
  154. package/dist-server/server/team-memory.js +209 -0
  155. package/dist-server/server/thread-events.js +2 -2
  156. package/dist-server/server/tunnel.js +4 -1
  157. package/dist-server/server/turn-images.js +1 -21
  158. package/dist-server/server/turn-resources.js +19 -150
  159. package/dist-server/server/usage-ledger.js +3 -1
  160. package/dist-server/server/vps-computer.js +4 -14
  161. package/dist-server/server/workspace.js +87 -50
  162. package/dist-server/server-launcher.js +72 -0
  163. package/dist-server/shared/activity.js +3 -0
  164. package/dist-server/shared/ask-question.js +1 -1
  165. package/dist-server/shared/bot-avatar.js +8 -0
  166. package/dist-server/shared/cloud-computer.js +12 -0
  167. package/dist-server/shared/connector-scopes.js +78 -0
  168. package/dist-server/shared/failed-turn.js +25 -0
  169. package/dist-server/shared/live-approval.js +2 -2
  170. package/dist-server/shared/outbound.js +128 -0
  171. package/dist-server/shared/package-format.js +3 -3
  172. package/dist-server/shared/pairing-link.js +144 -0
  173. package/dist-server/shared/tool-surface.js +3 -2
  174. package/dist-server/vps-container-mcp.js +16 -29
  175. package/dist-server/workspace-backup.worker.js +14 -27
  176. package/enterprise/server/index.js +2 -1
  177. package/package.json +1 -1
  178. package/dist/assets/channel-CdJX6i8k.js +0 -1
  179. package/dist/assets/index-CPYrGsv1.js +0 -610
  180. package/dist/assets/index-CzT42GR8.css +0 -1
  181. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +0 -8
  182. package/dist-server/server/claim-idle.js +0 -33
  183. package/dist-server/server/cloud-overflow.js +0 -167
  184. package/dist-server/server/drivers/chat-boat-tools.js +0 -90
  185. package/dist-server/server/drivers/grokagent.js +0 -4
  186. package/dist-server/server/openmaus-status-capsule.js +0 -329
  187. package/dist-server/server/tightening-requests.js +0 -435
  188. package/dist-server/shared/tightening-request.js +0 -87
@@ -0,0 +1,209 @@
1
+ // Team memory: what every bot in a section knows in common.
2
+ //
3
+ // Four kinds of entry — people (with the names they go by), places (where a
4
+ // document or a thing lives), decisions (dated, with where they were made),
5
+ // and terms (what an abbreviation or a nickname means). A bot proposes an
6
+ // entry from its conversation; every kind waits for admin review before
7
+ // entering shared prompts. The person can edit or delete any of it.
8
+ //
9
+ // Distinct from section-context.ts, which is the user's brief and is never
10
+ // written by a bot, and from a bot's private MEMORY.md. This is the layer
11
+ // in between: bot-fed, person-reviewed, read by all.
12
+ import { readFileSync } from "node:fs";
13
+ import { z } from "zod";
14
+ import { writeFileAtomic } from "./atomic.js";
15
+ import { newId } from "./contracts.js";
16
+ import { sectionContextKey, sectionContextLabel } from "./section-context.js";
17
+ export const TEAM_MEMORY_KINDS = ["person", "place", "decision", "term"];
18
+ export const TEAM_MEMORY_NAME_MAX = 120;
19
+ export const TEAM_MEMORY_DETAIL_MAX = 600;
20
+ export const TEAM_MEMORY_ALIASES_MAX = 8;
21
+ /** How much of the team's memory rides into a prompt. */
22
+ export const TEAM_MEMORY_PROMPT_MAX_BYTES = 6_000;
23
+ const entrySchema = z.object({
24
+ id: z.string().min(1),
25
+ kind: z.enum(TEAM_MEMORY_KINDS),
26
+ name: z.string().min(1).max(TEAM_MEMORY_NAME_MAX),
27
+ detail: z.string().max(TEAM_MEMORY_DETAIL_MAX),
28
+ aliases: z.array(z.string().min(1).max(TEAM_MEMORY_NAME_MAX)).max(TEAM_MEMORY_ALIASES_MAX),
29
+ status: z.enum(["accepted", "proposed"]),
30
+ source: z.object({ botId: z.string(), botName: z.string(), threadId: z.string(), at: z.number().finite() }),
31
+ updatedAt: z.number().finite(),
32
+ });
33
+ const fileSchema = z.object({ version: z.literal(1), sections: z.record(z.string(), z.array(entrySchema)) });
34
+ const emptyFile = () => ({ version: 1, sections: Object.create(null) });
35
+ const clean = (value, max) => (typeof value === "string" ? value.replace(/\s+/g, " ").trim().slice(0, max + 1) : "");
36
+ const sameName = (a, b) => a.toLowerCase() === b.toLowerCase();
37
+ export class TeamMemory {
38
+ file;
39
+ data;
40
+ saved;
41
+ readable = true;
42
+ constructor(file) {
43
+ this.file = file;
44
+ this.data = this.load();
45
+ this.saved = this.snapshot();
46
+ }
47
+ snapshot() {
48
+ return { version: 1, sections: Object.assign(Object.create(null), structuredClone(this.data.sections)) };
49
+ }
50
+ load() {
51
+ try {
52
+ const input = JSON.parse(readFileSync(this.file, "utf8"));
53
+ if (!fileSchema.safeParse(input).success)
54
+ throw new Error("invalid team memory");
55
+ const data = emptyFile();
56
+ // Like section context, team names are own keys, not inherited
57
+ // properties; record parsers can omit ordinary names like __proto__.
58
+ for (const [key, value] of Object.entries(input.sections)) {
59
+ data.sections[sectionContextKey(key)] = z.array(entrySchema).parse(value);
60
+ }
61
+ return data;
62
+ }
63
+ catch (error) {
64
+ if (error.code !== "ENOENT")
65
+ this.readable = false;
66
+ return emptyFile();
67
+ }
68
+ }
69
+ save() {
70
+ try {
71
+ if (!this.readable)
72
+ throw new Error("Saved team memory could not be read; the existing file was left unchanged");
73
+ writeFileAtomic(this.file, JSON.stringify(this.data, null, 2), { mode: 0o600 });
74
+ this.saved = this.snapshot();
75
+ }
76
+ catch (error) {
77
+ this.data = { version: 1, sections: Object.assign(Object.create(null), structuredClone(this.saved.sections)) };
78
+ throw error;
79
+ }
80
+ }
81
+ entries(section) {
82
+ const key = sectionContextKey(section);
83
+ return (this.data.sections[key] ??= []);
84
+ }
85
+ /** Every entry in the section, proposals included, oldest first. */
86
+ list(section) {
87
+ return this.entries(section).map((entry) => ({ ...entry, aliases: [...entry.aliases], source: { ...entry.source } }));
88
+ }
89
+ /** Validate a proposal's fields. Throws with a message naming the field. */
90
+ static normalize(input) {
91
+ if (!TEAM_MEMORY_KINDS.includes(input.kind))
92
+ throw new Error(`kind must be one of ${TEAM_MEMORY_KINDS.join(", ")}`);
93
+ const name = clean(input.name, TEAM_MEMORY_NAME_MAX);
94
+ if (!name || name.length > TEAM_MEMORY_NAME_MAX)
95
+ throw new Error(`name is required and at most ${TEAM_MEMORY_NAME_MAX} characters`);
96
+ const detail = clean(input.detail, TEAM_MEMORY_DETAIL_MAX);
97
+ if (detail.length > TEAM_MEMORY_DETAIL_MAX)
98
+ throw new Error(`detail is at most ${TEAM_MEMORY_DETAIL_MAX} characters`);
99
+ const aliases = [...new Set((input.aliases ?? []).map((alias) => clean(alias, TEAM_MEMORY_NAME_MAX)).filter((alias) => alias && !sameName(alias, name)))].slice(0, TEAM_MEMORY_ALIASES_MAX);
100
+ if (aliases.some(alias => alias.length > TEAM_MEMORY_NAME_MAX))
101
+ throw new Error(`aliases are at most ${TEAM_MEMORY_NAME_MAX} characters`);
102
+ return { kind: input.kind, name, detail, aliases };
103
+ }
104
+ /** Every autonomous addition or replacement waits for admin review without
105
+ * changing accepted facts, including terms and places from untrusted input. */
106
+ propose(section, input, source) {
107
+ const normalized = TeamMemory.normalize(input);
108
+ const entries = this.entries(section);
109
+ const status = "proposed";
110
+ const entry = {
111
+ id: newId(),
112
+ ...normalized,
113
+ status,
114
+ source: { ...source },
115
+ updatedAt: source.at,
116
+ };
117
+ entries.push(entry);
118
+ this.save();
119
+ return { entry: { ...entry }, status };
120
+ }
121
+ /** The person's answer to a proposal. Rejecting removes it. */
122
+ resolve(section, id, answer) {
123
+ const entries = this.entries(section);
124
+ const index = entries.findIndex((entry) => entry.id === id);
125
+ if (index === -1)
126
+ return { claimed: false };
127
+ if (entries[index].status === "accepted")
128
+ return { claimed: true, state: "already_settled" };
129
+ if (answer === "accept") {
130
+ entries[index].status = "accepted";
131
+ entries[index].updatedAt = Date.now();
132
+ // Keep the old accepted fact until a person approves its replacement.
133
+ const accepted = entries[index];
134
+ this.data.sections[sectionContextKey(section)] = entries.filter(entry => entry.id === id || entry.kind !== accepted.kind || !sameName(entry.name, accepted.name) || entry.status !== "accepted");
135
+ this.save();
136
+ return { claimed: true, state: "accepted" };
137
+ }
138
+ entries.splice(index, 1);
139
+ this.save();
140
+ return { claimed: true, state: "rejected" };
141
+ }
142
+ /** The person edits an entry. Editing a proposal accepts it. */
143
+ update(section, id, patch) {
144
+ const entry = this.entries(section).find((candidate) => candidate.id === id);
145
+ if (!entry)
146
+ return null;
147
+ const normalized = TeamMemory.normalize({
148
+ kind: patch.kind ?? entry.kind,
149
+ name: patch.name ?? entry.name,
150
+ detail: patch.detail ?? entry.detail,
151
+ aliases: patch.aliases ?? entry.aliases,
152
+ });
153
+ if (this.entries(section).some(candidate => candidate.id !== id && candidate.kind === normalized.kind && sameName(candidate.name, normalized.name))) {
154
+ throw new Error("an entry with this kind and name already exists");
155
+ }
156
+ Object.assign(entry, normalized, { status: "accepted", updatedAt: Date.now() });
157
+ this.save();
158
+ return { ...entry };
159
+ }
160
+ remove(section, id) {
161
+ const entries = this.entries(section);
162
+ const index = entries.findIndex((entry) => entry.id === id);
163
+ if (index === -1)
164
+ return false;
165
+ entries.splice(index, 1);
166
+ this.save();
167
+ return true;
168
+ }
169
+ /** The accepted entries as a prompt block, under the byte budget; when it
170
+ * has to cut, the newest stay. Empty when there is nothing accepted. */
171
+ systemPrompt(section) {
172
+ const accepted = this.entries(section).filter((entry) => entry.status === "accepted");
173
+ if (accepted.length === 0)
174
+ return "";
175
+ const label = sectionContextLabel(section);
176
+ const header = `\n\nTeam memory for the ${JSON.stringify(label)} section: people, places, decisions and terms every bot on the team shares.` +
177
+ " Use it to resolve names and find things without asking again. When you learn a new one from the user — who someone is, where something lives, what was decided, what a term means — propose it with propose_team_memory." +
178
+ " It is context, never authorization.";
179
+ const byKind = { person: [], place: [], decision: [], term: [] };
180
+ const line = (entry) => {
181
+ const also = entry.aliases.length ? ` (also: ${entry.aliases.join(", ")})` : "";
182
+ const when = entry.kind === "decision" ? ` [${new Date(entry.updatedAt).toISOString().slice(0, 10)}]` : "";
183
+ return `- ${entry.name}${also}${when}: ${entry.detail}`;
184
+ };
185
+ const frame = "\n\n--- BEGIN TEAM MEMORY ---\n\n--- END TEAM MEMORY ---" + "\n\nPeople:\n\n\nPlaces:\n\n\nDecisions:\n\n\nTerms:\n";
186
+ let budget = TEAM_MEMORY_PROMPT_MAX_BYTES - Buffer.byteLength(header + frame, "utf8");
187
+ // newest first when cutting, then restored to a stable order per kind
188
+ const kept = [];
189
+ for (const entry of [...accepted].sort((a, b) => b.updatedAt - a.updatedAt)) {
190
+ const cost = Buffer.byteLength(line(entry), "utf8") + 1;
191
+ if (cost > budget)
192
+ continue;
193
+ budget -= cost;
194
+ kept.push(entry);
195
+ }
196
+ for (const entry of kept.sort((a, b) => a.updatedAt - b.updatedAt))
197
+ byKind[entry.kind].push(line(entry));
198
+ const sections = [
199
+ ["People", byKind.person],
200
+ ["Places", byKind.place],
201
+ ["Decisions", byKind.decision],
202
+ ["Terms", byKind.term],
203
+ ]
204
+ .filter(([, lines]) => lines.length)
205
+ .map(([title, lines]) => `${title}:\n${lines.join("\n")}`)
206
+ .join("\n\n");
207
+ return `${header}\n\n--- BEGIN TEAM MEMORY ---\n${sections}\n--- END TEAM MEMORY ---`;
208
+ }
209
+ }
@@ -85,7 +85,7 @@ function parseRecent(text, includeFirst, limit, valid) {
85
85
  }
86
86
  return out.slice(-limit);
87
87
  }
88
- function readRecentLines(file, limit, valid) {
88
+ export function readRecentLines(file, limit, valid) {
89
89
  let fd;
90
90
  try {
91
91
  fd = openSync(file, "r");
@@ -145,7 +145,7 @@ const askQuestionsOrMissing = (value) => value === undefined ||
145
145
  typeof question.question === "string" &&
146
146
  Array.isArray(question.options) &&
147
147
  question.options.every((option) => isRecord(option) && typeof option.label === "string")));
148
- function isRuntimeEvent(value) {
148
+ export function isRuntimeEvent(value) {
149
149
  if (!isRecord(value) ||
150
150
  typeof value.eventId !== "string" ||
151
151
  typeof value.provider !== "string" ||
@@ -108,6 +108,7 @@ export function createTunnelAccount(options) {
108
108
  updateCredentials: (derive, afterPersist) => credentials.update(derive, afterPersist),
109
109
  identity: { name: options.machineName ?? hostname(), platform: platformName(), appVersion: options.version },
110
110
  newClientInstanceId: () => randomUUID(),
111
+ ...(options.log ? { log: options.log } : {}),
111
112
  ...(recovery
112
113
  ? {
113
114
  autoRecover: true,
@@ -149,7 +150,9 @@ export async function fleetAccess(options) {
149
150
  if (error instanceof ControlPlaneError && error.status === 401) {
150
151
  throw new Error(`the installation credential in ${FLEET_CREDENTIAL_ENV} was rejected by ${controlPlane}; the fleet has to issue a new one`);
151
152
  }
152
- throw new Error(`could not get this machine's public address from ${controlPlane}: ${error instanceof Error ? error.message : String(error)}`);
153
+ // The request id is what support looks the failure up by.
154
+ const reference = error instanceof ControlPlaneError && error.requestId ? ` (reference ${error.requestId})` : "";
155
+ throw new Error(`could not get this machine's public address from ${controlPlane}: ${error instanceof Error ? error.message : String(error)}${reference}`);
153
156
  }
154
157
  }
155
158
  // ── cloudflared and the guardian: where they are, or how to get them ──────
@@ -9,6 +9,7 @@ import { lstatSync, realpathSync, statSync } from "node:fs";
9
9
  import { dirname, extname, resolve } from "node:path";
10
10
  import { fromMarkdown } from "mdast-util-from-markdown";
11
11
  import { ATTACHMENTS_DIR, IMAGE_MAX_BYTES } from "./attachments.js";
12
+ import { decodeAttachmentAttribute } from "./message-file.js";
12
13
  /** Matches the companion composer policy. Four maximum-sized images are
13
14
  * bounded to 40 MiB before a provider is asked to ingest them. */
14
15
  export const TURN_IMAGE_MAX_COUNT = 4;
@@ -24,27 +25,6 @@ const MIME_BY_EXTENSION = {
24
25
  function statusError(status, message) {
25
26
  return Object.assign(new Error(message), { status });
26
27
  }
27
- /** Decode exactly the entities emitted by the composer. Repeated or unknown
28
- * encodings stay encoded and therefore cannot acquire a privileged path. */
29
- function decodeAttachmentAttribute(value) {
30
- return value.replace(/&(quot|lt|gt|amp);|&#(9|10|13);/g, (entity, named, numeric) => {
31
- if (numeric === "9")
32
- return "\t";
33
- if (numeric === "10")
34
- return "\n";
35
- if (numeric === "13")
36
- return "\r";
37
- if (named === "quot")
38
- return '"';
39
- if (named === "lt")
40
- return "<";
41
- if (named === "gt")
42
- return ">";
43
- if (named === "amp")
44
- return "&";
45
- return entity;
46
- });
47
- }
48
28
  function walk(node, visit) {
49
29
  const pending = [node];
50
30
  while (pending.length > 0) {
@@ -1,12 +1,12 @@
1
- import { realpathSync } from "node:fs";
2
- import { relative, resolve, sep } from "node:path";
3
1
  /** One harness owns the data directory. Claims are synchronous and last for
4
2
  * the whole turn, not just a click: a screenshot and its following click
5
- * must see the same desktop. These coordinate app-managed resources; they
6
- * are not a sandbox for arbitrary shell commands. */
3
+ * must see the same desktop. These coordinate app-managed resources — a
4
+ * desktop, a browser session, a phone — that only one turn can drive at a
5
+ * time. A project folder is not one: a bot's threads work in one folder
6
+ * side by side, as several agent sessions do in one repo. They are not a
7
+ * sandbox for arbitrary shell commands. */
7
8
  export class TurnResources {
8
9
  owners = new Map();
9
- reclaims = new Map();
10
10
  /** Arrival-ordered waiters per requested resource (#1652). Entries exist
11
11
  * only while a turn is genuinely waiting (the exclusive bind's poll loop):
12
12
  * a lazy claim that failed once and moved on must not sit at the front and
@@ -72,25 +72,16 @@ export class TurnResources {
72
72
  waitEstimateMs(resource) {
73
73
  return this.waitStats.get(resource)?.ewmaMs;
74
74
  }
75
- blocker(resource, owner, now = Date.now()) {
76
- this.expireIdle(resource, now);
77
- for (const [key, current] of this.owners) {
78
- if (overlaps(key, resource) && !sameOwner(current.owner, owner))
79
- return current.owner;
80
- }
81
- return undefined;
75
+ blocker(resource, owner) {
76
+ const current = this.owners.get(resource);
77
+ return current && !sameOwner(current, owner) ? current : undefined;
82
78
  }
83
- claim(resource, owner, options = {}) {
84
- const now = options.now ?? Date.now();
85
- if (this.blocker(resource, owner, now))
79
+ claim(resource, owner) {
80
+ if (this.blocker(resource, owner))
86
81
  return false;
87
82
  const existing = this.owners.get(resource);
88
- if (existing && sameOwner(existing.owner, owner)) {
89
- // A repeated claim by the sitting owner changes nothing: replacing
90
- // the record would restart activityAt, and re-claims alone would
91
- // hold the seat past every quiet window (#1653).
83
+ if (existing && sameOwner(existing, owner))
92
84
  return true;
93
- }
94
85
  // The free seat belongs to the front waiter; a current holder's
95
86
  // revalidation above never loses its own claim to this queue.
96
87
  const queue = this.waiters.get(resource);
@@ -101,76 +92,12 @@ export class TurnResources {
101
92
  if (!queue.length)
102
93
  this.waiters.delete(resource);
103
94
  }
104
- const record = { owner };
105
- if (options.idle) {
106
- record.idle = options.idle;
107
- // Taking (or retaking) a seat is itself screen activity: the quiet
108
- // window of #1653 starts at the claim, not at the first later call.
109
- record.activityAt = now;
110
- }
111
- this.owners.set(resource, record);
112
- const reclaimed = this.reclaims.get(resource);
113
- // Seating an owner closes any idle-release record for this resource:
114
- // the previous holder re-claimed (its turn lives on), or another turn
115
- // took the free seat — an occupied seat is exactly where #1653 says
116
- // the old holder yields.
117
- if (reclaimed)
118
- this.reclaims.delete(resource);
95
+ this.owners.set(resource, owner);
119
96
  return true;
120
97
  }
121
- owns(resource, owner, now = Date.now()) {
122
- this.expireIdle(resource, now);
123
- const current = this.owners.get(resource);
124
- return Boolean(current && sameOwner(current.owner, owner));
125
- }
126
- /** A real screen call touched this claim (#1653): restart its quiet
127
- * window. Claims without an idle policy — and anything the screen
128
- * poller drives — are no-ops, so preview frames can never hold a seat. */
129
- activity(resource, owner, now = Date.now()) {
130
- // Expiry first: a straggler screen completion arriving at or past the
131
- // quiet boundary must release the seat and open the reclaim window,
132
- // not restart the clock on a claim that already lapsed.
133
- this.expireIdle(resource, now);
98
+ owns(resource, owner) {
134
99
  const current = this.owners.get(resource);
135
- if (current?.idle && sameOwner(current.owner, owner))
136
- current.activityAt = now;
137
- }
138
- /** A screen-touching computer tool call started on this claim (#1653):
139
- * the desktop is executing real I/O for the sitting owner, so the seat
140
- * cannot be idle-released mid-call. Only the sitting owner's start
141
- * counts; a start whose completion never arrives holds the seat until
142
- * settle — the pre-#1653 behavior, never an overlapping handoff. */
143
- beginComputerCall(resource, owner) {
144
- const current = this.owners.get(resource);
145
- if (current?.idle && sameOwner(current.owner, owner)) {
146
- current.computerCallsInFlight = (current.computerCallsInFlight ?? 0) + 1;
147
- }
148
- }
149
- /** The in-flight computer call finished: drop its fence and let the
150
- * completion's own activity tick govern the quiet window. A no-op when
151
- * the claim lapsed or another turn seated itself in between. */
152
- endComputerCall(resource, owner) {
153
- const current = this.owners.get(resource);
154
- if (!current?.idle || !sameOwner(current.owner, owner))
155
- return;
156
- const inFlight = current.computerCallsInFlight ?? 0;
157
- if (inFlight > 0)
158
- current.computerCallsInFlight = inFlight - 1;
159
- }
160
- /** Who may still re-claim `resource` directly after an idle release
161
- * (#1653): the mid-task turn that went quiet, until its reclaim window
162
- * ends or another turn seats itself. The direct re-claim itself is
163
- * just `claim`: a free seat answers the returning holder's next screen
164
- * call without the wait a new arrival would enter. */
165
- reclaimHolder(resource, now = Date.now()) {
166
- const reclaimed = this.reclaims.get(resource);
167
- if (!reclaimed)
168
- return undefined;
169
- if (reclaimed.until <= now) {
170
- this.reclaims.delete(resource);
171
- return undefined;
172
- }
173
- return reclaimed.owner;
100
+ return Boolean(current && sameOwner(current, owner));
174
101
  }
175
102
  release(owner) {
176
103
  for (const [key, queue] of this.waiters) {
@@ -181,81 +108,23 @@ export class TurnResources {
181
108
  this.waiters.delete(key);
182
109
  }
183
110
  for (const [key, current] of this.owners) {
184
- if (sameOwner(current.owner, owner))
111
+ if (sameOwner(current, owner))
185
112
  this.owners.delete(key);
186
113
  }
187
- // A settling turn keeps no reclaim priority (#1653): only a live
188
- // mid-task turn may pick its seat back up.
189
- for (const [key, reclaimed] of this.reclaims) {
190
- if (sameOwner(reclaimed.owner, owner))
191
- this.reclaims.delete(key);
192
- }
193
114
  }
194
115
  /** Drop one of an owner's claims early, when the sequence that took it
195
116
  * could not finish. The owner's other claims stand until settle. */
196
- releaseOne(resource, owner, now = Date.now()) {
197
- if (this.owns(resource, owner, now))
117
+ releaseOne(resource, owner) {
118
+ if (this.owns(resource, owner))
198
119
  this.owners.delete(resource);
199
120
  this.stopWaiting(resource, owner);
200
- // owns() may have just expired a quiet claim into a reclaim record
201
- // for this same owner: an early release keeps no reclaim priority.
202
- const reclaimed = this.reclaims.get(resource);
203
- if (reclaimed && sameOwner(reclaimed.owner, owner))
204
- this.reclaims.delete(resource);
205
- }
206
- /** Quiet-window expiry (#1653), evaluated when the claim is touched: a
207
- * screen-quiet seat is released while its turn lives and remembered for
208
- * the reclaim window. Synchronous on purpose — the claim map decides
209
- * every transition on the server's single thread. */
210
- expireIdle(resource, now) {
211
- const current = this.owners.get(resource);
212
- if (!current?.idle || current.activityAt === undefined)
213
- return;
214
- // A call the desktop is still executing fences the seat (#1653):
215
- // releasing mid-call would hand one desktop to two turns at once.
216
- if ((current.computerCallsInFlight ?? 0) > 0)
217
- return;
218
- if (!current.idle.quietElapsed(current.activityAt, now))
219
- return;
220
- this.owners.delete(resource);
221
- this.reclaims.set(resource, { owner: current.owner, until: now + current.idle.reclaimMs });
222
121
  }
223
122
  /** Whether any live owner holds this resource: the parked-resume drain's
224
123
  * gate (#1651) — a resume fires only when the seat it queued on is free. */
225
- free(resource, now = Date.now()) {
226
- for (const key of this.owners.keys()) {
227
- if (!overlaps(key, resource))
228
- continue;
229
- // A quiet-window claim expires the moment anything needs to know
230
- // whether the seat is free: the parked-resume gate must not wait on
231
- // a claim whose window already elapsed (#1653).
232
- this.expireIdle(key, now);
233
- if (this.owners.has(key))
234
- return false;
235
- }
236
- return true;
124
+ free(resource) {
125
+ return !this.owners.has(resource);
237
126
  }
238
127
  }
239
128
  function sameOwner(a, b) {
240
129
  return a.threadId === b.threadId && a.generation === b.generation;
241
130
  }
242
- export function workspaceResource(cwd) {
243
- // Selected folders must exist before the engine starts. Resolve symlinks
244
- // and native filename casing so aliases cannot grant two writers to the
245
- // same project on case-insensitive volumes.
246
- const canonical = realpathSync.native(resolve(cwd));
247
- return `workspace:${process.platform === "win32" ? canonical.toLowerCase() : canonical}`;
248
- }
249
- function overlaps(a, b) {
250
- if (a === b)
251
- return true;
252
- if (!a.startsWith("workspace:") || !b.startsWith("workspace:"))
253
- return false;
254
- const left = a.slice("workspace:".length);
255
- const right = b.slice("workspace:".length);
256
- const contains = (parent, child) => {
257
- const path = relative(parent, child);
258
- return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !path.startsWith(sep) && !/^[A-Za-z]:/.test(path));
259
- };
260
- return contains(left, right) || contains(right, left);
261
- }
@@ -26,7 +26,9 @@ const promptBytesOf = (value) => {
26
26
  ? { stable: clean(record.stable), volatile: clean(record.volatile) }
27
27
  : null;
28
28
  };
29
- function monthKey(at) {
29
+ /** The UTC month a row belongs to, `YYYY-MM`: the name of its month file.
30
+ * The decision, admin-activity and decider logs use the same layout. */
31
+ export function monthKey(at) {
30
32
  return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
31
33
  }
32
34
  export function usageFileFor(dataDir, at) {
@@ -433,7 +433,7 @@ function statusProblem(status) {
433
433
  if (status.mounts === "unsafe")
434
434
  return "The VPS container has host mounts; refusing to use it";
435
435
  if (status.security === "unsafe")
436
- return "The VPS container is missing OpenMausBot safety limits";
436
+ return "The VPS container has unsupported privilege, isolation, or automatic-removal settings";
437
437
  if (status.container === "stopped")
438
438
  return "The OpenMausBot VPS container is stopped";
439
439
  if (status.desktop_error)
@@ -518,7 +518,7 @@ async function computeVpsComputerStatus(cfg, botId, runner) {
518
518
  (environmentLabel === undefined || environmentLabel === vpsEnvironmentId());
519
519
  status.network = hasNoPublishedPorts(detail?.HostConfig, detail?.NetworkSettings?.Networks) ? "private" : "unsafe";
520
520
  status.mounts = hasNoHostMounts(detail ?? {}) ? "none" : "unsafe";
521
- status.security = dockerSecurityIsHardened(detail?.HostConfig, { restartPolicy: "unless-stopped" })
521
+ status.security = dockerSecurityIsHardened(detail?.HostConfig, { restartPolicy: "any" })
522
522
  ? "hardened"
523
523
  : "unsafe";
524
524
  const connectionKey = `${alias}:${status.container_name}`;
@@ -815,9 +815,8 @@ export function vpsContainerRunArgs(containerName, imageRef = VPS_IMAGE, viewerS
815
815
  // A VPS reboots with nobody watching; without a restart policy the
816
816
  // container stays down afterwards and every turn silently degrades until
817
817
  // someone opens the panel. unless-stopped survives reboots while still
818
- // honoring an explicit Stop. The shared hardening check accepts exactly
819
- // this policy for the VPS caller (and only "no"/unset for the Local VM,
820
- // whose starts are controlled by OMB's turn lifecycle).
818
+ // honoring an explicit Stop. This is a creation default; existing
819
+ // containers keep the operator's restart policy.
821
820
  "--restart",
822
821
  "unless-stopped",
823
822
  "-e",
@@ -1204,15 +1203,6 @@ export function vpsComputerMcp(cfg, botId, containerRef) {
1204
1203
  env: { ELECTRON_RUN_AS_NODE: "1" },
1205
1204
  };
1206
1205
  }
1207
- export function vpsDriverError(driverKind, computerMcp) {
1208
- if (driverKind === "boxAgent") {
1209
- return "The Computer engine runs its agent on Boat and cannot use a self-hosted VPS — choose Claude or an ACP engine";
1210
- }
1211
- if (!computerMcp) {
1212
- return "This model cannot mount a self-hosted VPS computer — choose Claude or an ACP model provider";
1213
- }
1214
- return null;
1215
- }
1216
1206
  export async function vpsComputerScreenshot(cfg, botId, runner = defaultRunner) {
1217
1207
  cfg = snapshotVpsConfig(cfg);
1218
1208
  const alias = vpsSshAlias(cfg);