openmausbot 0.1.93 → 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-CKNR3SEW.js → DesktopViewer-bxM7NV3N.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Ckrm1I1X.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
- package/dist/assets/{arc-BBdBuCgB.js → arc-B5itKDcx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CDdA3Hjs.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-AUrsx1YR.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BEqQ0Unh.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
- package/dist/assets/channel-CdJX6i8k.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CEEuvd4C.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-DyANCiHl.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-Dx_Slpw9.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dl7h7iiD.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BLx07SPb.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BMxDj3n7.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-CN66PT4H.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-6HwhsDKw.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-gWnLt7SN.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CMS5l2ea.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-CV7sQwIG.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CSyl-7vl.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BCLKL6az.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DiaZs6S1.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BeTtmt3I.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CsdBG-QQ.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BczvdbR1.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DHTTTMhi.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-RTjHyd8z.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-BDenHEH_.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BSiRSbtL.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BCAElss-.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-C4_1ZPJe.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bcb4Z75S.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiPOzxzb.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-B1wtF3ZS.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
- package/dist/assets/index-CPYrGsv1.js +610 -0
- package/dist/assets/{index-BkYwilkG.js → index-CPiqc_DN.js} +1 -1
- package/dist/assets/index-CzT42GR8.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-A19No71U.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DAO0pVan.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-NepmKEe2.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
- package/dist/assets/{linear-bq1PTDTW.js → linear-BpKhuyLm.js} +1 -1
- package/dist/assets/{mermaid.core-s4LTbQle.js → mermaid.core-B_BKNCw5.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-LtdbgHFH.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-tdbaCxiK.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DvkbpW8l.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Do4KaH-E.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BQWULOAs.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-w9kei1FU.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-D2gRy0e8.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-a2sO5_Eo.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CiPJmFEB.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DAkUUlL7.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DANf2R8Y.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BKpQmzrT.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-BtI7qLN-.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-gXJXrAG_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-GU8Po5-n.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BF4J30g6.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 +21 -0
- package/dist-server/container-mcp.js +93 -76
- package/dist-server/drivers/agents-proxy.js +38 -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 +16942 -13191
- package/dist-server/local-computer.js +1 -0
- package/dist-server/mcp-gate.js +147 -19
- package/dist-server/mcp-remote-proxy.js +437 -0
- package/dist-server/openmausbot.js +8233 -7651
- package/dist-server/pair-cli.js +8233 -7651
- 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/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/computer-wait.js +33 -13
- package/dist-server/server/config.js +44 -10
- package/dist-server/server/decision-log.js +9 -6
- package/dist-server/server/delegations.js +47 -22
- package/dist-server/server/drivers/acp/core.js +57 -8
- package/dist-server/server/drivers/acp/grok.js +208 -3
- 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/chat-mcp-tools.js +27 -14
- package/dist-server/server/drivers/claude.js +18 -6
- package/dist-server/server/drivers/codex.js +84 -10
- package/dist-server/server/drivers/openai-chat.js +11 -3
- package/dist-server/server/drivers/pi.js +48 -4
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/guarded-requests.js +2 -1
- package/dist-server/server/index.js +1143 -254
- package/dist-server/server/live-call-controller.js +868 -0
- package/dist-server/server/live-call.js +216 -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/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/live.js +98 -0
- 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 +32 -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/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/skill-md.js +10 -0
- package/dist-server/shared/team-backup.js +3 -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 +93 -76
- package/dist-server/workspace-backup.worker.js +89 -100
- package/enterprise/server/index.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-BU94VpxE.js +0 -1
- package/dist/assets/index-Biji-bfL.css +0 -1
- package/dist/assets/index-DZlhoES0.js +0 -597
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +0 -8
|
@@ -33,6 +33,7 @@ import { writeFileAtomic } from "./atomic.js";
|
|
|
33
33
|
import { DATA_DIR } from "./config.js";
|
|
34
34
|
import { redactSecretsInText } from "./redact.js";
|
|
35
35
|
import { LEARN_SOURCE_PREFIX } from "./skill-learn.js";
|
|
36
|
+
import { librarySkillFilePath, listLibrarySkills } from "./skill-library.js";
|
|
36
37
|
import { workspaceDir } from "./workspace.js";
|
|
37
38
|
import { isSkillName, parseSkillMd, scanSkillText, SKILL_FILE_MAX_BYTES } from "../shared/skill-md.js";
|
|
38
39
|
// SKILL.md parsing and the static scan live in shared/ so the package
|
|
@@ -1191,11 +1192,14 @@ export function applySkillWriteWithReceipt(botId, staged, recordApplied) {
|
|
|
1191
1192
|
* index lines only — the same progressive-disclosure shape the spec asks
|
|
1192
1193
|
* agents for. Bodies never ride the prompt; the bot reads the file when a
|
|
1193
1194
|
* task matches. */
|
|
1194
|
-
export function skillsSystemPrompt(botId) {
|
|
1195
|
+
export function skillsSystemPrompt(botId, assignedLibrary) {
|
|
1196
|
+
return composeSkillsSystemPrompt(botId, assignedLibrary);
|
|
1197
|
+
}
|
|
1198
|
+
function composeSkillsSystemPrompt(botId, assignedLibrary) {
|
|
1195
1199
|
// Reconcile links on every turn. If the workspace copy changed since its
|
|
1196
1200
|
// review, integrity filtering below removes it from native discovery too.
|
|
1197
1201
|
syncSkillLinks(botId);
|
|
1198
|
-
const enabled =
|
|
1202
|
+
const enabled = resolveBotSkills(botId, assignedLibrary).filter((skill) => skill.enabled);
|
|
1199
1203
|
if (!enabled.length) {
|
|
1200
1204
|
loggedIndexOmissions.delete(botId);
|
|
1201
1205
|
return "";
|
|
@@ -1212,7 +1216,7 @@ export function skillsSystemPrompt(botId) {
|
|
|
1212
1216
|
(omitted ? `${entries.length ? "\n" : ""}${notice(omitted, entries.length)}` : "");
|
|
1213
1217
|
for (const skill of enabled.slice(0, INDEX_MAX_SKILLS)) {
|
|
1214
1218
|
const entry = manifest[skill.name];
|
|
1215
|
-
const file = join(skillTarget(root, skill.name, entry), "SKILL.md");
|
|
1219
|
+
const file = entry ? join(skillTarget(root, skill.name, entry), "SKILL.md") : librarySkillFilePath(skill.name);
|
|
1216
1220
|
const line = `- ${skill.name}: ${skill.description} Read ${JSON.stringify(file)}.`;
|
|
1217
1221
|
if (Buffer.byteLength(block([...lines, line], enabled.length - lines.length - 1), "utf8") > INDEX_MAX_BYTES)
|
|
1218
1222
|
break;
|
|
@@ -1231,3 +1235,62 @@ export function skillsSystemPrompt(botId) {
|
|
|
1231
1235
|
}
|
|
1232
1236
|
return block(lines, omitted.length);
|
|
1233
1237
|
}
|
|
1238
|
+
// --- Skills library composition (features.skillsLibrary) ---
|
|
1239
|
+
/** One bot's private skills plus its assigned library skills, private
|
|
1240
|
+
* winning any name collision (the lane's resolution order: bot-private >
|
|
1241
|
+
* library > bundled). `undefined` assignments return exactly
|
|
1242
|
+
* `listSkills(botId)` — the flag-off contract. */
|
|
1243
|
+
export function resolveBotSkills(botId, assignedLibrary) {
|
|
1244
|
+
const own = listSkills(botId);
|
|
1245
|
+
if (!assignedLibrary?.length)
|
|
1246
|
+
return own;
|
|
1247
|
+
const ownNames = new Set(own.map((skill) => skill.name));
|
|
1248
|
+
const assigned = new Set(assignedLibrary);
|
|
1249
|
+
const fromLibrary = listLibrarySkills().filter((skill) => assigned.has(skill.name) && !ownNames.has(skill.name));
|
|
1250
|
+
return [...own, ...fromLibrary];
|
|
1251
|
+
}
|
|
1252
|
+
export function listBotSkillsWithOrigin(botId, assignedLibrary) {
|
|
1253
|
+
const own = listSkills(botId);
|
|
1254
|
+
const ownNames = new Set(own.map((skill) => skill.name));
|
|
1255
|
+
const assigned = new Set(assignedLibrary ?? []);
|
|
1256
|
+
const skills = resolveBotSkills(botId, assignedLibrary).map((skill) => ({
|
|
1257
|
+
...skill,
|
|
1258
|
+
origin: ownNames.has(skill.name) ? "private" : "library",
|
|
1259
|
+
}));
|
|
1260
|
+
const library = listLibrarySkills().filter((entry) => !ownNames.has(entry.name) && !assigned.has(entry.name));
|
|
1261
|
+
return { skills, library };
|
|
1262
|
+
}
|
|
1263
|
+
export function listMigratableSkills(botId) {
|
|
1264
|
+
return Object.entries(readManifest(botId))
|
|
1265
|
+
.map(([name, entry]) => ({
|
|
1266
|
+
name,
|
|
1267
|
+
directory: skillDirectory(botId, name, entry),
|
|
1268
|
+
sha256: entry.sha256,
|
|
1269
|
+
enabled: entry.enabled,
|
|
1270
|
+
source: entry.source,
|
|
1271
|
+
description: entry.description,
|
|
1272
|
+
...(entry.license ? { license: entry.license } : {}),
|
|
1273
|
+
...(entry.compatibility ? { compatibility: entry.compatibility } : {}),
|
|
1274
|
+
...(entry.package ? { package: { ...entry.package } } : {}),
|
|
1275
|
+
intact: skillContentMatches(botId, name, entry),
|
|
1276
|
+
}))
|
|
1277
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
1278
|
+
}
|
|
1279
|
+
/** Drop manifest entries the migration has archived. The caller archives
|
|
1280
|
+
* the bytes first and passes the sha256 it read, so a skill that changed
|
|
1281
|
+
* between read and removal is left untouched. */
|
|
1282
|
+
export function removeManifestEntry(botId, name, expectSha256) {
|
|
1283
|
+
if (!isSkillName(name))
|
|
1284
|
+
return { error: "invalid skill name" };
|
|
1285
|
+
const manifest = readManifest(botId);
|
|
1286
|
+
const entry = manifest[name];
|
|
1287
|
+
if (!entry)
|
|
1288
|
+
return { removed: true };
|
|
1289
|
+
if (entry.sha256 !== expectSha256) {
|
|
1290
|
+
return { error: `skill "${name}" changed since it was read — leaving it in place` };
|
|
1291
|
+
}
|
|
1292
|
+
delete manifest[name];
|
|
1293
|
+
writeManifest(botId, manifest);
|
|
1294
|
+
syncSkillLinks(botId);
|
|
1295
|
+
return { removed: true };
|
|
1296
|
+
}
|
|
@@ -28,8 +28,11 @@ export function restoreSteeredMessages() {
|
|
|
28
28
|
const entry = queues.get(row.threadId) ?? { botId: row.ownerId, items: [] };
|
|
29
29
|
if (entry.botId !== row.ownerId)
|
|
30
30
|
throw new Error("queued task belongs to another bot");
|
|
31
|
+
// a 1:1 line is only ever stamped "call"; "api" belongs to channel rows
|
|
32
|
+
const { via, ...payload } = row.payload;
|
|
31
33
|
entry.items.push({
|
|
32
|
-
...
|
|
34
|
+
...payload,
|
|
35
|
+
...(via === "call" ? { via } : {}),
|
|
33
36
|
messageId: row.id,
|
|
34
37
|
prompt: row.payload.prompt ?? row.payload.text,
|
|
35
38
|
// rows queued before timestamps were kept read as queued at restore
|
|
@@ -83,6 +86,7 @@ export function queueSteeredMessage(botId, threadId, text, options = {}) {
|
|
|
83
86
|
sender: options.sender,
|
|
84
87
|
trigger: options.trigger,
|
|
85
88
|
queuedAt: Date.now(),
|
|
89
|
+
via: options.via,
|
|
86
90
|
};
|
|
87
91
|
saveChatFollowup({ id, kind: "bot", ownerId: botId, threadId, payload: item });
|
|
88
92
|
entry.items.push(item);
|
|
@@ -112,6 +116,13 @@ export function hasQueuedSteeredMessages(botId, threadId) {
|
|
|
112
116
|
const entry = queues.get(threadId);
|
|
113
117
|
return entry?.botId === botId && entry.items.length > 0;
|
|
114
118
|
}
|
|
119
|
+
/** Whether one send still waits in its thread's queue. False while it is
|
|
120
|
+
* out of the queue: drained onto the thread, held for a steer into the
|
|
121
|
+
* running turn, or cancelled (editing a queued line cancels it). */
|
|
122
|
+
export function isSteeredMessageQueued(botId, threadId, queueId) {
|
|
123
|
+
const entry = queues.get(threadId);
|
|
124
|
+
return entry?.botId === botId && entry.items.some((item) => item.messageId === queueId);
|
|
125
|
+
}
|
|
115
126
|
/** Drain every queue whose task is idle: append the held lines (leaf is now
|
|
116
127
|
* the finished turn's last item), then one run per thread whose prompt is
|
|
117
128
|
* the drained group's texts separated by a blank line. `userMessage` is the last appended line
|
|
@@ -161,6 +172,7 @@ export function drainSteeredMessages(store, run, isBlocked) {
|
|
|
161
172
|
queueId: item.messageId,
|
|
162
173
|
peerAsk: item.peerAsk,
|
|
163
174
|
sender: item.sender,
|
|
175
|
+
...(item.via ? { via: item.via } : {}),
|
|
164
176
|
}));
|
|
165
177
|
}
|
|
166
178
|
const last = appended.at(-1);
|
|
@@ -6,6 +6,7 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
import { chmodSync, existsSync, readFileSync, mkdirSync, rmSync, statSync, unlinkSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { writeFileAtomic } from "./atomic.js";
|
|
9
|
+
import { parseToolScope, toolScopeWidens } from "../shared/tool-scope.js";
|
|
9
10
|
import { ensureSections, readSections, changeEmptySection } from "./section-context.js";
|
|
10
11
|
import { removeBotFolder, soulFile, soulHash, writeSoulMirror } from "./bot-folder.js";
|
|
11
12
|
import { peerAllowKey } from "./peer-approval-key.js";
|
|
@@ -32,8 +33,10 @@ export const taskWireProjectionIsExact = true;
|
|
|
32
33
|
/** The typed wire projection for one task. Pairs with the assertion above:
|
|
33
34
|
* returning WireTask means an undeclared server field cannot ride silently. */
|
|
34
35
|
export function toWireTask(task) {
|
|
35
|
-
const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, appliedCompactionId: _appliedCompactionId, contextFloor: _contextFloor, lastContextModel: _lastContextModel, surfaceSource
|
|
36
|
-
|
|
36
|
+
const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, appliedCompactionId: _appliedCompactionId, contextFloor: _contextFloor, lastContextModel: _lastContextModel, surfaceSource, ...wire } = task;
|
|
37
|
+
// Who pinned stays private. A client learns only whether the pin is the
|
|
38
|
+
// machine's own record, so it never presents one as the person's choice.
|
|
39
|
+
return surfaceSource === "auto" && wire.surface !== undefined ? { ...wire, surfaceAuto: true } : wire;
|
|
37
40
|
}
|
|
38
41
|
const TASK_PATCH_FIELDS = [
|
|
39
42
|
"title", "projectId", "modelSelection", "approvalMode", "autoApprove", "alwaysAllow",
|
|
@@ -1586,6 +1589,9 @@ export class Store {
|
|
|
1586
1589
|
return this.completeNewBotSelection(requested ?? this.defaultSelection());
|
|
1587
1590
|
}
|
|
1588
1591
|
createBot(profile = {}, opts = {}) {
|
|
1592
|
+
const toolScope = parseToolScope(profile.toolScope);
|
|
1593
|
+
if (!toolScope.ok)
|
|
1594
|
+
throw new Error(toolScope.error);
|
|
1589
1595
|
this.rememberSections([profile.section]);
|
|
1590
1596
|
const name = profile.name?.trim() || pickBotName(this.bots.map((b) => b.name));
|
|
1591
1597
|
const section = sectionKey(profile.section);
|
|
@@ -1603,6 +1609,7 @@ export class Store {
|
|
|
1603
1609
|
...(profile.mascotBody ? { mascotBody: profile.mascotBody } : {}),
|
|
1604
1610
|
// Restricted from its first frame: no one else is ever told it exists.
|
|
1605
1611
|
...(profile.visibility && profile.visibility !== "everyone" ? { visibility: structuredClone(profile.visibility) } : {}),
|
|
1612
|
+
...(toolScope.scope ? { toolScope: toolScope.scope } : {}),
|
|
1606
1613
|
unread: false,
|
|
1607
1614
|
modelSelection: this.newBotSelection(profile.modelSelection),
|
|
1608
1615
|
resumeCursors: {},
|
|
@@ -1623,8 +1630,10 @@ export class Store {
|
|
|
1623
1630
|
activity: "idle",
|
|
1624
1631
|
busy: false,
|
|
1625
1632
|
}];
|
|
1633
|
+
// Persist the selection in the first record, before publishing the bot
|
|
1634
|
+
// or starting its greeting. An interrupted creation cannot inherit tools.
|
|
1635
|
+
this.saveBots([bot, ...this.bots]);
|
|
1626
1636
|
this.bots.unshift(bot);
|
|
1627
|
-
this.saveBots();
|
|
1628
1637
|
// The folder exists from the first moment, so the user can open
|
|
1629
1638
|
// SOUL.md before the bot has said a word. The record is canonical: a
|
|
1630
1639
|
// mirror-write failure must never fail bot creation.
|
|
@@ -1838,6 +1847,18 @@ export class Store {
|
|
|
1838
1847
|
throw new Error(parsed.error);
|
|
1839
1848
|
patch = { ...patch, connectorTools: parsed.grants };
|
|
1840
1849
|
}
|
|
1850
|
+
if (Object.hasOwn(patch, "toolScope")) {
|
|
1851
|
+
const parsed = parseToolScope(patch.toolScope);
|
|
1852
|
+
if (!parsed.ok)
|
|
1853
|
+
throw new Error(parsed.error);
|
|
1854
|
+
patch = { ...patch, toolScope: parsed.scope };
|
|
1855
|
+
}
|
|
1856
|
+
const wideningToolScope = Object.hasOwn(patch, "toolScope") && toolScopeWidens(bot.toolScope, patch.toolScope);
|
|
1857
|
+
const nextToolScope = patch.toolScope;
|
|
1858
|
+
if (wideningToolScope) {
|
|
1859
|
+
patch = { ...patch };
|
|
1860
|
+
delete patch.toolScope;
|
|
1861
|
+
}
|
|
1841
1862
|
// Runtime revocations must become effective in memory even when disk is
|
|
1842
1863
|
// unavailable. Profile edits use the separate atomic path below.
|
|
1843
1864
|
Object.assign(bot, patch);
|
|
@@ -1850,7 +1871,14 @@ export class Store {
|
|
|
1850
1871
|
}
|
|
1851
1872
|
bot.unread = bot.tasks.some(taskCountsAsBotUnread);
|
|
1852
1873
|
}
|
|
1853
|
-
|
|
1874
|
+
if (wideningToolScope) {
|
|
1875
|
+
// New authority becomes live only after its durable save succeeds.
|
|
1876
|
+
// Other runtime revocations above still take effect on a failed write.
|
|
1877
|
+
this.saveBots(this.bots.map((candidate) => candidate === bot ? { ...bot, toolScope: nextToolScope } : candidate));
|
|
1878
|
+
bot.toolScope = nextToolScope;
|
|
1879
|
+
}
|
|
1880
|
+
else
|
|
1881
|
+
this.saveBots();
|
|
1854
1882
|
this.emit({ type: "bot", botId: id });
|
|
1855
1883
|
return bot;
|
|
1856
1884
|
}
|
|
@@ -85,7 +85,13 @@ export function surfaceLabel(surface) {
|
|
|
85
85
|
/** Said once per task, so the person hears the place before the first click
|
|
86
86
|
* lands there. Same words as the panel and the composer chip. */
|
|
87
87
|
const RESTATE_SENTENCE = " Before your first action on a screen or page in a task, say in one short sentence where you are working, using that same name.";
|
|
88
|
-
|
|
88
|
+
/** Where a person moves a conversation: the Computer panel is the one place
|
|
89
|
+
* control both interface modes show (Simple has no composer chip). */
|
|
90
|
+
const PLACE_CONTROL = "the Computer panel";
|
|
91
|
+
/** Without select_computer the person has to switch; with it, surfacePrompt
|
|
92
|
+
* swaps exactly this phrase for the model's own switch. */
|
|
93
|
+
const ASK_TO_SWITCH = `explain the mismatch and ask the user to change where this conversation works in ${PLACE_CONTROL}`;
|
|
94
|
+
const SURFACE_AUTHORITY = ` For browser and computer tasks, use OpenMausBot's mounted browser/computer tools first: inspect the target, perform the action, and verify its result before claiming success. Discover deferred tools by their server/name when needed. Do not substitute the provider's own desktop, a shell-launched browser, or another automation path for the selected OpenMausBot surface. Ordinary code and file tasks may still use their normal tools. Announcing an action is not performing it. A request naming another place does not move these tools: this computer is the user's host, Local VM is an isolated desktop, the cloud computer is remote, and the built-in browser is a separate browser. If the requested place differs from the mounted one, ${ASK_TO_SWITCH}; never act on a different computer or describe a host window as a VM.`;
|
|
89
95
|
/** A Cloud home offers neither this computer nor a Local VM
|
|
90
96
|
* (server/cloud-home.ts), so its bots are told only about the places it has.
|
|
91
97
|
* Each pair rewrites one phrase of the desktop wording above. */
|
|
@@ -116,13 +122,13 @@ export function surfacePrompt(mounted, opts = {}) {
|
|
|
116
122
|
}
|
|
117
123
|
if (text)
|
|
118
124
|
text += RESTATE_SENTENCE + (opts.canSelect
|
|
119
|
-
? SURFACE_AUTHORITY.replace(
|
|
125
|
+
? SURFACE_AUTHORITY.replace(ASK_TO_SWITCH, "inspect connected choices with select_computer and select the requested available place; on a pending result end this turn so OpenMausBot can reconnect the correct tools and continue the original request")
|
|
120
126
|
: SURFACE_AUTHORITY);
|
|
121
127
|
else if (!opts.note && !opts.canSelect) {
|
|
122
128
|
text = " No computer or built-in browser tools are mounted this turn. You cannot open apps, click, or inspect a screen through OpenMausBot. If asked for screen work, explain this and ask the user to choose and connect a computer in the Computer panel; do not claim to have opened or checked it.";
|
|
123
129
|
}
|
|
124
130
|
if (opts.pinned) {
|
|
125
|
-
text += ` This conversation is pinned to ${surfaceLabel(opts.pinned)}; changing places requires ${opts.canSelect ? "select_computer or " : ""}the
|
|
131
|
+
text += ` This conversation is pinned to ${surfaceLabel(opts.pinned)}; changing places requires ${opts.canSelect ? "select_computer or " : ""}the user's choice in ${PLACE_CONTROL}, not a different tool name.`;
|
|
126
132
|
}
|
|
127
133
|
if (opts.canSelect)
|
|
128
134
|
text += " For a screen task, use select_computer with no arguments when you need to inspect the actual available targets. Choose the requested place from its result; if the user left the place open, use a suitable available target or surface auto instead of asking them to operate the menu. Browser-only work can stay in Browser; when it needs desktop apps or capabilities the current Browser lacks, select an available Local VM without asking the user to switch it manually. Choose before taking actions, and do not repeat actions already completed if the task must continue elsewhere. OpenMausBot can start an existing configured computer and highlight the selected target. If the right tools are already mounted, use them directly. On a pending switch, end this turn: the original request resumes automatically with the new tools, and then you must carry out the task. Only ask for input for a genuine blocker, such as missing setup, required sign-in or an approval. Never silently replace an explicitly requested VM with the host desktop.";
|
|
@@ -119,6 +119,7 @@ export function createTeamBackup(store, routines, name) {
|
|
|
119
119
|
// so the team's shape is not lost, but the import below still lands
|
|
120
120
|
// every bot grant-less — restoring them is a deliberate later choice.
|
|
121
121
|
...(bot.connectorTools ? { connectorTools: structuredClone(bot.connectorTools) } : {}),
|
|
122
|
+
...(bot.toolScope !== undefined ? { toolScope: structuredClone(bot.toolScope) } : {}),
|
|
122
123
|
memory: memoryFor(bot.id),
|
|
123
124
|
activeTask: bot.threadId, tasks: history(bot),
|
|
124
125
|
})),
|
|
@@ -188,7 +189,7 @@ export function importTeamBackup(store, routines, input, selection, options = {}
|
|
|
188
189
|
bots.push(bot);
|
|
189
190
|
botIds.set(source.key, bot.id);
|
|
190
191
|
store.patchBot(bot.id, { composio: false, computer: "off", browser: false, approvalMode: "ask", autoApprove: false,
|
|
191
|
-
connectorTools: {}, hidden: source.hidden, chiefOfStaff: source.chiefOfStaff, playbooks: source.playbooks });
|
|
192
|
+
connectorTools: {}, toolScope: source.toolScope, hidden: source.hidden, chiefOfStaff: source.chiefOfStaff, playbooks: source.playbooks });
|
|
192
193
|
if (source.memory)
|
|
193
194
|
restoreMemory(bot.id, source.memory);
|
|
194
195
|
}
|
|
@@ -187,7 +187,7 @@ function isRuntimeEvent(value) {
|
|
|
187
187
|
typeof value.waitedMs === "number" &&
|
|
188
188
|
Number.isFinite(value.waitedMs) &&
|
|
189
189
|
value.waitedMs >= 0 &&
|
|
190
|
-
(value.outcome === "acquired" || value.outcome === "gave_up" || value.outcome === "stopped"));
|
|
190
|
+
(value.outcome === "acquired" || value.outcome === "gave_up" || value.outcome === "parked" || value.outcome === "stopped"));
|
|
191
191
|
case "item.started":
|
|
192
192
|
return (value.itemType === "tool" || value.itemType === "reasoning") && stringOrMissing(value.title);
|
|
193
193
|
case "item.updated":
|
|
@@ -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;
|
|
@@ -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
|
+
}
|