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.
- package/dist/assets/{DesktopViewer-Do6lBa4C.js → DesktopViewer-bxM7NV3N.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
- package/dist/assets/{arc-DIYc7Ymx.js → arc-B5itKDcx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
- package/dist/assets/channel-CdJX6i8k.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
- package/dist/assets/index-CPYrGsv1.js +610 -0
- package/dist/assets/{index-DGBaEXrM.js → index-CPiqc_DN.js} +1 -1
- package/dist/assets/index-CzT42GR8.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
- package/dist/assets/{linear-CmOWzX0P.js → linear-BpKhuyLm.js} +1 -1
- package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-B_BKNCw5.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +1 -0
- package/dist-server/companion/src/routes.js +45 -1
- package/dist-server/container-mcp.js +99 -77
- package/dist-server/drivers/agents-proxy.js +40 -1
- package/dist-server/drivers/pi-mcp-extension.ts +245 -299
- package/dist-server/enterprise/server/index.js +1 -0
- package/dist-server/index.js +18361 -13794
- package/dist-server/local-computer-proxy.js +5 -1
- package/dist-server/local-computer.js +1 -0
- package/dist-server/mcp-gate.js +152 -20
- package/dist-server/mcp-remote-proxy.js +437 -0
- package/dist-server/openmausbot.js +8872 -7620
- package/dist-server/pair-cli.js +8872 -7620
- package/dist-server/permission-proxy.js +65 -1
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/auto-approve.js +33 -11
- package/dist-server/server/boat.js +3 -3
- package/dist-server/server/browser-live.js +14 -5
- package/dist-server/server/browser-runtime.js +6 -0
- package/dist-server/server/channel-queue.js +4 -1
- package/dist-server/server/cli.js +32 -12
- package/dist-server/server/cloud-move-http.js +9 -3
- package/dist-server/server/cloud-move.js +18 -0
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/computer-wait.js +33 -13
- package/dist-server/server/config.js +57 -10
- package/dist-server/server/container-computer.js +43 -32
- package/dist-server/server/decision-log.js +9 -6
- package/dist-server/server/delegations.js +47 -22
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +229 -20
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/grok.js +208 -3
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/agents-call.js +28 -0
- package/dist-server/server/drivers/agents-catalog.js +10 -1
- package/dist-server/server/drivers/boatagent.js +2 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +45 -15
- package/dist-server/server/drivers/claude.js +35 -9
- package/dist-server/server/drivers/codex.js +84 -10
- package/dist-server/server/drivers/openai-chat.js +37 -5
- package/dist-server/server/drivers/pi.js +48 -4
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/env-path.js +6 -2
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/guarded-requests.js +2 -1
- package/dist-server/server/index.js +1353 -311
- package/dist-server/server/live-call-controller.js +868 -0
- package/dist-server/server/live-call.js +216 -0
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/mcp-gate-config.js +43 -12
- package/dist-server/server/mcp-gate.js +127 -25
- package/dist-server/server/mcp-http.js +46 -8
- package/dist-server/server/mcp-remote-proxy.js +108 -0
- package/dist-server/server/new-bot-defaults.js +3 -0
- package/dist-server/server/peer-provenance.js +4 -2
- package/dist-server/server/permission-proxy.js +15 -1
- package/dist-server/server/plan-usage.js +797 -0
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/redact.js +2 -0
- package/dist-server/server/request-auth.js +3 -2
- package/dist-server/server/routes/desktop-viewer.js +50 -6
- package/dist-server/server/routes/live.js +98 -0
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/skill-library.js +207 -0
- package/dist-server/server/skills-library-migration.js +235 -0
- package/dist-server/server/skills.js +66 -3
- package/dist-server/server/steer-queue.js +13 -1
- package/dist-server/server/store.js +86 -4
- package/dist-server/server/surface.js +9 -3
- package/dist-server/server/team-backup.js +2 -1
- package/dist-server/server/thread-events.js +1 -1
- package/dist-server/server/tunnel.js +20 -1
- package/dist-server/server/turn-log.js +10 -0
- package/dist-server/server/turn-resources.js +124 -0
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup.js +22 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +13 -0
- package/dist-server/shared/call-consent.js +28 -0
- package/dist-server/shared/live-approval.js +124 -0
- package/dist-server/shared/live-call.js +176 -0
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/skill-md.js +10 -0
- package/dist-server/shared/team-backup.js +3 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/tool-scope-support.js +25 -0
- package/dist-server/shared/tool-scope.js +136 -0
- package/dist-server/src/lib/thread-refs.js +406 -0
- package/dist-server/vps-container-mcp.js +99 -77
- package/dist-server/workspace-backup.worker.js +103 -100
- package/enterprise/server/index.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +0 -1
- package/dist/assets/index-Cdmjrl28.css +0 -1
- package/dist/assets/index-DR7YPMkW.js +0 -597
- 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
|
|
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
|
+
};
|