openmausbot 0.1.92 → 0.1.94

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 (163) hide show
  1. package/dist/assets/{DesktopViewer-Do6lBa4C.js → DesktopViewer-bxM7NV3N.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
  3. package/dist/assets/{arc-DIYc7Ymx.js → arc-B5itKDcx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
  7. package/dist/assets/channel-CdJX6i8k.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
  34. package/dist/assets/index-CPYrGsv1.js +610 -0
  35. package/dist/assets/{index-DGBaEXrM.js → index-CPiqc_DN.js} +1 -1
  36. package/dist/assets/index-CzT42GR8.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
  41. package/dist/assets/{linear-CmOWzX0P.js → linear-BpKhuyLm.js} +1 -1
  42. package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-B_BKNCw5.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +1 -0
  62. package/dist-server/companion/src/routes.js +45 -1
  63. package/dist-server/container-mcp.js +99 -77
  64. package/dist-server/drivers/agents-proxy.js +40 -1
  65. package/dist-server/drivers/pi-mcp-extension.ts +245 -299
  66. package/dist-server/enterprise/server/index.js +1 -0
  67. package/dist-server/index.js +18361 -13794
  68. package/dist-server/local-computer-proxy.js +5 -1
  69. package/dist-server/local-computer.js +1 -0
  70. package/dist-server/mcp-gate.js +152 -20
  71. package/dist-server/mcp-remote-proxy.js +437 -0
  72. package/dist-server/openmausbot.js +8872 -7620
  73. package/dist-server/pair-cli.js +8872 -7620
  74. package/dist-server/permission-proxy.js +65 -1
  75. package/dist-server/proxy-paths.js +1 -0
  76. package/dist-server/server/auto-approve.js +33 -11
  77. package/dist-server/server/boat.js +3 -3
  78. package/dist-server/server/browser-live.js +14 -5
  79. package/dist-server/server/browser-runtime.js +6 -0
  80. package/dist-server/server/channel-queue.js +4 -1
  81. package/dist-server/server/cli.js +32 -12
  82. package/dist-server/server/cloud-move-http.js +9 -3
  83. package/dist-server/server/cloud-move.js +18 -0
  84. package/dist-server/server/cloud-owner.js +1 -1
  85. package/dist-server/server/computer-control.js +6 -0
  86. package/dist-server/server/computer-wait.js +33 -13
  87. package/dist-server/server/config.js +57 -10
  88. package/dist-server/server/container-computer.js +43 -32
  89. package/dist-server/server/decision-log.js +9 -6
  90. package/dist-server/server/delegations.js +47 -22
  91. package/dist-server/server/digest.js +12 -2
  92. package/dist-server/server/drivers/acp/core.js +229 -20
  93. package/dist-server/server/drivers/acp/cursor.js +13 -1
  94. package/dist-server/server/drivers/acp/grok.js +208 -3
  95. package/dist-server/server/drivers/acp/quiet-status.js +257 -0
  96. package/dist-server/server/drivers/acp/qwen.js +18 -0
  97. package/dist-server/server/drivers/agents-call.js +28 -0
  98. package/dist-server/server/drivers/agents-catalog.js +10 -1
  99. package/dist-server/server/drivers/boatagent.js +2 -0
  100. package/dist-server/server/drivers/builtIn.js +2 -0
  101. package/dist-server/server/drivers/cerebras.js +108 -0
  102. package/dist-server/server/drivers/chat-mcp-tools.js +45 -15
  103. package/dist-server/server/drivers/claude.js +35 -9
  104. package/dist-server/server/drivers/codex.js +84 -10
  105. package/dist-server/server/drivers/openai-chat.js +37 -5
  106. package/dist-server/server/drivers/pi.js +48 -4
  107. package/dist-server/server/drivers/prompt-split.js +2 -0
  108. package/dist-server/server/env-path.js +6 -2
  109. package/dist-server/server/environment.js +3 -0
  110. package/dist-server/server/guarded-requests.js +2 -1
  111. package/dist-server/server/index.js +1353 -311
  112. package/dist-server/server/live-call-controller.js +868 -0
  113. package/dist-server/server/live-call.js +216 -0
  114. package/dist-server/server/local-vm-stop-reason.js +26 -0
  115. package/dist-server/server/mcp-gate-config.js +43 -12
  116. package/dist-server/server/mcp-gate.js +127 -25
  117. package/dist-server/server/mcp-http.js +46 -8
  118. package/dist-server/server/mcp-remote-proxy.js +108 -0
  119. package/dist-server/server/new-bot-defaults.js +3 -0
  120. package/dist-server/server/peer-provenance.js +4 -2
  121. package/dist-server/server/permission-proxy.js +15 -1
  122. package/dist-server/server/plan-usage.js +797 -0
  123. package/dist-server/server/provider-key-check.js +2 -1
  124. package/dist-server/server/proxy-paths.js +1 -0
  125. package/dist-server/server/redact.js +2 -0
  126. package/dist-server/server/request-auth.js +3 -2
  127. package/dist-server/server/routes/desktop-viewer.js +50 -6
  128. package/dist-server/server/routes/live.js +98 -0
  129. package/dist-server/server/routines.js +6 -3
  130. package/dist-server/server/skill-library.js +207 -0
  131. package/dist-server/server/skills-library-migration.js +235 -0
  132. package/dist-server/server/skills.js +66 -3
  133. package/dist-server/server/steer-queue.js +13 -1
  134. package/dist-server/server/store.js +86 -4
  135. package/dist-server/server/surface.js +9 -3
  136. package/dist-server/server/team-backup.js +2 -1
  137. package/dist-server/server/thread-events.js +1 -1
  138. package/dist-server/server/tunnel.js +20 -1
  139. package/dist-server/server/turn-log.js +10 -0
  140. package/dist-server/server/turn-resources.js +124 -0
  141. package/dist-server/server/vps-computer.js +1 -1
  142. package/dist-server/server/workspace-backup.js +22 -0
  143. package/dist-server/server/workspace.js +1 -1
  144. package/dist-server/shared/approval-mode.js +1 -1
  145. package/dist-server/shared/ask-question.js +13 -0
  146. package/dist-server/shared/call-consent.js +28 -0
  147. package/dist-server/shared/live-approval.js +124 -0
  148. package/dist-server/shared/live-call.js +176 -0
  149. package/dist-server/shared/redact.js +1 -0
  150. package/dist-server/shared/skill-md.js +10 -0
  151. package/dist-server/shared/team-backup.js +3 -0
  152. package/dist-server/shared/tool-name.js +22 -0
  153. package/dist-server/shared/tool-scope-support.js +25 -0
  154. package/dist-server/shared/tool-scope.js +136 -0
  155. package/dist-server/src/lib/thread-refs.js +406 -0
  156. package/dist-server/vps-container-mcp.js +99 -77
  157. package/dist-server/workspace-backup.worker.js +103 -100
  158. package/enterprise/server/index.js +1 -0
  159. package/package.json +1 -1
  160. package/dist/assets/channel-Dtk5Mtic.js +0 -1
  161. package/dist/assets/index-Cdmjrl28.css +0 -1
  162. package/dist/assets/index-DR7YPMkW.js +0 -597
  163. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +0 -8
@@ -91,20 +91,38 @@ export function tunnelAccess(document) {
91
91
  export function platformName(platform = process.platform) {
92
92
  return platform === "darwin" ? "darwin" : platform === "win32" ? "windows" : "linux";
93
93
  }
94
+ const STOPPED_TUNNEL = { status: "stopped", ready: false };
94
95
  /** The desktop honours OMB_CONTROL_PLANE_URL only in development builds; a
95
96
  * server's operator owns its environment, so it is honoured here always and
96
- * the caller says so in its output. */
97
+ * the caller says so in its output. One-shot commands (login, status) pass no
98
+ * `recovery` and so start no background timers. */
97
99
  export function createTunnelAccount(options) {
98
100
  const env = options.env ?? process.env;
99
101
  const credentials = openTunnelCredentials(options.dataDir);
100
102
  const controlPlane = resolveCompanionControlPlaneURL({ isPackaged: true, environment: env });
101
103
  const client = controlPlane ? createControlPlaneClient({ baseURL: controlPlane, fetchImpl: options.fetchImpl }) : null;
104
+ const recovery = options.recovery;
102
105
  const service = createCompanionAccountService({
103
106
  client,
104
107
  readCredentials: () => credentials.read(),
105
108
  updateCredentials: (derive, afterPersist) => credentials.update(derive, afterPersist),
106
109
  identity: { name: options.machineName ?? hostname(), platform: platformName(), appVersion: options.version },
107
110
  newClientInstanceId: () => randomUUID(),
111
+ ...(recovery
112
+ ? {
113
+ autoRecover: true,
114
+ companionIsOn: () => recovery.running() !== null,
115
+ managedConnectionState: () => recovery.running()?.state() ?? STOPPED_TUNNEL,
116
+ activatePersistedEndpoint: async () => {
117
+ const tunnel = recovery.running();
118
+ const access = tunnelAccess(credentials.read());
119
+ if (!tunnel || !access)
120
+ return tunnel?.state() ?? STOPPED_TUNNEL;
121
+ return tunnel.restart(access);
122
+ },
123
+ ...options.clock,
124
+ }
125
+ : {}),
108
126
  });
109
127
  return { service, credentials, controlPlane };
110
128
  }
@@ -206,6 +224,7 @@ export function startTunnel(options) {
206
224
  address: options.access.endpoint,
207
225
  state: () => current,
208
226
  started,
227
+ restart: (access) => tunnel.start({ endpoint: access.endpoint, token: access.token, originTarget: options.originTarget }),
209
228
  stop: async () => {
210
229
  await tunnel.stop();
211
230
  },
@@ -0,0 +1,10 @@
1
+ // The line the harness prints when a turn starts (stderr, which the desktop
2
+ // app keeps in server.log). It shows the start of the prompt, so a stuck turn
3
+ // can be traced to what was asked. Words spoken on a Live call are never
4
+ // logged — the call's summary line (liveCallSummaryLine) has counters only —
5
+ // so a turn carrying any of them shows `text=(spoken)` instead.
6
+ const LOGGED_CHARS = 70;
7
+ export function turnStartLogLine(turn) {
8
+ const text = turn.spoken ? "(spoken)" : JSON.stringify(turn.text.slice(0, LOGGED_CHARS));
9
+ return `[omb-turn] bot=${turn.botId} text=${text} images=${turn.images} depth=${turn.depth} card=${turn.card}`;
10
+ }
@@ -7,6 +7,71 @@ import { relative, resolve, sep } from "node:path";
7
7
  export class TurnResources {
8
8
  owners = new Map();
9
9
  reclaims = new Map();
10
+ /** Arrival-ordered waiters per requested resource (#1652). Entries exist
11
+ * only while a turn is genuinely waiting (the exclusive bind's poll loop):
12
+ * a lazy claim that failed once and moved on must not sit at the front and
13
+ * block an actively waiting turn behind it from being granted the seat. */
14
+ waiters = new Map();
15
+ /** Exponentially weighted moving average of recent wait durations per
16
+ * resource, recorded when a waiting turn finally acquires. No history,
17
+ * no estimate: the chip shows one only once this map has an entry. */
18
+ waitStats = new Map();
19
+ /** Join the waitlist for a resource and return the stable position: where
20
+ * this turn sits among the waiters, in arrival order. Joining twice keeps
21
+ * the original place; positions only improve (a waiter ahead leaving),
22
+ * never jitter. */
23
+ startWaiting(resource, owner) {
24
+ const queue = this.waiters.get(resource);
25
+ if (!queue) {
26
+ this.waiters.set(resource, [owner]);
27
+ return 1;
28
+ }
29
+ const index = queue.findIndex(waiting => sameOwner(waiting, owner));
30
+ if (index >= 0)
31
+ return index + 1;
32
+ queue.push(owner);
33
+ return queue.length;
34
+ }
35
+ /** Leave the waitlist without claiming: the wait stopped, parked, or was
36
+ * cancelled. Idempotent, and safe for an owner who never joined. */
37
+ stopWaiting(resource, owner) {
38
+ const queue = this.waiters.get(resource);
39
+ if (!queue)
40
+ return;
41
+ const next = queue.filter(waiting => !sameOwner(waiting, owner));
42
+ if (next.length)
43
+ this.waiters.set(resource, next);
44
+ else
45
+ this.waiters.delete(resource);
46
+ }
47
+ /** This owner's current position in a resource's waitlist, or undefined
48
+ * when they are not waiting for it. */
49
+ waitPosition(resource, owner) {
50
+ const queue = this.waiters.get(resource);
51
+ if (!queue)
52
+ return undefined;
53
+ const index = queue.findIndex(waiting => sameOwner(waiting, owner));
54
+ return index < 0 ? undefined : index + 1;
55
+ }
56
+ /** Record how long a wait lasted once it ended in acquisition, feeding the
57
+ * per-resource estimate. Clamped to sane values so a clock skew cannot
58
+ * poison the average. */
59
+ noteWait(resource, waitedMs) {
60
+ if (!Number.isFinite(waitedMs) || waitedMs < 0)
61
+ return;
62
+ const sample = Math.min(waitedMs, 24 * 60 * 60_000);
63
+ const stats = this.waitStats.get(resource);
64
+ // Weight 1/4: recent waits dominate, one outlier does not rewrite the
65
+ // estimate, and the very first wait seeds the history on its own.
66
+ this.waitStats.set(resource, stats
67
+ ? { ewmaMs: stats.ewmaMs + 0.25 * (sample - stats.ewmaMs), samples: stats.samples + 1 }
68
+ : { ewmaMs: sample, samples: 1 });
69
+ }
70
+ /** The smoothed recent wait for a resource, or undefined until at least one
71
+ * wait has completed — the chip's estimate condition (#1652). */
72
+ waitEstimateMs(resource) {
73
+ return this.waitStats.get(resource)?.ewmaMs;
74
+ }
10
75
  blocker(resource, owner, now = Date.now()) {
11
76
  this.expireIdle(resource, now);
12
77
  for (const [key, current] of this.owners) {
@@ -26,6 +91,16 @@ export class TurnResources {
26
91
  // hold the seat past every quiet window (#1653).
27
92
  return true;
28
93
  }
94
+ // The free seat belongs to the front waiter; a current holder's
95
+ // revalidation above never loses its own claim to this queue.
96
+ const queue = this.waiters.get(resource);
97
+ if (queue?.length && !sameOwner(queue[0], owner))
98
+ return false;
99
+ if (queue?.length) {
100
+ queue.shift();
101
+ if (!queue.length)
102
+ this.waiters.delete(resource);
103
+ }
29
104
  const record = { owner };
30
105
  if (options.idle) {
31
106
  record.idle = options.idle;
@@ -60,6 +135,28 @@ export class TurnResources {
60
135
  if (current?.idle && sameOwner(current.owner, owner))
61
136
  current.activityAt = now;
62
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
+ }
63
160
  /** Who may still re-claim `resource` directly after an idle release
64
161
  * (#1653): the mid-task turn that went quiet, until its reclaim window
65
162
  * ends or another turn seats itself. The direct re-claim itself is
@@ -76,6 +173,13 @@ export class TurnResources {
76
173
  return reclaimed.owner;
77
174
  }
78
175
  release(owner) {
176
+ for (const [key, queue] of this.waiters) {
177
+ const next = queue.filter(waiting => !sameOwner(waiting, owner));
178
+ if (next.length)
179
+ this.waiters.set(key, next);
180
+ else
181
+ this.waiters.delete(key);
182
+ }
79
183
  for (const [key, current] of this.owners) {
80
184
  if (sameOwner(current.owner, owner))
81
185
  this.owners.delete(key);
@@ -92,6 +196,7 @@ export class TurnResources {
92
196
  releaseOne(resource, owner, now = Date.now()) {
93
197
  if (this.owns(resource, owner, now))
94
198
  this.owners.delete(resource);
199
+ this.stopWaiting(resource, owner);
95
200
  // owns() may have just expired a quiet claim into a reclaim record
96
201
  // for this same owner: an early release keeps no reclaim priority.
97
202
  const reclaimed = this.reclaims.get(resource);
@@ -106,11 +211,30 @@ export class TurnResources {
106
211
  const current = this.owners.get(resource);
107
212
  if (!current?.idle || current.activityAt === undefined)
108
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;
109
218
  if (!current.idle.quietElapsed(current.activityAt, now))
110
219
  return;
111
220
  this.owners.delete(resource);
112
221
  this.reclaims.set(resource, { owner: current.owner, until: now + current.idle.reclaimMs });
113
222
  }
223
+ /** Whether any live owner holds this resource: the parked-resume drain's
224
+ * 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;
237
+ }
114
238
  }
115
239
  function sameOwner(a, b) {
116
240
  return a.threadId === b.threadId && a.generation === b.generation;
@@ -817,7 +817,7 @@ export function vpsContainerRunArgs(containerName, imageRef = VPS_IMAGE, viewerS
817
817
  // someone opens the panel. unless-stopped survives reboots while still
818
818
  // honoring an explicit Stop. The shared hardening check accepts exactly
819
819
  // this policy for the VPS caller (and only "no"/unset for the Local VM,
820
- // whose desktop cannot safely resume).
820
+ // whose starts are controlled by OMB's turn lifecycle).
821
821
  "--restart",
822
822
  "unless-stopped",
823
823
  "-e",
@@ -37,6 +37,7 @@ const EXCLUSION_NOTES = [
37
37
  "Device pairing, server identity, live leases and runtime files (existing destination identities are preserved).",
38
38
  "Saved credentials, provider and MCP connections, managed provider login homes and browser login profiles are not transferred. Destination connections are preserved; reconnect on a new device.",
39
39
  "Downloaded tools and caches; these can be installed again.",
40
+ "Installed dependency folders (node_modules) and symbolic links inside conversation work folders; reinstall or recreate them in the project.",
40
41
  "External project folders, CLI login homes, browser session homes, OS keychains, companion devices and other servers.",
41
42
  "VM/container disk layers and remote cloud data; durable files inside this workspace are included.",
42
43
  ];
@@ -327,6 +328,8 @@ export async function createWorkspaceBackupSnapshot(dataDir, options, jobId) {
327
328
  const warnings = ["Stop external editors and managed desktops before exporting; files written outside this server cannot be frozen by the backup gate.", "Conversation text and user files are not redacted and may contain secrets you pasted. Keep the encrypted backup private.", RESTORE_WARNING];
328
329
  let bytes = 0;
329
330
  let skippedLinks = 0;
331
+ let skippedDependencies = 0;
332
+ let skippedWorkLinks = 0;
330
333
  try {
331
334
  const root = realpathSync(dataDir);
332
335
  const sourceDb = join(root, "messages.db");
@@ -370,6 +373,21 @@ export async function createWorkspaceBackupSnapshot(dataDir, options, jobId) {
370
373
  throw new Error("A workspace filename cannot be safely restored on supported platforms.");
371
374
  const source = join(directory, name);
372
375
  const stat = lstatSync(source);
376
+ // Installed dependencies come back from the project's lockfile, can
377
+ // hold more files than this format allows, and are often a link an
378
+ // agent made to another checkout's install. Leave them out.
379
+ if (name === "node_modules" && (stat.isDirectory() || stat.isSymbolicLink())) {
380
+ skippedDependencies++;
381
+ continue;
382
+ }
383
+ // A conversation's work folder (task-workspaces/<bot>/<thread>) holds
384
+ // what an agent made there, and agents link things in (another
385
+ // checkout, a virtualenv). A link's target is not this workspace's
386
+ // data, so the link is left out rather than failing the backup.
387
+ if (stat.isSymbolicLink() && path.startsWith("task-workspaces/")) {
388
+ skippedWorkLinks++;
389
+ continue;
390
+ }
373
391
  if (stat.isSymbolicLink()) {
374
392
  let target;
375
393
  try {
@@ -430,6 +448,10 @@ export async function createWorkspaceBackupSnapshot(dataDir, options, jobId) {
430
448
  walk(root);
431
449
  if (skippedLinks)
432
450
  warnings.push(`${skippedLinks} managed skill discovery link(s) were omitted and are recreated by the app.`);
451
+ if (skippedDependencies)
452
+ warnings.push(`${skippedDependencies} installed dependency folder(s) (node_modules) were omitted; reinstall them in the project after restoring.`);
453
+ if (skippedWorkLinks)
454
+ warnings.push(`${skippedWorkLinks} symbolic link(s) in conversation work folders were omitted; what they point to is outside this backup.`);
433
455
  const summary = {
434
456
  format: "openmaus.workspace-backup", version: 1, id: job.id, createdAt: new Date().toISOString(),
435
457
  appVersion: options.appVersion ?? "unknown", files: entries.filter((entry) => entry.type === "file").length,
@@ -20,7 +20,7 @@ export const WORKSPACES_DIR = join(DATA_DIR, "workspaces");
20
20
  export const TASK_WORKSPACES_DIR = join(DATA_DIR, "task-workspaces");
21
21
  /** MCP support does not imply native filesystem tools or a local working directory. */
22
22
  export function supportsWorkspaceFiles(driverKind) {
23
- return !["grok", "openai-compat", "minimax", "mistral", "boxAgent"].includes(driverKind);
23
+ return !["grok", "openai-compat", "minimax", "mistral", "cerebras", "boxAgent"].includes(driverKind);
24
24
  }
25
25
  /** Default task files are private to the thread, outside the bot's shared
26
26
  * memory folder. This is directory organization, not a shell sandbox. */
@@ -21,7 +21,7 @@ export function supportsApprovalMode(driverKind, mode) {
21
21
  // engines could never stop asking — not by its own level, and not through
22
22
  // a Chief's delegated Full access either.
23
23
  return ["codex", "claudeAgent", "antigravityAgent", "cursorAgent", "grokAgent", "opencodeGo", "qwenAgent", "geminiAgent",
24
- "openai-compat", "grok", "minimax", "mistral"].includes(driverKind ?? "");
24
+ "openai-compat", "grok", "minimax", "mistral", "cerebras"].includes(driverKind ?? "");
25
25
  }
26
26
  /** A Full/Custom grant belongs to one provider's tool semantics. Other
27
27
  * modes carry across only when the destination actually implements them.
@@ -15,6 +15,7 @@
15
15
  */
16
16
  /** The provider tool whose input this module understands. */
17
17
  export const ASK_USER_QUESTION_TOOL = "AskUserQuestion";
18
+ export const QUESTION_DISMISS_MESSAGE = "The user closed this question without answering. Use your best judgment and continue.";
18
19
  /** Caps. Claude Code's own limits are smaller (1-4 questions, 2-4 options);
19
20
  * these leave room for a provider that widens them without letting a card
20
21
  * grow without bound. */
@@ -27,6 +28,18 @@ const MAX_LABEL = 120;
27
28
  const MAX_DESCRIPTION = 400;
28
29
  /** One free-text answer. Long enough for a sentence or two of context. */
29
30
  export const MAX_CUSTOM_ANSWER = 2000;
31
+ /** Older live question cards predate requestType and only omit the permission
32
+ * tool. Keep those actionable too, while excluding other durable proposal
33
+ * card kinds that share requestId. */
34
+ export function isPersistentQuestionCard(card) {
35
+ return Boolean(card && (card.requestType === "question" || card.questionRequest ||
36
+ (!card.requestType && !card.tool && !card.routineRequest && !card.skillRequest && !card.profileRequest &&
37
+ !card.modelRequest && !card.tighteningRequest && !card.teamSetupRequest)));
38
+ }
39
+ /** System/provider completion ends the live run, not the human's question. */
40
+ export function shouldSettleRequestCard(card, source) {
41
+ return !isPersistentQuestionCard(card) || source === "user";
42
+ }
30
43
  function text(value, limit) {
31
44
  if (typeof value !== "string")
32
45
  return undefined;
@@ -0,0 +1,28 @@
1
+ // Spoken answers to a permission card, shared by both call modes. Anything
2
+ // else is read as a reply to the bot, not as consent — an approval must
3
+ // never be granted by a sentence that merely contained the word "sure".
4
+ export const YES = /^(yes|yeah|yep|yup|sure|ok|okay|go ahead|do it|allow|approve|approved|fine|please do)\b/i;
5
+ export const NO = /^(no|nope|don'?t|do not|stop|deny|denied|cancel|never|skip it)\b/i;
6
+ const FILLER = /^(?:(?:um+|uh+|er+|so|well|hmm+)[\s,.!?-]+)+/i;
7
+ /** On a Live call these only hedge: "okay…" then a pause then "wait, what
8
+ * does it delete?", a bystander's "okay", or the voice's own "Okay, I need
9
+ * your permission" heard back through a phone's speaker. They are filler
10
+ * there, never the answer; a clear yes or no after them still counts. */
11
+ const LIVE_HEDGES = /^(?:(?:um+|uh+|er+|so|well|hmm+|ok|okay|sure|fine)(?:[\s,.!?-]+|$))+/i;
12
+ // A Live approval grants the entire action on the card. Extra conditions,
13
+ // questions or corrections are not permission for that unchanged action.
14
+ const LIVE_YES = /^(?:yes|yeah|yep|yup|go ahead|do it|allow(?: it)?|approve(?: it)?|approved|please do)(?:[\s,]+(?:please|go ahead|do it))*[.!]*$/i;
15
+ /** "allow", "deny", or null when the words are not a clear decision. The
16
+ * answer must open the utterance; filler before it ("uh, yes") is dropped.
17
+ * `live`: a Live call, whose microphone stays open while the voice speaks
18
+ * and which decides after a short quiet window — hedges never decide there.
19
+ * `turns` (take turns) keeps its own rule: its microphone is closed while
20
+ * the bot talks. */
21
+ export function spokenConsent(text, mode = "turns") {
22
+ const said = text.trim().replace(mode === "live" ? LIVE_HEDGES : FILLER, "");
23
+ if ((mode === "live" ? LIVE_YES : YES).test(said))
24
+ return "allow";
25
+ if (NO.test(said))
26
+ return "deny";
27
+ return null;
28
+ }
@@ -0,0 +1,124 @@
1
+ /** "approval": a provider or peer ask the voice may decide with a strict yes/no.
2
+ * "review": a harness-native proposal (skill, routine, profile, default
3
+ * model, tightening, team setup) that must be reviewed on screen.
4
+ * "question": a provider question. An expired proposal is settled: nothing
5
+ * can answer it any more. */
6
+ export function liveCardKind(card) {
7
+ if (!card?.requestId || card.answered || card.dismissed || card.expired)
8
+ return null;
9
+ if (card.skillRequest || card.routineRequest || card.profileRequest || card.modelRequest || card.tighteningRequest || card.teamSetupRequest) {
10
+ return "review";
11
+ }
12
+ return card.tool ? "approval" : "question";
13
+ }
14
+ /** Why a decision made on a Live call must not reach this card, or null to
15
+ * deliver it. A card settled a moment earlier (answered or dismissed on
16
+ * screen) is refused before anything runs: delivering to it would add a
17
+ * false "the action was not run" line after the tap did run it. A card that
18
+ * is not on the thread is left to the normal answer path. */
19
+ export function liveDecisionRefusal(card) {
20
+ if (!card)
21
+ return null;
22
+ const kind = liveCardKind(card);
23
+ if (kind === "review")
24
+ return "This request is reviewed on screen.";
25
+ return kind ? null : "The request is no longer open.";
26
+ }
27
+ const TOOL_PHRASES = {
28
+ Bash: "run a command",
29
+ Read: "read a file",
30
+ Write: "write a file",
31
+ Edit: "edit a file",
32
+ WebFetch: "fetch a web page",
33
+ WebSearch: "search the web",
34
+ schedule_routine: "schedule a routine",
35
+ manage_routine: "change a routine",
36
+ stage_skill: "enable a learned skill",
37
+ update_skill: "update a learned skill",
38
+ update_profile: "update its profile",
39
+ shell: "run a command",
40
+ edit: "edit a file",
41
+ read: "read a file",
42
+ fetch: "fetch a web page",
43
+ delete: "delete a file",
44
+ think: "think",
45
+ other: "take an action",
46
+ tool: "use a tool",
47
+ };
48
+ /** Same rule as toolLabel in src/components/ApprovalCard.tsx, in English. */
49
+ export function toolPhrase(tool) {
50
+ if (!tool)
51
+ return "take an action";
52
+ return TOOL_PHRASES[tool] ?? tool.replace(/^mcp__[^_]+__/, "").replace(/_/g, " ");
53
+ }
54
+ const DETAIL_LIMIT = 400;
55
+ function duration(ms) {
56
+ const seconds = Math.max(0, Math.round(ms / 1_000));
57
+ if (seconds < 60)
58
+ return `${seconds} second${seconds === 1 ? "" : "s"}`;
59
+ const minutes = Math.round(seconds / 60);
60
+ return `${minutes} minute${minutes === 1 ? "" : "s"}`;
61
+ }
62
+ /** A bot's step as the voice may name it: the tool's own spoken label, or
63
+ * the tool's name ("team notes: query database") — never its arguments,
64
+ * which can hold paths, queries or private text. */
65
+ export function liveStepLabel(tool) {
66
+ if (tool.spoken?.trim())
67
+ return brief(tool.spoken, 80);
68
+ const parts = tool.name.split("__");
69
+ if (parts[0] === "mcp" && parts.length >= 3) {
70
+ const server = parts[1].replace(/[-_]+/g, " ").trim();
71
+ const action = parts.slice(2).join(" ").replace(/_+/g, " ").trim();
72
+ return brief(`${server}: ${action}`, 80);
73
+ }
74
+ return toolPhrase(tool.name);
75
+ }
76
+ function brief(text, limit = DETAIL_LIMIT) {
77
+ const flat = text.replace(/\s+/g, " ").trim().replace(/[.!?]+$/, "");
78
+ return flat.length <= limit ? flat : `${flat.slice(0, limit).replace(/\s+\S*$/, "")}…`;
79
+ }
80
+ // The voice IS the bot to the person on the call, so everything below is
81
+ // either said as the bot ("I …") or addressed to the voice as "you". Never
82
+ // name the bot in the third person: the voice then talks about itself as a
83
+ // separate "backend" (seen in the first test calls).
84
+ export function spokenApprovalPrompt(card) {
85
+ const detail = brief(card.subtitle);
86
+ return `I want to ${toolPhrase(card.tool)}.${detail ? ` ${detail}.` : ""} May I?`;
87
+ }
88
+ export function spokenReviewPrompt(card) {
89
+ const title = brief(card.title || "a proposal", 200);
90
+ return `Tell the user, in your own words, that you need their decision in the chat: ${title}. They review it on screen and choose there; it cannot be decided by voice.`;
91
+ }
92
+ /** The bot waits for an app to be connected in the chat (a connector card). */
93
+ export function spokenConnectorPrompt(label) {
94
+ return `Tell the user, in your own words, that you need them to connect ${brief(label || "an app", 80)} in the chat before you can go on. It cannot be done by voice.`;
95
+ }
96
+ /** The bot waits for a credential entered on screen (a secret card). It must
97
+ * never travel through the call, so the voice never asks for it aloud. */
98
+ export function spokenSecretPrompt(label) {
99
+ return `Tell the user, in your own words, that you need ${brief(label || "a credential", 80)} entered in the chat on screen before you can go on. Never ask them to say it aloud.`;
100
+ }
101
+ export function spokenQuestionPrompt(card) {
102
+ const detail = brief(card.subtitle || card.title);
103
+ const choices = card.options.length ? ` The options are ${card.options.join(", ")}.` : "";
104
+ // brief() drops the detail's own closing mark, so the "?" is never doubled
105
+ return `Ask the user, in your own words: ${detail}?${choices} Then delegate their answer.`;
106
+ }
107
+ export const LIVE_COPY = {
108
+ notHeard: "The request was not heard clearly. Ask the user to say it again.",
109
+ notClear: "That was not a clear yes or no. Ask the user again for a clear yes or no.",
110
+ deniedMessage: "Denied by the user, on a live call.",
111
+ answeredInChat: "The user answered that request in the chat. Do not ask about it again.",
112
+ working: "You are working on the request now. Nothing is done until the result comes back; do not guess it.",
113
+ answerPassed: "You are using the user's answer now. Wait for the result.",
114
+ granted: "Thanks, I'll go ahead.",
115
+ denied: "Okay, I won't do it.",
116
+ saveFailed: (detail) => `The decision could not be saved${detail ? `: ${detail}` : "."} Ask the user to try again.`,
117
+ noAnswer: "I'm done. The result, or what went wrong, is in the chat.",
118
+ typedAnswerLead: (typed) => `About what you typed in the chat ("${brief(typed, 200)}"):`,
119
+ progress: (spoken) => `Progress: ${brief(spoken, 200)}`,
120
+ status: (workedMs, steps, lastStep, lastStepAgoMs) => `Status note, do not announce it: you are still working on it (${duration(workedMs)} so far, ${steps} step${steps === 1 ? "" : "s"}${lastStep ? `; last step: ${lastStep}, ${duration(lastStepAgoMs)} ago` : ""}).`,
121
+ signedOut: "The call has ended because the sign-in that started it has ended. Sign in again to start a new call.",
122
+ unpaired: "The call has ended because the phone that started it was unpaired from this computer.",
123
+ permissionRequest: (prompt) => `You need the user's permission. Say it in your own words: ${prompt} Ask for a clear yes or no, then delegate their answer.`,
124
+ };