openmausbot 0.1.93 → 0.1.95
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-BwWpc4RK.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Ckrm1I1X.js → abnfDiagram-O67JEVCF-jsQkIqTn.js} +1 -1
- package/dist/assets/{arc-BBdBuCgB.js → arc-7kei4Hsu.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CDdA3Hjs.js → architectureDiagram-NJMV4G6O-CRWDFDMq.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-AUrsx1YR.js → blockDiagram-BEXU5L5S-CTKW_IKs.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BEqQ0Unh.js → c4Diagram-YGBWAQC7-BBTW7-6M.js} +1 -1
- package/dist/assets/channel-DS2NbT5x.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CEEuvd4C.js → chunk-2Q5K7J3B-M_Ox-MkD.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-DyANCiHl.js → chunk-3FUC2YCW-D2YiN-Ud.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-Dx_Slpw9.js → chunk-5VM5RSS4-BmVYJ0f8.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dl7h7iiD.js → chunk-GTNCS2PH-BtiLVVnZ.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BLx07SPb.js → chunk-GWA4HPMP-DFYSb6J6.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BMxDj3n7.js → chunk-JWPE2WC7-mknKBzgV.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-CN66PT4H.js → chunk-NETBCI7D-DgSYGiOu.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-6HwhsDKw.js → chunk-WEXAMYUT-C7ZvuguH.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-gWnLt7SN.js → chunk-XXDRQBXY-CPqxINpA.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CMS5l2ea.js → classDiagram-v2-NBCMYWYE-Cd6xfJaC.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-CV7sQwIG.js → cose-bilkent-JH36ORCC-BSXY5qRh.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CSyl-7vl.js → cynefin-OW5HDTMX-D_lhEMj1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BCLKL6az.js → cynefinDiagram-VND7K2PF-Da9FrFwk.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DiaZs6S1.js → dagre-6A5THRUB-T77jMOXe.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BeTtmt3I.js → diagram-22UHCM2B-Dkj96X5E.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CsdBG-QQ.js → diagram-3UASUU5V-CMi9zlD_.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BczvdbR1.js → diagram-ATOU4E4O-CXK6YUfS.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DHTTTMhi.js → diagram-CDSNMT55-BE7B1u9Q.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-RTjHyd8z.js → diagram-MLGK6HIB-CCMQphKr.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-BDenHEH_.js → diagram-MPIPVDR6-iHJ_-tGH.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BSiRSbtL.js → ebnfDiagram-ZINNZB2B-DONsCWQ7.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BCAElss-.js → elk-276RUBZZ-CU_WOaXk.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-C4_1ZPJe.js → erDiagram-OPXOYQCR-BeYwM04G.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bcb4Z75S.js → flowDiagram-KWPJA3E3-Bdw9DqEl.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiPOzxzb.js → ganttDiagram-FUAMR5RP-C9iwq3qm.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-B1wtF3ZS.js → gitGraphDiagram-X574FWY7-Dx-VWzUg.js} +1 -1
- package/dist/assets/{index-BkYwilkG.js → index-KygHRE3E.js} +1 -1
- package/dist/assets/index-NF7XCHAy.css +1 -0
- package/dist/assets/index-X_5ydXIE.js +610 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-A19No71U.js → infoDiagram-VRGFBTTK-BG7uaEd4.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js → ishikawaDiagram-OU5B5YK6-D72WAXnM.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DAO0pVan.js → journeyDiagram-ZHPQQLJL-DKH1gjYT.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-NepmKEe2.js → kanban-definition-PNTS6WVX-3upm9JUT.js} +1 -1
- package/dist/assets/{linear-bq1PTDTW.js → linear-CcR4pc0-.js} +1 -1
- package/dist/assets/{mermaid.core-s4LTbQle.js → mermaid.core-C6yU5FSS.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-LtdbgHFH.js → mindmap-definition-NLK3R4M7-DOI2xEuv.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-tdbaCxiK.js → pegDiagram-GJSIUBJH-BVWJSgLd.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DvkbpW8l.js → pieDiagram-5QR66LMP-jtbYezhq.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Do4KaH-E.js → quadrantDiagram-O4NWA36T-g0u8mtPX.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BQWULOAs.js → railroadDiagram-XR7U4H2S-k8689Wny.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-w9kei1FU.js → requirementDiagram-PLB6GJNP-DsJ22PDt.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-D2gRy0e8.js → sankeyDiagram-IPEJSGJF-CneWBRgG.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-a2sO5_Eo.js → sequenceDiagram-PO4LG4MO-DWsFCWap.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CiPJmFEB.js → sizeCapture-INFHLROL-Bvf0YE3S.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DAkUUlL7.js → stateDiagram-v2-GCMORJYK-Bxb3Iei8.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DANf2R8Y.js → swimlanes-2SLR337P-DLkk7W4A.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BKpQmzrT.js → timeline-definition-EJHVYXUP-KR9bIHYC.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-BtI7qLN-.js → usecaseDiagram-POWQR4AR-DfsOrj8t.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-gXJXrAG_.js → vennDiagram-UO4OBE2U-DRSTzG3h.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-GU8Po5-n.js → wardleyDiagram-VNRHLVJA-DtEdhPgT.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BF4J30g6.js → xychartDiagram-PMCCYNJV-B-XhTxjG.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +3 -1
- package/dist-server/companion/src/routes.js +60 -0
- package/dist-server/container-mcp.js +94 -77
- package/dist-server/drivers/agents-proxy.js +96 -39
- package/dist-server/drivers/pi-mcp-extension.ts +245 -299
- package/dist-server/enterprise/server/index.js +3 -1
- package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +35 -19
- package/dist-server/index.js +18242 -13645
- package/dist-server/local-computer.js +3 -1
- package/dist-server/mcp-gate.js +147 -19
- package/dist-server/mcp-remote-proxy.js +437 -0
- package/dist-server/openmausbot.js +9618 -10058
- package/dist-server/pair-cli.js +9618 -10058
- package/dist-server/permission-proxy.js +65 -1
- package/dist-server/proxy-paths.js +3 -1
- package/dist-server/server/account-signin.js +0 -2
- package/dist-server/server/activity.js +201 -0
- package/dist-server/server/auto-approve.js +39 -11
- package/dist-server/server/automatic-recovery.js +1 -1
- package/dist-server/server/boat-create-idempotency.js +0 -7
- package/dist-server/server/boat-delete-journal.js +0 -4
- package/dist-server/server/bot-package.js +1 -19
- package/dist-server/server/browser-live.js +14 -5
- package/dist-server/server/browser-runtime.js +7 -2
- package/dist-server/server/channel-queue.js +4 -1
- package/dist-server/server/chief-of-staff.js +2 -3
- package/dist-server/server/cli.js +32 -12
- package/dist-server/server/cloud-backend.js +0 -4
- package/dist-server/server/cloud-computer-tools.js +110 -0
- package/dist-server/server/cloud-home.js +0 -1
- package/dist-server/server/cloud-lending.js +0 -3
- package/dist-server/server/cloud-move-http.js +9 -3
- package/dist-server/server/cloud-move.js +18 -0
- package/dist-server/server/composio.js +31 -2
- package/dist-server/server/computer-control.js +0 -15
- package/dist-server/server/computer-observation.js +0 -74
- package/dist-server/server/computer-wait.js +33 -13
- package/dist-server/server/config.js +73 -11
- package/dist-server/server/container-computer.js +7 -19
- package/dist-server/server/contracts.js +9 -0
- package/dist-server/server/control-client.js +4 -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 +64 -13
- package/dist-server/server/drivers/acp/grok.js +208 -3
- package/dist-server/server/drivers/agents-call.js +70 -29
- package/dist-server/server/drivers/agents-catalog.js +34 -12
- package/dist-server/server/drivers/agents-proxy.js +8 -30
- package/dist-server/server/drivers/antigravity.js +1 -4
- package/dist-server/server/drivers/boatagent.js +25 -4
- package/dist-server/server/drivers/cerebras.js +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +29 -19
- package/dist-server/server/drivers/claude.js +33 -19
- package/dist-server/server/drivers/codex.js +84 -10
- package/dist-server/server/drivers/openai-chat.js +14 -9
- package/dist-server/server/drivers/pi.js +48 -4
- package/dist-server/server/drivers/retry.js +8 -4
- package/dist-server/server/engine-install.js +1 -5
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/guarded-requests.js +2 -1
- package/dist-server/server/harness/registry.js +0 -1
- package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
- package/dist-server/server/index.js +1929 -507
- package/dist-server/server/lending-memory.js +12 -8
- 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/memory-entries.js +38 -8
- package/dist-server/server/memory-organize.js +1 -8
- package/dist-server/server/memory-topics.js +4 -4
- package/dist-server/server/memory-upkeep.js +13 -15
- package/dist-server/server/message-file.js +5 -9
- package/dist-server/server/new-bot-defaults.js +3 -0
- package/dist-server/server/outbound-counts.js +79 -0
- package/dist-server/server/outbound-requests.js +60 -0
- package/dist-server/server/package-import.js +1 -1
- package/dist-server/server/peer-delivery.js +1 -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 +3 -1
- package/dist-server/server/redact.js +2 -0
- package/dist-server/server/request-auth.js +3 -2
- package/dist-server/server/room-handoffs.js +5 -13
- package/dist-server/server/routes/live.js +98 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +5 -1
- package/dist-server/server/setup-mode.js +0 -2
- package/dist-server/server/skill-learn.js +0 -1
- package/dist-server/server/skill-library.js +195 -3
- 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 +41 -5
- package/dist-server/server/surface.js +74 -23
- package/dist-server/server/system-prompt.js +4 -4
- package/dist-server/server/team-backup.js +2 -1
- package/dist-server/server/team-memory.js +209 -0
- package/dist-server/server/thread-events.js +3 -3
- 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 +4 -14
- package/dist-server/server/workspace.js +85 -47
- package/dist-server/shared/activity.js +3 -0
- package/dist-server/shared/ask-question.js +13 -0
- package/dist-server/shared/bot-avatar.js +8 -0
- package/dist-server/shared/call-consent.js +28 -0
- package/dist-server/shared/connector-scopes.js +78 -0
- package/dist-server/shared/live-approval.js +124 -0
- package/dist-server/shared/live-call.js +176 -0
- package/dist-server/shared/outbound.js +128 -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/shared/tool-surface.js +3 -2
- package/dist-server/src/lib/thread-refs.js +406 -0
- package/dist-server/vps-container-mcp.js +94 -77
- package/dist-server/workspace-backup.worker.js +89 -100
- package/enterprise/server/index.js +3 -1
- 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
- package/dist-server/server/drivers/chat-boat-tools.js +0 -90
- package/dist-server/server/drivers/grokagent.js +0 -4
- package/dist-server/server/openmaus-status-capsule.js +0 -329
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Skills library migration (features.skillsLibrary, skills lane S1).
|
|
2
|
+
//
|
|
3
|
+
// Per-bot skill copies move into the shared library, deduplicated by the
|
|
4
|
+
// reviewed SKILL.md sha256. Originals are archived under the library root —
|
|
5
|
+
// write-once, never deleted — and the per-bot manifest entry is dropped
|
|
6
|
+
// only after the archive exists and the assignment is durably recorded. The
|
|
7
|
+
// sweep is idempotent through state, not markers: a migrated skill has no
|
|
8
|
+
// manifest entry left to re-read, and an existing archive short-circuits
|
|
9
|
+
// the move.
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
14
|
+
import { installLibrarySkill, libraryArchiveDirectory, readSkillLibraryIndex, skillsLibraryRoot, } from "./skill-library.js";
|
|
15
|
+
import { listMigratableSkills, removeManifestEntry } from "./skills.js";
|
|
16
|
+
function sha256Hex(text) {
|
|
17
|
+
return createHash("sha256").update(text).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
/** Move a per-bot skill directory into its archive slot. Same-volume moves
|
|
20
|
+
* rename; a cross-device fallback copies recursively and verifies the
|
|
21
|
+
* SKILL.md hash before the copy counts as the archive. */
|
|
22
|
+
function archiveSkillDirectory(source, archive, expectSha256) {
|
|
23
|
+
mkdirSync(dirname(archive), { recursive: true, mode: 0o700 });
|
|
24
|
+
// Every source directory gets its own slot: the same sha256 can be
|
|
25
|
+
// reinstalled after its manifest entry was cleared, so a collision takes
|
|
26
|
+
// a hash-qualified name, then numeric suffixes, never a silent drop.
|
|
27
|
+
let destination = archive;
|
|
28
|
+
if (existsSync(destination)) {
|
|
29
|
+
const stem = `${archive}.${expectSha256}`;
|
|
30
|
+
destination = stem;
|
|
31
|
+
let suffix = 1;
|
|
32
|
+
while (existsSync(destination))
|
|
33
|
+
destination = `${stem}.${suffix++}`;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
renameSync(source, destination);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code !== "EXDEV")
|
|
40
|
+
throw error;
|
|
41
|
+
cpSync(source, destination, { recursive: true, force: false, errorOnExist: true });
|
|
42
|
+
}
|
|
43
|
+
const archived = readFileSync(join(destination, "SKILL.md"), "utf8");
|
|
44
|
+
if (sha256Hex(archived) !== expectSha256) {
|
|
45
|
+
throw new Error("the archived copy does not match the reviewed sha256");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Migrate one bot's per-bot skill copies into the library. Org-stamped
|
|
49
|
+
* skills carry their stamp onto the library entry, so an organization
|
|
50
|
+
* install becomes a library source instead of a parallel system. A bot is
|
|
51
|
+
* assigned only when its own copy was enabled; the first copy's enabled
|
|
52
|
+
* flag still decides the entry's reviewState. */
|
|
53
|
+
export function migrateBotSkillsToLibrary(botId, root = skillsLibraryRoot()) {
|
|
54
|
+
const outcomes = [];
|
|
55
|
+
const assigned = [];
|
|
56
|
+
for (const copy of listMigratableSkills(botId)) {
|
|
57
|
+
if (!copy.intact || !copy.directory) {
|
|
58
|
+
outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: "stored SKILL.md is missing or changed after review" });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// The shared library currently stores SKILL.md only. Moving a package
|
|
62
|
+
// with scripts/references/assets would make its relative paths vanish
|
|
63
|
+
// from the bot's usable directory, so keep that working private copy.
|
|
64
|
+
try {
|
|
65
|
+
if (readdirSync(copy.directory).some((name) => name !== "SKILL.md")) {
|
|
66
|
+
outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: "skill has auxiliary files — keeping its complete private directory" });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: "could not inspect the private skill directory" });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
let instructions;
|
|
75
|
+
try {
|
|
76
|
+
instructions = readFileSync(join(copy.directory, "SKILL.md"), "utf8");
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: `could not read the stored SKILL.md: ${error instanceof Error ? error.message : String(error)}` });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (sha256Hex(instructions) !== copy.sha256) {
|
|
83
|
+
outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: "stored SKILL.md changed after review" });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const existing = readSkillLibraryIndex(root)[copy.name];
|
|
87
|
+
if (existing && existing.sha256 !== copy.sha256) {
|
|
88
|
+
outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: `the library already has a different "${copy.name}" — rename one of them` });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// The library entry's review state is shared by every assigned bot, so
|
|
92
|
+
// deduplicating a copy whose own on/off differs from it would silently
|
|
93
|
+
// flip that bot's switch. Keep the per-bot copy in place for a human to
|
|
94
|
+
// reconcile instead of archiving and assigning it.
|
|
95
|
+
if (existing && (existing.reviewState === "approved") !== copy.enabled) {
|
|
96
|
+
outcomes.push({
|
|
97
|
+
botId,
|
|
98
|
+
name: copy.name,
|
|
99
|
+
outcome: "skipped",
|
|
100
|
+
detail: `the library "${copy.name}" is ${existing.reviewState === "approved" ? "enabled" : "disabled"} while this bot had it ${copy.enabled ? "enabled" : "disabled"} — align them before migrating`,
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!existing) {
|
|
105
|
+
const installed = installLibrarySkill({
|
|
106
|
+
name: copy.name,
|
|
107
|
+
description: copy.description,
|
|
108
|
+
instructions,
|
|
109
|
+
source: copy.source,
|
|
110
|
+
license: copy.license,
|
|
111
|
+
compatibility: copy.compatibility,
|
|
112
|
+
reviewState: copy.enabled ? "approved" : "disabled",
|
|
113
|
+
package: copy.package,
|
|
114
|
+
root,
|
|
115
|
+
});
|
|
116
|
+
if ("error" in installed) {
|
|
117
|
+
outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: installed.error });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
outcomes.push({ botId, name: copy.name, outcome: "migrated" });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
outcomes.push({ botId, name: copy.name, outcome: "deduplicated", detail: "identical sha256 already in the library" });
|
|
124
|
+
}
|
|
125
|
+
// The manifest entry is the only re-readable record of this skill, so
|
|
126
|
+
// the assignment must be durable before it goes away. Record it in the
|
|
127
|
+
// pending-assignments file the boot sweep and the standalone script both
|
|
128
|
+
// replay: if this write — or the later patch to the bot record — fails,
|
|
129
|
+
// the next boot restores the assignment instead of stranding an
|
|
130
|
+
// archived skill with no manifest and no assignment.
|
|
131
|
+
try {
|
|
132
|
+
const pendingAssignments = readPendingAssignments(root);
|
|
133
|
+
if (copy.enabled)
|
|
134
|
+
writePendingAssignments({
|
|
135
|
+
...pendingAssignments,
|
|
136
|
+
[botId]: [...new Set([...(pendingAssignments[botId] ?? []), ...assigned, copy.name])].sort(),
|
|
137
|
+
}, root);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: `could not record the assignment durably: ${error instanceof Error ? error.message : String(error)}` });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
archiveSkillDirectory(copy.directory, libraryArchiveDirectory(root, botId, copy.name), copy.sha256);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
// The library entry is idempotent, so leaving the per-bot copy in
|
|
148
|
+
// place only means the next sweep retries this skill. The assignment
|
|
149
|
+
// recorded above replays idempotently, so the extra pending entry a
|
|
150
|
+
// failed archive leaves behind is harmless.
|
|
151
|
+
outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: `archiving failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const removed = removeManifestEntry(botId, copy.name, copy.sha256);
|
|
155
|
+
if ("error" in removed) {
|
|
156
|
+
outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: removed.error });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// Assignment follows this bot's own copy.enabled: a disabled copy
|
|
160
|
+
// migrates its bytes but assigns nothing, so nobody gains a skill
|
|
161
|
+
// they never turned on.
|
|
162
|
+
if (copy.enabled)
|
|
163
|
+
assigned.push(copy.name);
|
|
164
|
+
}
|
|
165
|
+
return { ranAt: new Date().toISOString(), outcomes, assignments: assigned.length ? { [botId]: assigned } : {} };
|
|
166
|
+
}
|
|
167
|
+
export function pendingAssignmentsPath(root = skillsLibraryRoot()) {
|
|
168
|
+
return join(root, "pending-assignments.json");
|
|
169
|
+
}
|
|
170
|
+
/** Assignments waiting to reach bot records: recorded by the standalone
|
|
171
|
+
* script for a stopped server, and by the migration itself before a
|
|
172
|
+
* manifest entry is dropped. The next flag-on boot applies them through
|
|
173
|
+
* the Store, the single writer of bots.json. */
|
|
174
|
+
export function readPendingAssignments(root = skillsLibraryRoot()) {
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(readFileSync(pendingAssignmentsPath(root), "utf8"));
|
|
177
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
178
|
+
return {};
|
|
179
|
+
const result = {};
|
|
180
|
+
for (const [botId, names] of Object.entries(parsed)) {
|
|
181
|
+
if (Array.isArray(names) && names.every((name) => typeof name === "string"))
|
|
182
|
+
result[botId] = [...names];
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return {};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export function writePendingAssignments(assignments, root = skillsLibraryRoot()) {
|
|
191
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
192
|
+
writeFileAtomic(pendingAssignmentsPath(root), `${JSON.stringify(assignments, null, 2)}\n`, { mode: 0o600 });
|
|
193
|
+
}
|
|
194
|
+
/** The boot sweep: migrate every bot's copies, then apply assignments.
|
|
195
|
+
* With a `patch` callback (the live server) assignments land on the bot
|
|
196
|
+
* records directly and any script-recorded pending file is cleared; without
|
|
197
|
+
* one this only reports. Runs under features.skillsLibrary only. */
|
|
198
|
+
export function runSkillsLibraryBootSweep(options) {
|
|
199
|
+
const root = options.root ?? skillsLibraryRoot();
|
|
200
|
+
const outcomes = [];
|
|
201
|
+
const assignments = {};
|
|
202
|
+
const pending = options.patch ? readPendingAssignments(root) : {};
|
|
203
|
+
for (const bot of options.bots) {
|
|
204
|
+
const report = migrateBotSkillsToLibrary(bot.id, root);
|
|
205
|
+
outcomes.push(...report.outcomes);
|
|
206
|
+
const current = bot.assignedSkills ?? [];
|
|
207
|
+
const merged = [...new Set([...(pending[bot.id] ?? []), ...(report.assignments[bot.id] ?? []), ...current])].sort();
|
|
208
|
+
const changed = merged.length !== current.length || merged.some((name, index) => name !== current[index]);
|
|
209
|
+
if (!changed)
|
|
210
|
+
continue;
|
|
211
|
+
if (options.patch)
|
|
212
|
+
options.patch(bot.id, merged);
|
|
213
|
+
assignments[bot.id] = merged;
|
|
214
|
+
}
|
|
215
|
+
// Clear the pending file once every patch applied: entries recorded by
|
|
216
|
+
// the script before this boot and recovery records written by this run's
|
|
217
|
+
// migrations are both spent.
|
|
218
|
+
if (options.patch && (Object.keys(pending).length || Object.keys(readPendingAssignments(root)).length)) {
|
|
219
|
+
writePendingAssignments({}, root);
|
|
220
|
+
}
|
|
221
|
+
return { ranAt: new Date().toISOString(), outcomes, assignments };
|
|
222
|
+
}
|
|
223
|
+
/** Bot ids that have per-bot skill manifests on disk. Used by the
|
|
224
|
+
* standalone script, which cannot reach the Store. */
|
|
225
|
+
export function botIdsWithSkillState(dataDir) {
|
|
226
|
+
try {
|
|
227
|
+
return readdirSync(join(dataDir, "skill-state"), { withFileTypes: true })
|
|
228
|
+
.filter((entry) => entry.isDirectory())
|
|
229
|
+
.map((entry) => entry.name)
|
|
230
|
+
.sort();
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -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",
|
|
@@ -1460,7 +1463,7 @@ export class Store {
|
|
|
1460
1463
|
const full = { id: newId(), at: Date.now(), ...redactBotAuthored(message), parentId: anchorId };
|
|
1461
1464
|
const children = t.messages.filter((m) => m.parentId === anchorId);
|
|
1462
1465
|
t.messages.push(full);
|
|
1463
|
-
mdb.
|
|
1466
|
+
mdb.insertMessage(threadId, full); // the leaf stays where it was, in SQLite too
|
|
1464
1467
|
if (full.kind === "screen") {
|
|
1465
1468
|
for (const pruned of this.pruneScreenFrames(t)) {
|
|
1466
1469
|
mdb.updateMessage(threadId, pruned);
|
|
@@ -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
|
}
|
|
@@ -2348,6 +2376,14 @@ export class Store {
|
|
|
2348
2376
|
taskByThread(botId, threadId) {
|
|
2349
2377
|
return this.bot(botId)?.tasks?.find((t) => t.threadId === threadId);
|
|
2350
2378
|
}
|
|
2379
|
+
/** The thread `fromBotId` opened on this bot from its conversation
|
|
2380
|
+
* `fromThreadId` with coordinate_bots: one per conversation and teammate,
|
|
2381
|
+
* so whatever that conversation sends later continues it. A thread the
|
|
2382
|
+
* person archived is left alone. */
|
|
2383
|
+
workThread(botId, fromBotId, fromThreadId) {
|
|
2384
|
+
return this.tasks(botId).find((t) => t.openedBy?.kind === "work" && t.openedBy.botId === fromBotId &&
|
|
2385
|
+
t.openedBy.threadId === fromThreadId && t.archivedAt === undefined);
|
|
2386
|
+
}
|
|
2351
2387
|
/** A turn gets an independent snapshot without changing the selected task
|
|
2352
2388
|
* or mutating the bot's defaults while another turn is running. */
|
|
2353
2389
|
projectBotForTask(botId, threadId) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// next, and the "needs your hands" plea never said which. Everything that
|
|
6
6
|
// decides or describes a surface lives here, so the picker, the dispatch,
|
|
7
7
|
// the system prompt and the notification cannot drift apart.
|
|
8
|
+
import { usesCloudComputer } from "./contracts.js";
|
|
8
9
|
const SURFACES = new Set(["cloud", "vm", "local", "browser"]);
|
|
9
10
|
/** Parse a surface arriving over the wire; anything else is "not said". */
|
|
10
11
|
export function parseSurface(value) {
|
|
@@ -67,6 +68,70 @@ export function resolveSurface(input) {
|
|
|
67
68
|
}
|
|
68
69
|
return { computer: undefined, browser: browserOn, pinned: null, clearPin: false, note: "" };
|
|
69
70
|
}
|
|
71
|
+
/** The next action per source: the full sentence after a failed attach, and
|
|
72
|
+
* the clause after "Choose another model, or" when the engine is the cause. */
|
|
73
|
+
const PLACE_ACTION = {
|
|
74
|
+
pin: {
|
|
75
|
+
sentence: "Clear this conversation's place in the composer to continue.",
|
|
76
|
+
clause: "clear this conversation's place in the composer",
|
|
77
|
+
},
|
|
78
|
+
// The dispatch clears an Auto-recorded pin that failed, so the next
|
|
79
|
+
// message runs on Auto: that pin was the machine's memory, not a choice.
|
|
80
|
+
"auto-pin": {
|
|
81
|
+
sentence: "This conversation is back on Auto; send your message again.",
|
|
82
|
+
clause: "send your message again; this conversation is back on Auto",
|
|
83
|
+
},
|
|
84
|
+
"works-on": {
|
|
85
|
+
sentence: "Set Works on to Auto in this bot's settings to continue.",
|
|
86
|
+
clause: "set Works on to Auto",
|
|
87
|
+
},
|
|
88
|
+
routine: {
|
|
89
|
+
sentence: "Change where this routine runs.",
|
|
90
|
+
clause: "change where this routine runs",
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
/** The one rule for which engines can work on a cloud computer (Hosted
|
|
94
|
+
* desktop on Boat, or a self-hosted VPS): an engine with computer tools gets
|
|
95
|
+
* it as one more stdio computer server, and the Computer engine runs on its
|
|
96
|
+
* Boat. Checked before anything is provisioned, so a turn that cannot run
|
|
97
|
+
* never creates or wakes a machine. */
|
|
98
|
+
export function cloudPlaceDriverError(capabilities, backend, source = "works-on") {
|
|
99
|
+
const next = `Choose another model, or ${PLACE_ACTION[source].clause}.`;
|
|
100
|
+
if (!usesCloudComputer(capabilities))
|
|
101
|
+
return `This model can't use a computer. ${next}`;
|
|
102
|
+
if (backend === "vps" && capabilities.remoteAgent === true)
|
|
103
|
+
return `The Computer engine runs on Boat and can't use a self-hosted VPS. ${next}`;
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
export const CLOUD_PLACE_DRIVER_ERROR = cloudPlaceDriverError({}, "box");
|
|
107
|
+
/** One line, cause then next action. A bot thread's transcript row keeps
|
|
108
|
+
* 160 characters, so the cause is shortened there, never the action; the
|
|
109
|
+
* cause keeps its own words otherwise. */
|
|
110
|
+
export function placeFailureMessage(cause, source, limit = 160) {
|
|
111
|
+
const action = PLACE_ACTION[source].sentence;
|
|
112
|
+
const body = cause.trim().replace(/[\s.]+$/, "");
|
|
113
|
+
const room = limit - action.length - 2;
|
|
114
|
+
if (body.length <= room)
|
|
115
|
+
return `${body}. ${action}`;
|
|
116
|
+
return `${body.slice(0, Math.max(0, room - 1)).trimEnd()}… ${action}`;
|
|
117
|
+
}
|
|
118
|
+
/** A place the turn was told to use could not be used. Carries the place and
|
|
119
|
+
* where the choice came from, so the dispatch shows the one next action and
|
|
120
|
+
* clears a failed Auto-recorded pin: a failed place never sticks. */
|
|
121
|
+
export class PlaceUnavailableError extends Error {
|
|
122
|
+
place;
|
|
123
|
+
source;
|
|
124
|
+
constructor(place, source, message) {
|
|
125
|
+
super(message);
|
|
126
|
+
this.name = "PlaceUnavailableError";
|
|
127
|
+
this.place = place;
|
|
128
|
+
this.source = source;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** A failed attach: the attach's own words as the cause, plus the action. */
|
|
132
|
+
export function placeUnavailable(place, source, cause, limit = 160) {
|
|
133
|
+
return new PlaceUnavailableError(place, source, placeFailureMessage(cause, source, limit));
|
|
134
|
+
}
|
|
70
135
|
/** How the prompt and the app name a surface. Deliberately the same words
|
|
71
136
|
* the panel uses, so "tell them it is on the cloud computer" points at a
|
|
72
137
|
* label the person can find. */
|
|
@@ -85,7 +150,13 @@ export function surfaceLabel(surface) {
|
|
|
85
150
|
/** Said once per task, so the person hears the place before the first click
|
|
86
151
|
* lands there. Same words as the panel and the composer chip. */
|
|
87
152
|
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
|
-
|
|
153
|
+
/** Where a person moves a conversation: the Computer panel is the one place
|
|
154
|
+
* control both interface modes show (Simple has no composer chip). */
|
|
155
|
+
const PLACE_CONTROL = "the Computer panel";
|
|
156
|
+
/** Without select_computer the person has to switch; with it, surfacePrompt
|
|
157
|
+
* swaps exactly this phrase for the model's own switch. */
|
|
158
|
+
const ASK_TO_SWITCH = `explain the mismatch and ask the user to change where this conversation works in ${PLACE_CONTROL}`;
|
|
159
|
+
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
160
|
/** A Cloud home offers neither this computer nor a Local VM
|
|
90
161
|
* (server/cloud-home.ts), so its bots are told only about the places it has.
|
|
91
162
|
* Each pair rewrites one phrase of the desktop wording above. */
|
|
@@ -116,13 +187,13 @@ export function surfacePrompt(mounted, opts = {}) {
|
|
|
116
187
|
}
|
|
117
188
|
if (text)
|
|
118
189
|
text += RESTATE_SENTENCE + (opts.canSelect
|
|
119
|
-
? SURFACE_AUTHORITY.replace(
|
|
190
|
+
? 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
191
|
: SURFACE_AUTHORITY);
|
|
121
192
|
else if (!opts.note && !opts.canSelect) {
|
|
122
193
|
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
194
|
}
|
|
124
195
|
if (opts.pinned) {
|
|
125
|
-
text += ` This conversation is pinned to ${surfaceLabel(opts.pinned)}; changing places requires ${opts.canSelect ? "select_computer or " : ""}the
|
|
196
|
+
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
197
|
}
|
|
127
198
|
if (opts.canSelect)
|
|
128
199
|
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.";
|
|
@@ -137,23 +208,3 @@ export function surfacePrompt(mounted, opts = {}) {
|
|
|
137
208
|
text = text.replace(desktop, cloudHome);
|
|
138
209
|
return text + (opts.note ?? "");
|
|
139
210
|
}
|
|
140
|
-
/** Tool names that touch a screen, for engines that report bare names.
|
|
141
|
-
* Mirrors the screen-poller regex in the harness. */
|
|
142
|
-
const SCREEN_TOOL = /^(?:screenshot|click|type_text|press_key|scroll|open_url|wait_for|computer_|browser_)/i;
|
|
143
|
-
/** Which surface a completed tool call landed on, or null when it cannot
|
|
144
|
-
* be told apart. The Claude driver namespaces MCP tools by server, which
|
|
145
|
-
* is the only fully reliable signal; a bare name is trusted only when one
|
|
146
|
-
* surface was mounted, because both servers expose `browser_snapshot`. */
|
|
147
|
-
export function surfaceForTool(toolName, mounted) {
|
|
148
|
-
if (toolName.startsWith("mcp__browser__"))
|
|
149
|
-
return mounted.browser ? "browser" : null;
|
|
150
|
-
if (toolName.startsWith("mcp__computer__"))
|
|
151
|
-
return mounted.computer;
|
|
152
|
-
if (!SCREEN_TOOL.test(toolName))
|
|
153
|
-
return null;
|
|
154
|
-
if (mounted.computer && !mounted.browser)
|
|
155
|
-
return mounted.computer;
|
|
156
|
-
if (!mounted.computer && mounted.browser && /^browser_/i.test(toolName))
|
|
157
|
-
return "browser";
|
|
158
|
-
return null;
|
|
159
|
-
}
|