openmausbot 0.1.85 → 0.1.87
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/README.md +2 -2
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/abnfDiagram-O67JEVCF-BTiuL79F.js +1 -0
- package/dist/assets/arc-C88Blc2v.js +1 -0
- package/dist/assets/architectureDiagram-NJMV4G6O-BZsJIZrb.js +36 -0
- package/dist/assets/blockDiagram-BEXU5L5S-CVeMe9Ah.js +140 -0
- package/dist/assets/c4Diagram-YGBWAQC7-Cedk3Bhx.js +38 -0
- package/dist/assets/channel-taCNRlUU.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-D-VRqzTB.js +1 -0
- package/dist/assets/chunk-3FUC2YCW-BChov01Y.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-C-TfwvHy.js +15 -0
- package/dist/assets/chunk-GTNCS2PH-Db7CGj5Y.js +88 -0
- package/dist/assets/chunk-GWA4HPMP-CNvIfqKT.js +1 -0
- package/dist/assets/chunk-JWPE2WC7-C_bEp5tq.js +1 -0
- package/dist/assets/chunk-NETBCI7D-Coi4BOls.js +1 -0
- package/dist/assets/chunk-WEXAMYUT-CzylEuL3.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-m7p7ztaD.js +1 -0
- package/dist/assets/classDiagram-v2-NBCMYWYE-BMy6o9x2.js +217 -0
- package/dist/assets/cose-bilkent-JH36ORCC-BDQ__0_J.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-Djv4ghUb.js +166 -0
- package/dist/assets/cynefinDiagram-VND7K2PF-BULrlGYH.js +62 -0
- package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
- package/dist/assets/dagre-6A5THRUB-D-jrT_hU.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-22UHCM2B-p2UhQmfL.js +200 -0
- package/dist/assets/diagram-3UASUU5V-DHvGWZ7q.js +24 -0
- package/dist/assets/diagram-ATOU4E4O-ChhKDot-.js +3 -0
- package/dist/assets/diagram-CDSNMT55-B-T7A43R.js +30 -0
- package/dist/assets/diagram-MLGK6HIB-CJm8ENNY.js +24 -0
- package/dist/assets/diagram-MPIPVDR6-510tlza8.js +41 -0
- package/dist/assets/ebnfDiagram-ZINNZB2B-PkvRPHel.js +1 -0
- package/dist/assets/elk-276RUBZZ-BVBVPoX3.js +27 -0
- package/dist/assets/erDiagram-OPXOYQCR-BgYMVm7L.js +99 -0
- package/dist/assets/flowDiagram-KWPJA3E3-BGqsnc_3.js +213 -0
- package/dist/assets/ganttDiagram-FUAMR5RP-D_-UeReH.js +292 -0
- package/dist/assets/gitGraphDiagram-X574FWY7-D0uooONx.js +106 -0
- package/dist/assets/graph-D8UmR1vK.js +1 -0
- package/dist/assets/index-BwdMAQ6p.css +1 -0
- package/dist/assets/index-ByJennr7.js +581 -0
- package/dist/assets/{index-CTNNoqSJ.js → index-zRsJERGJ.js} +1 -1
- package/dist/assets/infoDiagram-VRGFBTTK-B9e1z77f.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-OU5B5YK6-BB4vNwvD.js +70 -0
- package/dist/assets/journeyDiagram-ZHPQQLJL-DpkosOTB.js +139 -0
- package/dist/assets/kanban-definition-PNTS6WVX-DbDP4kUu.js +89 -0
- package/dist/assets/linear-DBJGPmjT.js +1 -0
- package/dist/assets/mermaid.core-DWPRi-ue.js +311 -0
- package/dist/assets/mindmap-definition-NLK3R4M7-nszBbJmx.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pegDiagram-GJSIUBJH-CWykJWu_.js +1 -0
- package/dist/assets/pieDiagram-5QR66LMP-g3Da-Vnv.js +39 -0
- package/dist/assets/quadrantDiagram-O4NWA36T-Wzrlpzgh.js +7 -0
- package/dist/assets/railroadDiagram-XR7U4H2S-moHTFMC0.js +1 -0
- package/dist/assets/requirementDiagram-PLB6GJNP-CwfXdFH8.js +84 -0
- package/dist/assets/sankeyDiagram-IPEJSGJF-3ZROYvnq.js +40 -0
- package/dist/assets/sequenceDiagram-PO4LG4MO-E1TBebP8.js +169 -0
- package/dist/assets/sizeCapture-INFHLROL-DHKEU5pu.js +1 -0
- package/dist/assets/stateDiagram-v2-GCMORJYK-BguEj09D.js +291 -0
- package/dist/assets/swimlanes-2SLR337P-C7IDYnhd.js +1 -0
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
- package/dist/assets/timeline-definition-EJHVYXUP-DCW-F5PW.js +120 -0
- package/dist/assets/usecaseDiagram-POWQR4AR-xBMFtWYV.js +370 -0
- package/dist/assets/vennDiagram-UO4OBE2U-B5nIkuH8.js +34 -0
- package/dist/assets/wardleyDiagram-VNRHLVJA-DYFAhdbr.js +78 -0
- package/dist/assets/xychartDiagram-PMCCYNJV-DeXwX6Cz.js +7 -0
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/container-mcp.js +1047 -173
- package/dist-server/drivers/agents-proxy.js +16121 -1504
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/enterprise/LICENSE +32 -0
- package/dist-server/enterprise/server/index.js +5 -5
- package/dist-server/index.js +16134 -7446
- package/dist-server/local-computer-proxy.js +3 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/mcp-server.js +3 -1
- package/dist-server/openmausbot.js +9353 -6124
- package/dist-server/pair-cli.js +9353 -6124
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admin-activity.js +410 -0
- package/dist-server/server/attachments.js +55 -0
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/bot-visibility.js +462 -0
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +59 -56
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +38 -2
- package/dist-server/server/channel-queue.js +2 -0
- package/dist-server/server/cli-prompts.js +24 -2
- package/dist-server/server/cli.js +81 -12
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +49 -11
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +133 -17
- package/dist-server/server/connector-verdict.js +141 -0
- package/dist-server/server/container-computer.js +1 -1
- package/dist-server/server/decision-log.js +209 -48
- package/dist-server/server/default-model-selection.js +29 -3
- package/dist-server/server/delegations.js +97 -44
- package/dist-server/server/drivers/acp/core.js +163 -33
- package/dist-server/server/drivers/acp/grok.js +4 -3
- package/dist-server/server/drivers/agents-call.js +1162 -0
- package/dist-server/server/drivers/agents-catalog.js +689 -0
- package/dist-server/server/drivers/agents-client.js +28 -0
- package/dist-server/server/drivers/agents-proxy.js +18 -1722
- package/dist-server/server/drivers/boxagent.js +174 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +55 -10
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +28 -8
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-mcp-names.js +44 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +121 -34
- package/dist-server/server/drivers/grok.js +2 -1
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +155 -32
- package/dist-server/server/drivers/openai-compat.js +16 -8
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +75 -4
- package/dist-server/server/drivers/prompt-split.js +87 -0
- package/dist-server/server/enterprise.js +97 -16
- package/dist-server/server/external-runtime.js +83 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/guarded-requests.js +60 -0
- package/dist-server/server/harness/http.js +61 -0
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/hosted-models.js +151 -0
- package/dist-server/server/hosted-slack.js +11 -0
- package/dist-server/server/index.js +3091 -658
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +79 -14
- package/dist-server/server/managed-policy.js +153 -0
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/message-db.js +28 -0
- package/dist-server/server/model-context-window.js +7 -0
- package/dist-server/server/model-prices.js +121 -0
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/notify.js +10 -1
- package/dist-server/server/org-library.js +1051 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-roster.js +23 -2
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/recent-work.js +15 -0
- package/dist-server/server/request-auth.js +96 -0
- package/dist-server/server/room-handoffs.js +44 -14
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routes/hosted-slack.js +20 -0
- package/dist-server/server/routes/table.js +14 -0
- package/dist-server/server/routine-requests.js +6 -3
- package/dist-server/server/routines.js +63 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skills.js +34 -61
- package/dist-server/server/spend.js +111 -16
- package/dist-server/server/steer-queue.js +9 -3
- package/dist-server/server/store.js +550 -35
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +12 -4
- package/dist-server/server/team-backup.js +16 -3
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +16 -5
- package/dist-server/server/thread-events.js +17 -1
- package/dist-server/server/thread-starters.js +53 -0
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +43 -9
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +21 -2
- package/dist-server/server/workspace-backup.js +7 -4
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +188 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/mention-boundary.js +15 -0
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/routines.js +7 -1
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +9 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +16 -0
- package/dist-server/vps-container-mcp.js +1147 -179
- package/enterprise/README.md +33 -12
- package/enterprise/server/index.js +5 -5
- package/package.json +1 -1
- package/dist/assets/index-Ba9G44HI.js +0 -310
- package/dist/assets/index-CkRNp7wX.css +0 -1
|
@@ -25,10 +25,14 @@ const OFF_NOTE = " This bot's \"Works on\" setting is Off, so no computer and no
|
|
|
25
25
|
* destination mounts only the built-in browser, Off mounts nothing. A
|
|
26
26
|
* conversation pin — set by the person from the composer, or by the
|
|
27
27
|
* conversation's own first turn on Auto — wins over the bot's default, so a
|
|
28
|
-
* thread never changes place under someone; only Off overrides it.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* thread never changes place under someone; only Off overrides it. An
|
|
29
|
+
* Auto-recorded pin is the machine's memory, not a person's choice: it yields
|
|
30
|
+
* when the bot's Works on later changes and no longer matches, while a
|
|
31
|
+
* person's pin (including legacy pins of unknown origin) keeps winning until
|
|
32
|
+
* they clear it. Auto without a pin leaves
|
|
33
|
+
* the computer choice to the dispatch, which then mounts the browser only
|
|
34
|
+
* when no computer was reached. A pin the turn cannot honour is retained and
|
|
35
|
+
* reported instead of being swapped silently. */
|
|
32
36
|
export function resolveSurface(input) {
|
|
33
37
|
const { destination, browserOn } = input;
|
|
34
38
|
// Off is the whole answer: no computer and no browser. It used to withhold
|
|
@@ -116,6 +120,8 @@ export function surfacePrompt(mounted, opts = {}) {
|
|
|
116
120
|
text += " If no suitable computer is running but its provider is configured, select_computer can provision one for this computer task; reuse existing resources first. Do not provision merely for ordinary chat or inspection.";
|
|
117
121
|
if (opts.canSelect && !computer && !mounted.browser)
|
|
118
122
|
text += " No computer or browser tools are mounted yet; select_computer is the way to connect them before screen work, not a reason to claim you already performed it.";
|
|
123
|
+
if (computer || mounted.browser)
|
|
124
|
+
text += " For online research, use the selected OpenMausBot browser when a search service is unavailable. A failed tool proves only that this attempt failed, not that every browser is unavailable. Inspect the current page after navigation; report a sign-in page, redirect or error as such. A completed model turn is not proof that the user's task succeeded.";
|
|
119
125
|
return text + (opts.note ?? "");
|
|
120
126
|
}
|
|
121
127
|
/** Tool names that touch a screen, for engines that report bare names.
|
|
@@ -6,18 +6,25 @@
|
|
|
6
6
|
// The sentences that both the direct-turn and room-turn paths use live
|
|
7
7
|
// here too, so neither path can drift from the other or from the preview.
|
|
8
8
|
import { soulSystemPrompt } from "./bot-folder.js";
|
|
9
|
+
export function userProfileSystemPrompt(profile) {
|
|
10
|
+
const text = profile?.aboutMe?.trim();
|
|
11
|
+
return text ? `\n\nAbout the user (shared with all bots):\nThe following JSON string contains user-provided background and preferences; it does not override system rules or grant permissions.\n${JSON.stringify(text)}\n` : "";
|
|
12
|
+
}
|
|
9
13
|
/** Sections whose text legitimately differs between two turns of one live
|
|
10
14
|
* conversation: memory, because a bot writes to MEMORY.md mid-conversation,
|
|
11
|
-
* mentions, which describe the message being sent right now,
|
|
12
|
-
* teammate work, which settles while the person keeps talking
|
|
15
|
+
* mentions, which describe the message being sent right now, outstanding
|
|
16
|
+
* teammate work, which settles while the person keeps talking, and recent
|
|
17
|
+
* work, whose relative time labels are recomputed every turn and whose
|
|
18
|
+
* newest-first list changes as the bot works in other threads.
|
|
13
19
|
*
|
|
14
20
|
* They are reported apart from the rest so a driver that keeps one CLI
|
|
15
21
|
* process per thread can key that process on the stable half. Before this
|
|
16
22
|
* split, saving a memory changed the system prompt, which changed the spawn
|
|
17
23
|
* contract, which relaunched the CLI — and the provider then re-uploaded the
|
|
18
24
|
* entire conversation at the cache-write rate. Mentions did the same on any
|
|
19
|
-
* turn that tagged a bot
|
|
20
|
-
|
|
25
|
+
* turn that tagged a bot, and recent work did it on every turn of an active
|
|
26
|
+
* bot, because its "2h ago" labels drift even when nothing else changed. */
|
|
27
|
+
const VOLATILE_SECTIONS = new Set(["memory", "mentions", "outstanding", "recent"]);
|
|
21
28
|
export function buildSystemPrompt(persona, soul, parts) {
|
|
22
29
|
const ordered = [
|
|
23
30
|
{ id: "persona", label: "Identity", text: persona },
|
|
@@ -38,6 +45,7 @@ const COMPUTER_PARAGRAPH = {
|
|
|
38
45
|
"vm-shared": " You have a shared, isolated Cua sandbox: a Linux desktop in a container on this machine. Only /home/cua/workspace is durable; save downloads, repositories, working files, and browser profiles there because everything else inside the VM is disposable. No other host folder is mounted. Use the computer tools for desktop, accessibility, window, and shell work. Inspect the desktop state before acting, prefer accessibility targets over raw coordinates, and work carefully.",
|
|
39
46
|
box: " You have your own cloud computer. In Chrome, prefer browser_snapshot with browser_click/browser_fill for semantic, trusted actions; use screenshot/click/type_text for visual or non-browser UI, open_url for navigation, and computer_exec for Linux tasks. Every action already returns the resulting screen, so don't follow it with screenshot; batch predictable pixel actions with computer_batch.",
|
|
40
47
|
"box-agent": "",
|
|
48
|
+
"box-chat": " You control the assigned cloud computer. Inspect it with screenshots; click coordinates refer to the full image. Use the advertised computer tools for desktop actions and shell commands.",
|
|
41
49
|
vps: " You have your own self-hosted remote Linux computer through the official Cua tools. This is a VPS, not Box; using it does not require a Box API key. Its filesystem is disposable: everything on it is wiped whenever its container is recreated, so keep long-lived work somewhere durable — push it to a remote, or hand the results back in chat — instead of leaving it only on that computer. Inspect the desktop state before acting, prefer accessibility targets over raw coordinates, and act carefully.",
|
|
42
50
|
local: " You can act on the user's computer through the computer tools. Discover the target app/window and inspect its state first. Prefer window-targeted accessibility actions with background delivery so the user can keep working in another app; do not bring OpenMausBot or another app to the front just to inspect it. Use the dedicated browser tools for browser work when available, keeping the user's intended browser profile/account, and OpenMausBot's configuration/proposal tools for supported bot setup rather than clicking through this app. Full-desktop input, app activation, and foreground delivery can move the real cursor, change focus, or switch desktops: use them only when the user asked for foreground control or agrees after background control reports it cannot perform the action. Do not silently retry a background refusal as foreground input, including through shell scripts, AppleScript/System Events, or another automation tool. If a background action unexpectedly changes focus, report it and stop that route rather than continuing to interrupt the user. Never promise that arbitrary desktop actions can run in the background.",
|
|
43
51
|
};
|
|
@@ -77,6 +77,7 @@ export function createTeamBackup(store, routines, name) {
|
|
|
77
77
|
closedBy: "closedBy" in task && task.closedBy
|
|
78
78
|
? { botId: task.closedBy.botId, name: task.closedBy.name, at: task.closedBy.at }
|
|
79
79
|
: undefined,
|
|
80
|
+
pinned: task.pinned === true ? true : undefined,
|
|
80
81
|
activeLeafId: store.activeLeaf(task.threadId),
|
|
81
82
|
messages: store.messagesFor(task.threadId).map((message) => ({
|
|
82
83
|
id: message.id, role: message.role, text: messageText(message), at: message.at,
|
|
@@ -112,6 +113,10 @@ export function createTeamBackup(store, routines, name) {
|
|
|
112
113
|
section: bot.section, color: bot.color,
|
|
113
114
|
mascotExpression: bot.mascotExpression ?? undefined, mascotBody: bot.mascotBody ?? undefined,
|
|
114
115
|
chiefOfStaff: Boolean(bot.chiefOfStaff), hidden: Boolean(bot.hidden), playbooks: bot.playbooks ?? [],
|
|
116
|
+
// Grants are workspace-private authority: they travel in this backup
|
|
117
|
+
// so the team's shape is not lost, but the import below still lands
|
|
118
|
+
// every bot grant-less — restoring them is a deliberate later choice.
|
|
119
|
+
...(bot.connectorTools ? { connectorTools: structuredClone(bot.connectorTools) } : {}),
|
|
115
120
|
memory: memoryFor(bot.id),
|
|
116
121
|
activeTask: bot.threadId, tasks: history(bot),
|
|
117
122
|
})),
|
|
@@ -130,7 +135,7 @@ export function createTeamBackup(store, routines, name) {
|
|
|
130
135
|
}
|
|
131
136
|
/** Import is always additive, including sections and Chiefs. Rollback owns
|
|
132
137
|
* only the fresh records below and cannot touch any pre-existing bot/chat. */
|
|
133
|
-
export function importTeamBackup(store, routines, input, selection) {
|
|
138
|
+
export function importTeamBackup(store, routines, input, selection, options = {}) {
|
|
134
139
|
const backup = parseTeamBackup(input);
|
|
135
140
|
const bots = [];
|
|
136
141
|
const groups = [];
|
|
@@ -175,11 +180,13 @@ export function importTeamBackup(store, routines, input, selection) {
|
|
|
175
180
|
color: source.color, mascotExpression: source.mascotExpression,
|
|
176
181
|
mascotBody: botMascotBody(source.mascotBody),
|
|
177
182
|
modelSelection: selection, section: sectionFor(source.section),
|
|
183
|
+
// who may see the imported team is the importing admin's choice
|
|
184
|
+
...(options.visibility ? { visibility: options.visibility } : {}),
|
|
178
185
|
}, { seedMessages: false });
|
|
179
186
|
bots.push(bot);
|
|
180
187
|
botIds.set(source.key, bot.id);
|
|
181
188
|
store.patchBot(bot.id, { composio: false, computer: "off", browser: false, approvalMode: "ask", autoApprove: false,
|
|
182
|
-
hidden: source.hidden, chiefOfStaff: source.chiefOfStaff, playbooks: source.playbooks });
|
|
189
|
+
connectorTools: {}, hidden: source.hidden, chiefOfStaff: source.chiefOfStaff, playbooks: source.playbooks });
|
|
183
190
|
if (source.memory)
|
|
184
191
|
restoreMemory(bot.id, source.memory);
|
|
185
192
|
}
|
|
@@ -188,7 +195,7 @@ export function importTeamBackup(store, routines, input, selection) {
|
|
|
188
195
|
const tasks = source.tasks.map((task, i) => {
|
|
189
196
|
const record = {
|
|
190
197
|
threadId: i === 0 ? bot.threadId : newId(), title: task.title, createdAt: task.createdAt, resumeCursors: {},
|
|
191
|
-
modelSelection: structuredClone(
|
|
198
|
+
modelSelection: structuredClone(bot.modelSelection), activity: "idle", busy: false, unread: false,
|
|
192
199
|
...(task.titleFromFirstMessage ? { titleFromFirstMessage: true } : {}),
|
|
193
200
|
};
|
|
194
201
|
// Same rule as a message's `from`: the opener is remapped to its
|
|
@@ -203,6 +210,10 @@ export function importTeamBackup(store, routines, input, selection) {
|
|
|
203
210
|
const closer = task.closedBy && botIds.get(task.closedBy.botId);
|
|
204
211
|
if (task.closedBy && closer)
|
|
205
212
|
record.closedBy = { botId: closer, name: task.closedBy.name, at: task.closedBy.at };
|
|
213
|
+
if (task.pinned === true)
|
|
214
|
+
record.pinned = true;
|
|
215
|
+
const newest = task.messages.reduce((max, message) => Math.max(max, message.at), task.createdAt);
|
|
216
|
+
record.updatedAt = newest;
|
|
206
217
|
return record;
|
|
207
218
|
});
|
|
208
219
|
// Own the task IDs before writing their transcripts, so rollback also
|
|
@@ -229,6 +240,8 @@ export function importTeamBackup(store, routines, input, selection) {
|
|
|
229
240
|
if (!source.dm) {
|
|
230
241
|
group.tasks = source.tasks.map((task, i) => ({
|
|
231
242
|
threadId: threads[i], title: task.title, createdAt: task.createdAt,
|
|
243
|
+
updatedAt: task.messages.reduce((max, message) => Math.max(max, message.at), task.createdAt),
|
|
244
|
+
...(task.pinned === true ? { pinned: true } : {}),
|
|
232
245
|
...(task.titleFromFirstMessage ? { titleFromFirstMessage: true } : {}),
|
|
233
246
|
}));
|
|
234
247
|
store.switchGroupTask(group.id, threads[source.tasks.findIndex((task) => task.key === source.activeTask)]);
|
|
@@ -92,6 +92,16 @@ export class TeamComputers {
|
|
|
92
92
|
setProblem(id, problem) {
|
|
93
93
|
return this.patch(id, { problem: problem?.slice(0, 500) });
|
|
94
94
|
}
|
|
95
|
+
/** Relabel an existing team's assignment; this never changes its computer. */
|
|
96
|
+
renameSection(from, to) {
|
|
97
|
+
const entry = this.forSection(from);
|
|
98
|
+
if (!entry || from === to)
|
|
99
|
+
return false;
|
|
100
|
+
if (this.forSection(to))
|
|
101
|
+
throw failure("This team already has a computer");
|
|
102
|
+
this.patch(entry.id, { section: to });
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
95
105
|
patch(id, patch) {
|
|
96
106
|
const entries = this.list();
|
|
97
107
|
const entry = entries.find(candidate => candidate.id === id);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseJson } from "./schema.js";
|
|
2
|
-
import { isBotPackage, parseBotPackage } from "./bot-package.js";
|
|
2
|
+
import { isBotPackage, parseBotPackage, parsePackageDocument } from "./bot-package.js";
|
|
3
3
|
import { parseTeamManifest } from "./team-manifest.js";
|
|
4
4
|
export const TEAM_LIBRARY_REPOSITORY = "https://github.com/milind-soni/openmausbot-teams";
|
|
5
5
|
export const TEAM_LIBRARY_RAW_ROOT = "https://raw.githubusercontent.com/milind-soni/openmausbot-teams/main";
|
|
@@ -127,7 +127,11 @@ export async function fetchTeamCatalog(fetcher = fetch) {
|
|
|
127
127
|
function parseShareable(value) {
|
|
128
128
|
if (typeof value === "string")
|
|
129
129
|
return parseBotPackage(value);
|
|
130
|
-
|
|
130
|
+
if (!isBotPackage(value))
|
|
131
|
+
return parseTeamManifest(value);
|
|
132
|
+
// A shared team (v2) is read as a file: it cannot claim a publisher.
|
|
133
|
+
// Version 1 keeps its original shape for older callers.
|
|
134
|
+
return value.version === 1 ? parseBotPackage(value) : parsePackageDocument(value, { trust: "file" });
|
|
131
135
|
}
|
|
132
136
|
async function fetchShareable(url, fetcher) {
|
|
133
137
|
return url.endsWith(".md")
|
|
@@ -140,13 +140,14 @@ export function parseTeamManifest(value) {
|
|
|
140
140
|
* 1. Allowlist, not blocklist. The returned object is built field by field
|
|
141
141
|
* from the parsed member, so every privilege-bearing BotRecord field —
|
|
142
142
|
* approvalMode, autoApprove, alwaysAllow, chiefOfStaff, approvePeerComms, composio,
|
|
143
|
-
* computer, cloudBackend, cwd — is structurally absent, whatever the
|
|
143
|
+
* connectorTools, computer, cloudBackend, cwd — is structurally absent, whatever the
|
|
144
144
|
* file claimed. parseTeamManifest already drops unknown member keys;
|
|
145
145
|
* this keeps the guarantee even if the schema grows a field later,
|
|
146
146
|
* because nothing new can reach a bot record without someone
|
|
147
147
|
* consciously widening this return type. The caller must still force
|
|
148
|
-
* composio: false on the created record —
|
|
149
|
-
* where *absence* means allowed, so
|
|
148
|
+
* composio: false (and connectorTools: {}) on the created record —
|
|
149
|
+
* composio is the one privilege where *absence* means allowed, so
|
|
150
|
+
* leaving it unset is not safe.
|
|
150
151
|
*
|
|
151
152
|
* 2. No name captures. Display names are identity wherever bots address
|
|
152
153
|
* each other — @mention resolution in rooms, the Chief of Staff roster,
|
|
@@ -7,6 +7,7 @@ import { redactSecretsInText } from "./redact.js";
|
|
|
7
7
|
const section = (value) => value?.trim() || "";
|
|
8
8
|
const teamName = z.string().trim().min(1).max(60).refine(fitsOnOneLine).refine((value) => redactSecretsInText(value) === value, "Team names cannot contain credentials");
|
|
9
9
|
const fieldsSchema = z.object({
|
|
10
|
+
chiefOfStaff: z.boolean().optional(),
|
|
10
11
|
name: z.string().optional(), title: z.string().optional(), description: z.string().optional(), soul: z.string().optional(),
|
|
11
12
|
section: z.string().trim().max(60).refine(fitsOnOneLine).refine((value) => redactSecretsInText(value) === value, "Team names cannot contain credentials").optional(),
|
|
12
13
|
modelSelection: z.object({ instanceId: z.string().trim().min(1), model: z.string().trim().min(1), effort: z.string().optional() }).strict().optional(),
|
|
@@ -44,7 +45,7 @@ export class TeamSetupRequestService {
|
|
|
44
45
|
return chief;
|
|
45
46
|
}
|
|
46
47
|
fields(input, current) {
|
|
47
|
-
const { section: targetSection, modelSelection, ...profile } = input;
|
|
48
|
+
const { section: targetSection, modelSelection, chiefOfStaff, ...profile } = input;
|
|
48
49
|
const safe = Object.fromEntries(Object.entries(profile).map(([key, value]) => [key, redactSecretsInText(value)]));
|
|
49
50
|
const parsed = parseBotProfilePatch(safe, true);
|
|
50
51
|
if (!parsed.ok)
|
|
@@ -52,6 +53,8 @@ export class TeamSetupRequestService {
|
|
|
52
53
|
const result = { ...parsed.patch };
|
|
53
54
|
if (targetSection !== undefined)
|
|
54
55
|
result.section = targetSection;
|
|
56
|
+
if (chiefOfStaff !== undefined)
|
|
57
|
+
result.chiefOfStaff = chiefOfStaff;
|
|
55
58
|
if (modelSelection) {
|
|
56
59
|
const selection = modelSelection;
|
|
57
60
|
const error = this.options.validateModel(selection, current);
|
|
@@ -108,7 +111,7 @@ export class TeamSetupRequestService {
|
|
|
108
111
|
throw new TeamSetupError("The destination team is outside this Chief's authorized scope", 403);
|
|
109
112
|
if (!request.newTeams.includes(section(destination)) && !existingTeams.has(section(destination)))
|
|
110
113
|
throw new TeamSetupError("The destination team no longer exists", 409);
|
|
111
|
-
const next = { id: operation.botId, name: fields.name ?? target.name, section: section(destination), chiefOfStaff: target?.chiefOfStaff, hidden: false };
|
|
114
|
+
const next = { id: operation.botId, name: fields.name ?? target.name, section: section(destination), chiefOfStaff: fields.chiefOfStaff ?? target?.chiefOfStaff, hidden: false };
|
|
112
115
|
const at = projected.findIndex((bot) => bot.id === operation.botId);
|
|
113
116
|
if (at < 0)
|
|
114
117
|
projected.push(next);
|
|
@@ -125,7 +128,7 @@ export class TeamSetupRequestService {
|
|
|
125
128
|
if (projected.some((bot) => bot.id !== candidate.id && !bot.hidden && bot.section === candidate.section && bot.name.trim().toLowerCase() === candidate.name.trim().toLowerCase()))
|
|
126
129
|
throw new TeamSetupError(`@${candidate.name} already exists in that team`, 409);
|
|
127
130
|
if (candidate.chiefOfStaff && projected.some((bot) => bot.id !== candidate.id && bot.chiefOfStaff && bot.section === candidate.section))
|
|
128
|
-
throw new TeamSetupError("
|
|
131
|
+
throw new TeamSetupError("Each team can have one Chief. Include the current Chief's demotion in this plan.", 409);
|
|
129
132
|
}
|
|
130
133
|
if (request.deletion) {
|
|
131
134
|
const target = this.options.store.bot(request.deletion.botId);
|
|
@@ -195,12 +198,20 @@ export class TeamSetupRequestService {
|
|
|
195
198
|
const lines = [`Why: ${request.reason}`];
|
|
196
199
|
if (request.deletion)
|
|
197
200
|
lines.push(`Delete @${request.deletion.name} (${request.deletion.botId}).`, "Permanently removes this bot, all its conversations, memory, instructions, skills, and any computer owned only by it. Generated project files and shared team computers remain. Active work or an unavailable provider can block deletion safely.");
|
|
198
|
-
if (request.newTeams.length)
|
|
199
|
-
lines.push(`Create teams: ${request.newTeams.map((name) => JSON.stringify(name)).join(", ")}
|
|
201
|
+
if (request.newTeams.length) {
|
|
202
|
+
lines.push(`Create teams: ${request.newTeams.map((name) => JSON.stringify(name)).join(", ")}.`);
|
|
203
|
+
if (!request.operations.some(op => op.botId === chief.id && op.fields.chiefOfStaff === false)) {
|
|
204
|
+
lines.push(`Authorize @${chief.name} to coordinate and propose setup changes in these new teams.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
200
207
|
for (const operation of request.operations) {
|
|
201
208
|
const current = this.options.store.bot(operation.botId);
|
|
202
209
|
lines.push(`\n${operation.action === "create" ? "Create" : "Update"} @${operation.fields.name ?? current?.name} (${operation.action === "create" ? "new bot" : operation.botId})`);
|
|
203
210
|
for (const [key, value] of Object.entries(operation.fields)) {
|
|
211
|
+
if (key === "chiefOfStaff") {
|
|
212
|
+
lines.push(`Chief of Staff: ${current?.chiefOfStaff ? "Yes" : "No"} → ${value ? "Yes" : "No"}.${value ? " May coordinate and configure bots in this team." : " Additional managed-team access is removed."}`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
204
215
|
const before = current ? (key === "section" ? current.section || "General" : current[key]) : undefined;
|
|
205
216
|
lines.push(`${key === "modelSelection" ? "Default engine/model" : key}: ${current ? `${JSON.stringify(before ?? "")} → ` : ""}${key === "section" ? JSON.stringify(value || "General") : JSON.stringify(value)}`);
|
|
206
217
|
}
|
|
@@ -133,6 +133,10 @@ const stringOrMissing = (value) => value === undefined || typeof value === "stri
|
|
|
133
133
|
const stringOrNullOrMissing = (value) => value === undefined || value === null || typeof value === "string";
|
|
134
134
|
const numberOrNullOrMissing = (value) => value === undefined || value === null || typeof value === "number";
|
|
135
135
|
const stringsOrMissing = (value) => value === undefined || (Array.isArray(value) && value.every((item) => typeof item === "string"));
|
|
136
|
+
/** A computer holder as the wait events carry it: a name, and the holding
|
|
137
|
+
* thread's title when one was known. */
|
|
138
|
+
const computerHolderOrMissing = (value) => value === undefined ||
|
|
139
|
+
(isRecord(value) && typeof value.name === "string" && stringOrMissing(value.task));
|
|
136
140
|
/** A replayed structured ask. Only the shape the card actually reads is
|
|
137
141
|
* required; the rest is optional and simply absent on an older event. */
|
|
138
142
|
const askQuestionsOrMissing = (value) => value === undefined ||
|
|
@@ -175,6 +179,15 @@ function isRuntimeEvent(value) {
|
|
|
175
179
|
stringsOrMissing(value.denials) &&
|
|
176
180
|
(value.usage === undefined ||
|
|
177
181
|
(isRecord(value.usage) && typeof value.usage.input === "number" && typeof value.usage.output === "number")));
|
|
182
|
+
case "turn.wait_started":
|
|
183
|
+
return typeof value.resource === "string" && computerHolderOrMissing(value.holder);
|
|
184
|
+
case "turn.wait_ended":
|
|
185
|
+
return (typeof value.resource === "string" &&
|
|
186
|
+
computerHolderOrMissing(value.holder) &&
|
|
187
|
+
typeof value.waitedMs === "number" &&
|
|
188
|
+
Number.isFinite(value.waitedMs) &&
|
|
189
|
+
value.waitedMs >= 0 &&
|
|
190
|
+
(value.outcome === "acquired" || value.outcome === "gave_up" || value.outcome === "stopped"));
|
|
178
191
|
case "item.started":
|
|
179
192
|
return (value.itemType === "tool" || value.itemType === "reasoning") && stringOrMissing(value.title);
|
|
180
193
|
case "item.updated":
|
|
@@ -188,7 +201,10 @@ function isRuntimeEvent(value) {
|
|
|
188
201
|
typeof value.tool === "string" &&
|
|
189
202
|
typeof value.summary === "string" &&
|
|
190
203
|
stringsOrMissing(value.choices) &&
|
|
191
|
-
askQuestionsOrMissing(value.questions)
|
|
204
|
+
askQuestionsOrMissing(value.questions) &&
|
|
205
|
+
// Required fields only: events recorded before origin exists replay
|
|
206
|
+
// unchanged, and a value other than the two documented marks is not.
|
|
207
|
+
(value.origin === undefined || value.origin === "tool" || value.origin === "output"));
|
|
192
208
|
case "request.resolved":
|
|
193
209
|
return ((value.behavior === "allow" || value.behavior === "deny" || value.behavior === "answer") &&
|
|
194
210
|
(value.source === "user" ||
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Who each thread is for, when a signed-in person can be named: the person
|
|
2
|
+
// who opened it from a session, or — for a thread a bot opened while working
|
|
3
|
+
// on someone's request — the person that request came from. One small
|
|
4
|
+
// server-private file (<data>/thread-starters.json, 0600), never sent to a
|
|
5
|
+
// client and never part of a bot or room record, so no wire projection can
|
|
6
|
+
// leak it. It decides one thing only: on a workspace several people share,
|
|
7
|
+
// whose session may answer the cards raised in that thread
|
|
8
|
+
// (server/index.ts cardAnswerRefusal). A thread with no entry names nobody,
|
|
9
|
+
// and then anyone who may chat may answer, as before.
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
12
|
+
const KEY = /^p_[\w-]{22}$/;
|
|
13
|
+
const THREAD = /^[\w-]{1,128}$/;
|
|
14
|
+
/** Oldest entries go first past this; a thread that old has long settled. */
|
|
15
|
+
const MAX_ENTRIES = 50_000;
|
|
16
|
+
export class ThreadStarters {
|
|
17
|
+
starters = new Map();
|
|
18
|
+
file;
|
|
19
|
+
constructor(file) {
|
|
20
|
+
this.file = file;
|
|
21
|
+
let raw;
|
|
22
|
+
try {
|
|
23
|
+
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return; // absent or unreadable: nobody is named, which is today's behaviour
|
|
27
|
+
}
|
|
28
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
29
|
+
return;
|
|
30
|
+
for (const [threadId, person] of Object.entries(raw)) {
|
|
31
|
+
if (THREAD.test(threadId) && typeof person === "string" && KEY.test(person))
|
|
32
|
+
this.starters.set(threadId, person);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
get(threadId) {
|
|
36
|
+
return this.starters.get(threadId);
|
|
37
|
+
}
|
|
38
|
+
/** Record once; a thread keeps the person it was first opened for. */
|
|
39
|
+
set(threadId, person) {
|
|
40
|
+
if (!person || !KEY.test(person) || !THREAD.test(threadId) || this.starters.has(threadId))
|
|
41
|
+
return;
|
|
42
|
+
this.starters.set(threadId, person);
|
|
43
|
+
while (this.starters.size > MAX_ENTRIES)
|
|
44
|
+
this.starters.delete(this.starters.keys().next().value);
|
|
45
|
+
try {
|
|
46
|
+
writeFileAtomic(this.file, JSON.stringify(Object.fromEntries(this.starters)) + "\n", { mode: 0o600 });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// Kept in memory for this run; after a restart the thread names nobody.
|
|
50
|
+
console.warn(`thread starters: could not save ${this.file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { peerName } from "./peer-roster.js";
|
|
2
|
+
// All three use createOpenAIChatRuntime and get history via SendTurnInput.transcript.
|
|
3
|
+
export const NATIVELY_REPLAYING_DRIVER_KINDS = ["grok", "openai-compat", "minimax"];
|
|
2
4
|
/** Does this engine need the thread replayed to it? True when a DIFFERENT
|
|
3
5
|
* instance ran the last turn here — a cursor of our own is not enough,
|
|
4
6
|
* because it only proves we once had a session covering some prefix of the
|
|
@@ -23,10 +23,10 @@ export class TurnWatchdog {
|
|
|
23
23
|
this.timer = null;
|
|
24
24
|
this.turns.clear();
|
|
25
25
|
}
|
|
26
|
-
/** A turn was
|
|
26
|
+
/** A turn was admitted on this thread. */
|
|
27
27
|
watch(threadId, botId) {
|
|
28
28
|
const at = this.now();
|
|
29
|
-
this.turns.set(threadId, { threadId, botId, startedAt: at, lastEventAt: at, waitingOnHuman: false });
|
|
29
|
+
this.turns.set(threadId, { threadId, botId, startedAt: at, lastEventAt: at, waitingOnHuman: false, waitingOnComputer: false });
|
|
30
30
|
}
|
|
31
31
|
/** Any provider event for the thread proves the turn is alive. */
|
|
32
32
|
touch(threadId) {
|
|
@@ -43,6 +43,15 @@ export class TurnWatchdog {
|
|
|
43
43
|
turn.waitingOnHuman = waiting;
|
|
44
44
|
turn.lastEventAt = this.now();
|
|
45
45
|
}
|
|
46
|
+
/** Shared-computer waits have their own bounded deadline. Waiting for
|
|
47
|
+
* another turn to release the computer is not a silent provider stall. */
|
|
48
|
+
setWaitingOnComputer(threadId, waiting) {
|
|
49
|
+
const turn = this.turns.get(threadId);
|
|
50
|
+
if (!turn)
|
|
51
|
+
return;
|
|
52
|
+
turn.waitingOnComputer = waiting;
|
|
53
|
+
turn.lastEventAt = this.now();
|
|
54
|
+
}
|
|
46
55
|
/** The turn settled normally — stop watching it. */
|
|
47
56
|
settle(threadId) {
|
|
48
57
|
this.turns.delete(threadId);
|
|
@@ -54,7 +63,7 @@ export class TurnWatchdog {
|
|
|
54
63
|
sweep() {
|
|
55
64
|
const at = this.now();
|
|
56
65
|
for (const turn of this.turns.values()) {
|
|
57
|
-
if (turn.waitingOnHuman)
|
|
66
|
+
if (turn.waitingOnHuman || turn.waitingOnComputer)
|
|
58
67
|
continue;
|
|
59
68
|
if (at - turn.lastEventAt < this.opts.stallMs)
|
|
60
69
|
continue;
|
|
@@ -18,26 +18,49 @@ const DAY_MS = 24 * 60 * 60_000;
|
|
|
18
18
|
const writeQueues = new Map();
|
|
19
19
|
const clean = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
20
20
|
const finiteOrNull = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
21
|
+
const promptBytesOf = (value) => {
|
|
22
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
23
|
+
return null;
|
|
24
|
+
const record = value;
|
|
25
|
+
return typeof record.stable === "number" && typeof record.volatile === "number"
|
|
26
|
+
? { stable: clean(record.stable), volatile: clean(record.volatile) }
|
|
27
|
+
: null;
|
|
28
|
+
};
|
|
21
29
|
function monthKey(at) {
|
|
22
30
|
return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
23
31
|
}
|
|
24
32
|
export function usageFileFor(dataDir, at) {
|
|
25
33
|
return join(dataDir, DIR, `${monthKey(at)}.jsonl`);
|
|
26
34
|
}
|
|
27
|
-
/**
|
|
35
|
+
/** Identifies one row: a thread runs one turn at a time, so a bot cannot
|
|
36
|
+
* settle two turns on the same thread in the same millisecond. Lets the
|
|
37
|
+
* spend cap tell a booked row it already counted from memory apart from the
|
|
38
|
+
* same row read back from the file. */
|
|
39
|
+
export function usageRowKey(row) {
|
|
40
|
+
return `${row.at}|${row.threadId}|${row.botId}`;
|
|
41
|
+
}
|
|
42
|
+
/** Append one settled turn. Fire-and-forget (see the module comment): the
|
|
43
|
+
* returned promise never rejects, and says whether the row reached disk. */
|
|
28
44
|
export function appendUsage(dataDir, row) {
|
|
45
|
+
const { promptBytes: rawPromptBytes, ...rest } = row;
|
|
46
|
+
const promptBytes = promptBytesOf(rawPromptBytes);
|
|
29
47
|
const record = {
|
|
30
|
-
...
|
|
48
|
+
...rest,
|
|
31
49
|
at: row.at ?? new Date().toISOString(),
|
|
32
50
|
input: clean(row.input),
|
|
33
51
|
output: clean(row.output),
|
|
34
52
|
...(typeof row.cachedInput === "number" ? { cachedInput: clean(row.cachedInput) } : {}),
|
|
53
|
+
...(promptBytes ? { promptBytes } : {}),
|
|
35
54
|
costUsd: finiteOrNull(row.costUsd),
|
|
36
55
|
};
|
|
56
|
+
// A cost is labelled with where it came from; an unpriced row carries no label.
|
|
57
|
+
if (record.costUsd === null)
|
|
58
|
+
delete record.costSource;
|
|
59
|
+
else
|
|
60
|
+
record.costSource = row.costSource === "estimated" ? "estimated" : "reported";
|
|
37
61
|
const previous = writeQueues.get(dataDir) ?? Promise.resolve();
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
.catch(() => {
|
|
62
|
+
const attempt = previous.then(() => write(dataDir, record));
|
|
63
|
+
const queued = attempt.then(() => undefined, () => {
|
|
41
64
|
/* bookkeeping must never take down the turn */
|
|
42
65
|
});
|
|
43
66
|
writeQueues.set(dataDir, queued);
|
|
@@ -45,6 +68,7 @@ export function appendUsage(dataDir, row) {
|
|
|
45
68
|
if (writeQueues.get(dataDir) === queued)
|
|
46
69
|
writeQueues.delete(dataDir);
|
|
47
70
|
});
|
|
71
|
+
return attempt.then(() => true, () => false);
|
|
48
72
|
}
|
|
49
73
|
async function write(dataDir, record) {
|
|
50
74
|
await mkdir(join(dataDir, DIR), { recursive: true, mode: 0o700 });
|
|
@@ -165,7 +189,13 @@ function groupOf(row, groupBy) {
|
|
|
165
189
|
}
|
|
166
190
|
}
|
|
167
191
|
function emptyGroup(key, label) {
|
|
168
|
-
return { key, label, turns: 0, input: 0, output: 0, cachedInput: 0, costUsd: null, unpriced: 0, billableUsd: null };
|
|
192
|
+
return { key, label, turns: 0, input: 0, output: 0, cachedInput: 0, costUsd: null, estimatedUsd: null, unpriced: 0, billableUsd: null };
|
|
193
|
+
}
|
|
194
|
+
/** Where a row's cost came from, for rows old and new; null when it has none. */
|
|
195
|
+
export function costSourceOf(row) {
|
|
196
|
+
if (finiteOrNull(row.costUsd) === null)
|
|
197
|
+
return null;
|
|
198
|
+
return row.costSource === "estimated" ? "estimated" : "reported";
|
|
169
199
|
}
|
|
170
200
|
function add(group, row, prices) {
|
|
171
201
|
group.turns += 1;
|
|
@@ -175,8 +205,11 @@ function add(group, row, prices) {
|
|
|
175
205
|
const cost = finiteOrNull(row.costUsd);
|
|
176
206
|
if (cost === null)
|
|
177
207
|
group.unpriced += 1;
|
|
178
|
-
else
|
|
208
|
+
else {
|
|
179
209
|
group.costUsd = (group.costUsd ?? 0) + cost;
|
|
210
|
+
if (costSourceOf(row) === "estimated")
|
|
211
|
+
group.estimatedUsd = (group.estimatedUsd ?? 0) + cost;
|
|
212
|
+
}
|
|
180
213
|
const billable = prices ? billableFor(row, prices) : null;
|
|
181
214
|
if (billable !== null)
|
|
182
215
|
group.billableUsd = (group.billableUsd ?? 0) + billable;
|
|
@@ -207,7 +240,7 @@ export function summarizeUsage(rows, groupBy, prices = null) {
|
|
|
207
240
|
}
|
|
208
241
|
return { groups: ordered, total };
|
|
209
242
|
}
|
|
210
|
-
function csvCell(value) {
|
|
243
|
+
export function csvCell(value) {
|
|
211
244
|
if (value === null)
|
|
212
245
|
return "";
|
|
213
246
|
const text = String(value);
|
|
@@ -218,7 +251,7 @@ function csvCell(value) {
|
|
|
218
251
|
}
|
|
219
252
|
/** One line per turn, spreadsheet-ready; a billable column when a price list is given. */
|
|
220
253
|
export function usageCsv(rows, prices = null) {
|
|
221
|
-
const header = ["time", "bot", "model", "engine", "triggered_by", "input_tokens", "output_tokens", "cached_input_tokens", "cost_usd", ...(prices ? ["billable_usd"] : []), "thread"];
|
|
254
|
+
const header = ["time", "bot", "model", "engine", "triggered_by", "input_tokens", "output_tokens", "cached_input_tokens", "cost_usd", "cost_source", ...(prices ? ["billable_usd"] : []), "thread"];
|
|
222
255
|
const lines = [header.join(",")];
|
|
223
256
|
for (const row of rows) {
|
|
224
257
|
lines.push([
|
|
@@ -231,6 +264,7 @@ export function usageCsv(rows, prices = null) {
|
|
|
231
264
|
clean(row.output),
|
|
232
265
|
clean(row.cachedInput),
|
|
233
266
|
finiteOrNull(row.costUsd),
|
|
267
|
+
costSourceOf(row),
|
|
234
268
|
...(prices ? [billableFor(row, prices)] : []),
|
|
235
269
|
row.threadId,
|
|
236
270
|
].map(csvCell).join(","));
|