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
|
@@ -15,44 +15,88 @@
|
|
|
15
15
|
// credentials included), and fire-and-forget — an audit log must never
|
|
16
16
|
// take down the decision it is auditing.
|
|
17
17
|
//
|
|
18
|
+
// Layout and retention follow the usage ledger: one file per UTC month under
|
|
19
|
+
// <data>/decisions/YYYY-MM.ndjson, and a month file is deleted only once the
|
|
20
|
+
// whole month is older than the retention window (default 180 days, config
|
|
21
|
+
// `decisions.retentionDays` or OMB_DECISION_RETENTION_DAYS), so at least that
|
|
22
|
+
// much history is always kept. Older servers wrote one decisions.ndjson that
|
|
23
|
+
// rotated to .1 at 4 MB; both are still read, never written, and removed by
|
|
24
|
+
// the same window once their last row is older than it.
|
|
25
|
+
//
|
|
18
26
|
// Deliberately NOT covered: ask_bot peer-approval cards. They never cross
|
|
19
27
|
// the runtime bus (peer-approval.ts appends its cards straight to the
|
|
20
28
|
// store), so wiring them here would mean a second, parallel tap — a
|
|
21
29
|
// separate change if it earns its keep.
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
30
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
31
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
32
|
+
import { appendFile, mkdir, readdir, stat, unlink } from "node:fs/promises";
|
|
33
|
+
import { basename, join } from "node:path";
|
|
25
34
|
import { redactSecrets } from "./redact.js";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
35
|
+
import { csvCell } from "./usage-ledger.js";
|
|
36
|
+
const DIR = "decisions";
|
|
37
|
+
const LEGACY_FILE = "decisions.ndjson";
|
|
38
|
+
const MONTH_FILE = /^(\d{4})-(\d{2})\.ndjson$/;
|
|
39
|
+
const DAY_MS = 24 * 60 * 60_000;
|
|
40
|
+
const PRUNE_EVERY_MS = 60 * 60_000;
|
|
41
|
+
export const DEFAULT_DECISION_RETENTION_DAYS = 180;
|
|
42
|
+
export const MAX_DECISION_RETENTION_DAYS = 3650;
|
|
33
43
|
const writeQueues = new Map();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
const lastPrune = new Map();
|
|
45
|
+
let retentionDays = () => DEFAULT_DECISION_RETENTION_DAYS;
|
|
46
|
+
/** The server binds its configured window once; see decisionRetentionDays. */
|
|
47
|
+
export function bindDecisionRetention(provider) {
|
|
48
|
+
retentionDays = provider;
|
|
49
|
+
}
|
|
50
|
+
/** The window in force now. The admin activity log (admin-activity.ts)
|
|
51
|
+
* keeps its months for the same time, so one setting covers both. */
|
|
52
|
+
export function boundRetentionDays() {
|
|
53
|
+
return retentionDays();
|
|
54
|
+
}
|
|
55
|
+
/** The retention window in days: OMB_DECISION_RETENTION_DAYS when it is a
|
|
56
|
+
* whole number in range, else the configured value, else 180. */
|
|
57
|
+
export function decisionRetentionDays(configured, env = process.env) {
|
|
58
|
+
const fromEnv = env.OMB_DECISION_RETENTION_DAYS?.trim();
|
|
59
|
+
const parsed = fromEnv ? Number(fromEnv) : NaN;
|
|
60
|
+
if (Number.isInteger(parsed) && parsed >= 1 && parsed <= MAX_DECISION_RETENTION_DAYS)
|
|
61
|
+
return parsed;
|
|
62
|
+
if (typeof configured === "number" && Number.isInteger(configured) && configured >= 1 && configured <= MAX_DECISION_RETENTION_DAYS)
|
|
63
|
+
return configured;
|
|
64
|
+
return DEFAULT_DECISION_RETENTION_DAYS;
|
|
65
|
+
}
|
|
66
|
+
const actorScope = new AsyncLocalStorage();
|
|
67
|
+
/** Run `work` as a person's card answer: every `source: "user"` row it
|
|
68
|
+
* writes names `actor`, without each resolver having to thread it through. */
|
|
69
|
+
export function withDecisionActor(actor, work) {
|
|
70
|
+
return actorScope.run(actor, work);
|
|
71
|
+
}
|
|
72
|
+
function monthKey(at) {
|
|
73
|
+
return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
74
|
+
}
|
|
75
|
+
export function decisionFileFor(dataDir, at) {
|
|
76
|
+
return join(dataDir, DIR, `${monthKey(at)}.ndjson`);
|
|
77
|
+
}
|
|
78
|
+
async function writeDecision(dataDir, record) {
|
|
79
|
+
const at = new Date(record.at);
|
|
80
|
+
await mkdir(join(dataDir, DIR), { recursive: true, mode: 0o700 });
|
|
81
|
+
await appendFile(decisionFileFor(dataDir, at), JSON.stringify(record) + "\n", { mode: 0o600 });
|
|
82
|
+
if (at.getTime() - (lastPrune.get(dataDir) ?? 0) >= PRUNE_EVERY_MS) {
|
|
83
|
+
lastPrune.set(dataDir, at.getTime());
|
|
84
|
+
await pruneDecisions(dataDir, retentionDays(), at);
|
|
42
85
|
}
|
|
43
|
-
const record = redactSecrets({ at: new Date().toISOString(), ...row });
|
|
44
|
-
await appendFile(file, JSON.stringify(record) + "\n", { mode: 0o600 });
|
|
45
86
|
}
|
|
46
87
|
/** Append one decision row. Fire-and-forget, mirroring the event bus tee:
|
|
47
88
|
* the fold that calls this is delivering approvals and cards, and a full
|
|
48
89
|
* disk must not turn into denied tools. */
|
|
49
|
-
export function appendDecision(dataDir, row
|
|
90
|
+
export function appendDecision(dataDir, row) {
|
|
91
|
+
const actor = row.actor ?? (row.source === "user" ? actorScope.getStore() : undefined);
|
|
92
|
+
// Redact now, not when the queue drains: the row is what was true at the
|
|
93
|
+
// moment of the decision.
|
|
94
|
+
const record = redactSecrets({ at: new Date().toISOString(), ...row, ...(actor ? { actor } : {}) });
|
|
50
95
|
const previous = writeQueues.get(dataDir) ?? Promise.resolve();
|
|
51
|
-
// Serialize
|
|
52
|
-
//
|
|
53
|
-
// or append out of decision order.
|
|
96
|
+
// Serialize appends (and the occasional prune) per directory, so two
|
|
97
|
+
// simultaneous approvals keep their decision order.
|
|
54
98
|
const queued = previous
|
|
55
|
-
.then(() => writeDecision(dataDir,
|
|
99
|
+
.then(() => writeDecision(dataDir, record))
|
|
56
100
|
.catch(() => {
|
|
57
101
|
/* logging must never take down the fold */
|
|
58
102
|
});
|
|
@@ -67,40 +111,157 @@ export function appendDecision(dataDir, row, opts) {
|
|
|
67
111
|
export async function flushDecisionLog(dataDir) {
|
|
68
112
|
await writeQueues.get(dataDir);
|
|
69
113
|
}
|
|
114
|
+
/** Delete what the window no longer covers: a month file once the first
|
|
115
|
+
* instant after its month is at or before `now - days`, a legacy file once
|
|
116
|
+
* its last write is. Returns the names removed. Never throws. */
|
|
117
|
+
export async function pruneDecisions(dataDir, days, now = new Date()) {
|
|
118
|
+
const cutoff = now.getTime() - days * DAY_MS;
|
|
119
|
+
const removed = await pruneMonthFiles(join(dataDir, DIR), days, now);
|
|
120
|
+
for (const name of [`${LEGACY_FILE}.1`, LEGACY_FILE]) {
|
|
121
|
+
try {
|
|
122
|
+
if ((await stat(join(dataDir, name))).mtimeMs > cutoff)
|
|
123
|
+
continue;
|
|
124
|
+
await unlink(join(dataDir, name));
|
|
125
|
+
removed.push(name);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* absent */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return removed;
|
|
132
|
+
}
|
|
133
|
+
/** Delete the `YYYY-MM.ndjson` files in `dir` whose whole month is at or
|
|
134
|
+
* before `now - days`. Returns the names removed. Never throws. Shared with
|
|
135
|
+
* the admin activity log. */
|
|
136
|
+
export async function pruneMonthFiles(dir, days, now = new Date()) {
|
|
137
|
+
const cutoff = now.getTime() - days * DAY_MS;
|
|
138
|
+
const removed = [];
|
|
139
|
+
let names = [];
|
|
140
|
+
try {
|
|
141
|
+
names = await readdir(dir);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
/* nothing written in the new layout yet */
|
|
145
|
+
}
|
|
146
|
+
for (const name of names) {
|
|
147
|
+
const match = MONTH_FILE.exec(name);
|
|
148
|
+
if (!match || Date.UTC(Number(match[1]), Number(match[2]), 1) > cutoff)
|
|
149
|
+
continue;
|
|
150
|
+
try {
|
|
151
|
+
await unlink(join(dir, name));
|
|
152
|
+
removed.push(name);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
/* already gone, or not ours to remove */
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return removed;
|
|
159
|
+
}
|
|
70
160
|
const isDecisionRow = (value) => typeof value === "object" &&
|
|
71
161
|
value !== null &&
|
|
72
162
|
typeof value.at === "string" &&
|
|
73
163
|
typeof value.threadId === "string" &&
|
|
74
164
|
typeof value.decision === "string" &&
|
|
75
165
|
typeof value.source === "string";
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
166
|
+
function readRows(path) {
|
|
167
|
+
let text;
|
|
168
|
+
try {
|
|
169
|
+
text = readFileSync(path, "utf8");
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
83
174
|
const rows = [];
|
|
84
|
-
for (const
|
|
85
|
-
|
|
175
|
+
for (const line of text.split("\n")) {
|
|
176
|
+
if (!line)
|
|
177
|
+
continue;
|
|
86
178
|
try {
|
|
87
|
-
|
|
179
|
+
const value = JSON.parse(line);
|
|
180
|
+
if (isDecisionRow(value))
|
|
181
|
+
rows.push(value);
|
|
88
182
|
}
|
|
89
183
|
catch {
|
|
90
|
-
|
|
184
|
+
/* a line torn mid-write — skip the fragment, keep the rest */
|
|
91
185
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
186
|
+
}
|
|
187
|
+
return rows;
|
|
188
|
+
}
|
|
189
|
+
/** Every file that may hold rows, oldest first: the legacy pair (which only
|
|
190
|
+
* ever holds rows older than any month file), then the months in order. */
|
|
191
|
+
function sources(dataDir) {
|
|
192
|
+
let months = [];
|
|
193
|
+
try {
|
|
194
|
+
months = readdirSync(join(dataDir, DIR)).filter((name) => MONTH_FILE.test(name)).sort();
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
/* no month files yet */
|
|
198
|
+
}
|
|
199
|
+
return [join(dataDir, `${LEGACY_FILE}.1`), join(dataDir, LEGACY_FILE), ...months.map((name) => join(dataDir, DIR, name))];
|
|
200
|
+
}
|
|
201
|
+
/** The newest `limit` rows, oldest first — the same order the inspector
|
|
202
|
+
* uses for thread events. Reads month files newest first and stops once it
|
|
203
|
+
* has enough, so a long history costs no more than the page asked for. */
|
|
204
|
+
export function readDecisions(dataDir, limit) {
|
|
205
|
+
const files = sources(dataDir);
|
|
206
|
+
const chunks = [];
|
|
207
|
+
let count = 0;
|
|
208
|
+
for (let index = files.length - 1; index >= 0 && count < limit; index -= 1) {
|
|
209
|
+
const rows = readRows(files[index]);
|
|
210
|
+
chunks.unshift(rows);
|
|
211
|
+
count += rows.length;
|
|
212
|
+
}
|
|
213
|
+
return chunks.flat().slice(-limit);
|
|
214
|
+
}
|
|
215
|
+
/** Every row whose time falls inside the range, oldest first. */
|
|
216
|
+
export function readDecisionRange(dataDir, range) {
|
|
217
|
+
const from = range.from.getTime();
|
|
218
|
+
const to = range.to.getTime();
|
|
219
|
+
const first = monthKey(range.from);
|
|
220
|
+
const last = monthKey(range.to);
|
|
221
|
+
const rows = [];
|
|
222
|
+
for (const file of sources(dataDir)) {
|
|
223
|
+
// Month files outside the range are skipped unread; legacy files have no month.
|
|
224
|
+
const month = MONTH_FILE.exec(basename(file));
|
|
225
|
+
if (month && (`${month[1]}-${month[2]}` < first || `${month[1]}-${month[2]}` > last))
|
|
226
|
+
continue;
|
|
227
|
+
for (const row of readRows(file)) {
|
|
228
|
+
const at = Date.parse(row.at);
|
|
229
|
+
if (at >= from && at <= to)
|
|
230
|
+
rows.push(row);
|
|
103
231
|
}
|
|
104
232
|
}
|
|
105
|
-
return rows
|
|
233
|
+
return rows;
|
|
234
|
+
}
|
|
235
|
+
export function actorLabel(actor) {
|
|
236
|
+
if (!actor)
|
|
237
|
+
return "";
|
|
238
|
+
if (actor.kind === "loopback")
|
|
239
|
+
return "This computer";
|
|
240
|
+
if (actor.kind === "worker")
|
|
241
|
+
return "Local service";
|
|
242
|
+
return actor.email ?? actor.label;
|
|
243
|
+
}
|
|
244
|
+
/** One line per decision, spreadsheet-ready. Cells go through the usage
|
|
245
|
+
* CSV's formula neutralising, and every row through redaction again, so an
|
|
246
|
+
* export never carries more than the log itself may. */
|
|
247
|
+
export function decisionsCsv(rows) {
|
|
248
|
+
const header = ["time", "decision", "source", "bot", "tool", "summary", "rule", "unattended", "answered_by", "thread", "request"];
|
|
249
|
+
const lines = [header.join(",")];
|
|
250
|
+
for (const raw of rows) {
|
|
251
|
+
const row = redactSecrets(raw);
|
|
252
|
+
lines.push([
|
|
253
|
+
row.at,
|
|
254
|
+
row.decision,
|
|
255
|
+
row.source,
|
|
256
|
+
row.botName || row.botId || "",
|
|
257
|
+
row.tool ?? "",
|
|
258
|
+
row.summary ?? "",
|
|
259
|
+
row.rule ?? "",
|
|
260
|
+
row.unattended ? "yes" : "",
|
|
261
|
+
actorLabel(row.actor),
|
|
262
|
+
row.threadId,
|
|
263
|
+
row.requestId ?? "",
|
|
264
|
+
].map(csvCell).join(","));
|
|
265
|
+
}
|
|
266
|
+
return lines.join("\n") + "\n";
|
|
106
267
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
const claudeFirst = (instances) => instances.find((instance) => instance.driverKind === "claudeAgent") ?? instances[0];
|
|
1
2
|
/** A saved choice is intentional: an unavailable provider or removed model
|
|
2
3
|
* sends new bots to setup instead of silently changing their provider. */
|
|
3
|
-
export function selectDefaultModelSelection(instances, preferred) {
|
|
4
|
+
export function selectDefaultModelSelection(instances, preferred, context = {}) {
|
|
4
5
|
if (preferred) {
|
|
5
6
|
const instance = instances.find((candidate) => candidate.instanceId === preferred.instanceId);
|
|
6
7
|
if (instance?.snapshot.state !== "available" ||
|
|
7
8
|
instance.snapshot.authenticated === false ||
|
|
9
|
+
// An organisation that disallows the saved engine makes it unusable,
|
|
10
|
+
// exactly like an unavailable one: setup, never another provider.
|
|
11
|
+
context.refusal?.(instance) !== undefined ||
|
|
8
12
|
(preferred.variant !== undefined && !instance.capabilities?.modelVariants) ||
|
|
9
13
|
!(instance.models.default === preferred.model || instance.models.options.some((model) => model.id === preferred.model))) {
|
|
10
14
|
return { instanceId: "", model: "" };
|
|
@@ -16,7 +20,29 @@ export function selectDefaultModelSelection(instances, preferred) {
|
|
|
16
20
|
delete selection.effort;
|
|
17
21
|
return selection;
|
|
18
22
|
}
|
|
19
|
-
|
|
20
|
-
const
|
|
23
|
+
// The organisation's policy is inert (no refusal) unless this desktop is enrolled.
|
|
24
|
+
const available = instances.filter((instance) => instance.snapshot.state === "available" && context.refusal?.(instance) === undefined);
|
|
25
|
+
const company = context.company;
|
|
26
|
+
let pick;
|
|
27
|
+
if (company && instances.some((instance) => company(instance.instanceId))) {
|
|
28
|
+
// Enrolled: an installed but signed-out personal Claude would send every
|
|
29
|
+
// new bot to setup although a Company model can run. Prefer instances that
|
|
30
|
+
// can run a turn now; a working personal engine still wins, so billing
|
|
31
|
+
// stays the person's explicit choice.
|
|
32
|
+
const ready = available.filter((instance) => instance.access === "custom" || instance.snapshot.authenticated !== false);
|
|
33
|
+
pick = claudeFirst(ready.filter((instance) => !company(instance.instanceId))) ?? claudeFirst(ready) ?? claudeFirst(available);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
pick = claudeFirst(available);
|
|
37
|
+
}
|
|
21
38
|
return { instanceId: pick?.instanceId ?? "", model: pick?.models.default ?? "" };
|
|
22
39
|
}
|
|
40
|
+
/** Complete a new bot's selection with the workspace's new-bot effort. An
|
|
41
|
+
* explicit effort or model variant is the caller's choice and wins; an engine
|
|
42
|
+
* that does not offer the level keeps sending none rather than failing turn 1. */
|
|
43
|
+
export function withNewBotEffort(selection, effort, offered) {
|
|
44
|
+
if (!effort || selection.effort !== undefined || selection.variant !== undefined || !offered?.includes(effort)) {
|
|
45
|
+
return selection;
|
|
46
|
+
}
|
|
47
|
+
return { ...selection, effort };
|
|
48
|
+
}
|
|
@@ -16,7 +16,7 @@ import { writeFileAtomic } from "./atomic.js";
|
|
|
16
16
|
import { getOrCreateChannel, mirrorExchange } from "./comms-visibility.js";
|
|
17
17
|
import { DATA_DIR } from "./config.js";
|
|
18
18
|
import { newId } from "./contracts.js";
|
|
19
|
-
import { requestPeerApproval } from "./peer-approval.js";
|
|
19
|
+
import { peerApprovalFailure, requestPeerApproval } from "./peer-approval.js";
|
|
20
20
|
import { canAccessTeam, peerAllowed } from "./peer-roster.js";
|
|
21
21
|
/** Per source-thread queue. Persisted to delegations.json on every change
|
|
22
22
|
* and reloaded at boot: a handoff queued right before a restart runs after
|
|
@@ -40,6 +40,13 @@ const RESULT_MAX_CHARS = 4_000;
|
|
|
40
40
|
* to expire. This is not an uptime clock: sleep within a running process
|
|
41
41
|
* still counts, and a non-expired restored window keeps its deadline. */
|
|
42
42
|
export const DELEGATION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
43
|
+
/** How long a handoff may wait on a target that never goes idle. The 24-hour
|
|
44
|
+
* window above bounds the rare case; the busy hold is the common one — a peer
|
|
45
|
+
* stays busy for a whole day and the delegating bot hears nothing back. Past
|
|
46
|
+
* this cap a still-blocked handoff expires with its own wording. Env-tunable
|
|
47
|
+
* so tests (and patient teams) can shrink or stretch it. */
|
|
48
|
+
const configuredBusyHoldMaxMs = Number(process.env.OMB_DELEGATION_BUSY_HOLD_MAX_MS);
|
|
49
|
+
export const DELEGATION_BUSY_HOLD_MAX_MS = Math.max(1_000, Number.isFinite(configuredBusyHoldMaxMs) && process.env.OMB_DELEGATION_BUSY_HOLD_MAX_MS !== "" ? configuredBusyHoldMaxMs : 2 * 60 * 60 * 1000);
|
|
43
50
|
let receipts = [];
|
|
44
51
|
function saveReceipts() {
|
|
45
52
|
try {
|
|
@@ -63,6 +70,10 @@ export function recordDelegationReceipt(receipt) {
|
|
|
63
70
|
};
|
|
64
71
|
if (receipt.result !== undefined)
|
|
65
72
|
bounded.result = receipt.result.slice(0, RESULT_MAX_CHARS);
|
|
73
|
+
if (receipt.approvalOutcome !== undefined) {
|
|
74
|
+
bounded.approvalOutcome = receipt.approvalOutcome;
|
|
75
|
+
bounded.approvalSource = peerApprovalFailure(receipt.approvalOutcome).approvalSource;
|
|
76
|
+
}
|
|
66
77
|
receipts = [bounded, ...receipts.filter((existing) => existing.id !== bounded.id)]
|
|
67
78
|
.filter((existing) => now - existing.finishedAt <= RECEIPT_MAX_AGE_MS)
|
|
68
79
|
.slice(0, MAX_RECEIPTS);
|
|
@@ -90,9 +101,9 @@ export function threadsWaitingOn(toBotId) {
|
|
|
90
101
|
}
|
|
91
102
|
/** Mark a target's observed busy period as finished and return the source
|
|
92
103
|
* threads that should be retried. A handoff waits until the target is free,
|
|
93
|
-
* bounded
|
|
94
|
-
* busy period" marker so the next drain re-evaluates it, rather
|
|
95
|
-
* counting or limiting retries.
|
|
104
|
+
* bounded by the busy-hold cap and the 24-hour expiry — this just clears the
|
|
105
|
+
* "parked on a busy period" marker so the next drain re-evaluates it, rather
|
|
106
|
+
* than counting or limiting retries.
|
|
96
107
|
* `only` narrows the release: a bot that is still busy in one thread has
|
|
97
108
|
* nevertheless freed a slot for the fresh-thread handoffs waiting on it,
|
|
98
109
|
* while its active-thread handoffs go on waiting for it to go idle. */
|
|
@@ -104,6 +115,7 @@ export function releaseDelegationsWaitingOn(toBotId, only) {
|
|
|
104
115
|
if (item.toBotId !== toBotId || item.waitingOnBusy !== true || (only && !only(item)))
|
|
105
116
|
continue;
|
|
106
117
|
delete item.waitingOnBusy;
|
|
118
|
+
delete item.busySince;
|
|
107
119
|
any = true;
|
|
108
120
|
}
|
|
109
121
|
if (any)
|
|
@@ -164,8 +176,14 @@ export function _loadPending() {
|
|
|
164
176
|
};
|
|
165
177
|
if (item.approvalAlreadyGranted === true)
|
|
166
178
|
loaded.approvalAlreadyGranted = true;
|
|
167
|
-
if (item.waitingOnBusy === true)
|
|
179
|
+
if (item.waitingOnBusy === true) {
|
|
168
180
|
loaded.waitingOnBusy = true;
|
|
181
|
+
const currentHold = Number.isFinite(item.busySince) && item.busySince <= now &&
|
|
182
|
+
now - item.busySince < DELEGATION_BUSY_HOLD_MAX_MS;
|
|
183
|
+
loaded.busySince = currentHold ? item.busySince : now;
|
|
184
|
+
if (!currentHold)
|
|
185
|
+
backfilled = true;
|
|
186
|
+
}
|
|
169
187
|
if (item.waitAnnounced === true || legacyAlreadyAnnounced)
|
|
170
188
|
loaded.waitAnnounced = true;
|
|
171
189
|
if (typeof item.originatingGroupId === "string" && item.originatingGroupId) {
|
|
@@ -212,6 +230,10 @@ export function _loadPending() {
|
|
|
212
230
|
const receipt = { id, sourceThreadId, toBotId, toBotName, status, finishedAt: finishedAt };
|
|
213
231
|
if (typeof result === "string")
|
|
214
232
|
receipt.result = result;
|
|
233
|
+
if (candidate.approvalOutcome === "deny" || candidate.approvalOutcome === "expired" || candidate.approvalOutcome === "cancelled") {
|
|
234
|
+
receipt.approvalOutcome = candidate.approvalOutcome;
|
|
235
|
+
receipt.approvalSource = peerApprovalFailure(candidate.approvalOutcome).approvalSource;
|
|
236
|
+
}
|
|
215
237
|
loaded.push(receipt);
|
|
216
238
|
}
|
|
217
239
|
receipts = loaded.slice(0, MAX_RECEIPTS);
|
|
@@ -405,41 +427,61 @@ function acknowledgeDelegation(threadId, itemId) {
|
|
|
405
427
|
savePending();
|
|
406
428
|
}
|
|
407
429
|
const isExpired = (item, now) => now - item.queuedAt >= DELEGATION_TTL_MS;
|
|
430
|
+
/** Past the busy-hold cap — the tighter bound that fires while its target is
|
|
431
|
+
* still busy, long before the 24-hour window. */
|
|
432
|
+
const busyHoldExpired = (item, now) => item.busySince !== undefined && now - item.busySince >= DELEGATION_BUSY_HOLD_MAX_MS;
|
|
433
|
+
/** The busy-hold cap in chip-ready words ("2 hours", "90 minutes"). */
|
|
434
|
+
export function busyHoldCapText(maxMs = DELEGATION_BUSY_HOLD_MAX_MS) {
|
|
435
|
+
const minutes = Math.max(1, Math.round(maxMs / 60_000));
|
|
436
|
+
const amount = minutes % 60 === 0 ? minutes / 60 : minutes;
|
|
437
|
+
const unit = minutes % 60 === 0 ? "hour" : "minute";
|
|
438
|
+
return `${amount} ${unit}${amount === 1 ? "" : "s"}`;
|
|
439
|
+
}
|
|
408
440
|
/** Record an expired handoff. The chip goes into the source thread only
|
|
409
441
|
* while it still belongs to the bot that owns the handoff — a deleted
|
|
410
442
|
* conversation gets the receipt and nothing else. */
|
|
411
443
|
function expireDelegation(bus, sourceThreadId, item, ownerId) {
|
|
412
|
-
const
|
|
444
|
+
const target = bus.store.bot(item.toBotId);
|
|
445
|
+
const name = target?.name ?? item.toBotId;
|
|
446
|
+
// A busy hold has its own words so a two-hour busy wait is never reported
|
|
447
|
+
// as a 24-hour one; every other expiry keeps the TTL wording.
|
|
448
|
+
const now = Date.now();
|
|
449
|
+
const busyHold = Boolean(target) && busyHoldExpired(item, now) && !isExpired(item, now);
|
|
413
450
|
recordDelegationReceipt({
|
|
414
451
|
id: item.id,
|
|
415
452
|
sourceThreadId,
|
|
416
453
|
toBotId: item.toBotId,
|
|
417
454
|
toBotName: name,
|
|
418
455
|
status: "expired",
|
|
419
|
-
result: `@${name} was not free to take this for 24 hours`,
|
|
456
|
+
result: busyHold ? `@${name} was still busy after ${busyHoldCapText()}` : `@${name} was not free to take this for 24 hours`,
|
|
420
457
|
});
|
|
421
458
|
if (!sourceThreadBelongsToBot(bus.store, ownerId, sourceThreadId))
|
|
422
459
|
return;
|
|
423
460
|
bus.store.appendMessage(sourceThreadId, {
|
|
424
461
|
role: "bot",
|
|
425
462
|
kind: "activity",
|
|
426
|
-
tool: {
|
|
463
|
+
tool: {
|
|
464
|
+
name: busyHold
|
|
465
|
+
? `Delegation to @${name} expired — still busy after ${busyHoldCapText()}`
|
|
466
|
+
: `Delegation to @${name} expired — not picked up within 24 hours`,
|
|
467
|
+
ok: false,
|
|
468
|
+
},
|
|
427
469
|
});
|
|
428
470
|
}
|
|
429
|
-
/** Past
|
|
471
|
+
/** Past a delivery bound AND unable to be delivered right now — the same rule
|
|
430
472
|
* `processOne` applies, so the hourly sweep and a live drain never disagree
|
|
431
|
-
* about which items are actually stuck.
|
|
432
|
-
*
|
|
433
|
-
*
|
|
473
|
+
* about which items are actually stuck. The busy-hold cap expires a handoff
|
|
474
|
+
* whose target has been busy the whole time; the 24-hour TTL catches the
|
|
475
|
+
* rest. A target that was deleted counts as "cannot take the turn": there is
|
|
476
|
+
* nothing to wait on, so such items still expire even though there is no bot
|
|
477
|
+
* left to test busy/free against. */
|
|
434
478
|
function isDueForExpiry(bus, item, now) {
|
|
435
|
-
if (!isExpired(item, now))
|
|
436
|
-
return false;
|
|
437
479
|
const target = bus.store.bot(item.toBotId);
|
|
438
480
|
if (!target)
|
|
439
|
-
return
|
|
440
|
-
return !targetCanTakeTurn(bus, target, item);
|
|
481
|
+
return isExpired(item, now);
|
|
482
|
+
return !targetCanTakeTurn(bus, target, item) && (isExpired(item, now) || busyHoldExpired(item, now));
|
|
441
483
|
}
|
|
442
|
-
/** Expire every queued handoff past
|
|
484
|
+
/** Expire every queued handoff past a delivery bound that still cannot be
|
|
443
485
|
* delivered, wherever it waits. A drain already expires what it touches;
|
|
444
486
|
* this covers the handoff nothing drains — a target that never settles
|
|
445
487
|
* while its source sits idle. A thread mid-drain is skipped: that drain
|
|
@@ -549,13 +591,15 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
549
591
|
if (dropIfThreadGone(bus, target, sourceThreadId, item)) {
|
|
550
592
|
return "settled";
|
|
551
593
|
}
|
|
552
|
-
// Past
|
|
553
|
-
// bound on a busy wait
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
594
|
+
// Past a delivery bound AND the target still cannot take the turn: this is
|
|
595
|
+
// the bound on a busy wait — the busy-hold cap when the target has been
|
|
596
|
+
// busy the whole time, the 24-hour TTL otherwise. Use the same free/busy
|
|
597
|
+
// test as holdWhileTargetBusy; an available target gets even an overdue
|
|
598
|
+
// item. Restart recovery renews elapsed windows in _loadPending before any
|
|
599
|
+
// target becomes busy. Decide before announcing a wait so an item cannot
|
|
600
|
+
// post both chips in one pass.
|
|
557
601
|
const canTakeTurn = targetCanTakeTurn(bus, target, item);
|
|
558
|
-
if (!canTakeTurn && isExpired(item, Date.now())) {
|
|
602
|
+
if (!canTakeTurn && (isExpired(item, Date.now()) || busyHoldExpired(item, Date.now()))) {
|
|
559
603
|
expireDelegation(bus, sourceThreadId, item, sender.id);
|
|
560
604
|
return "settled";
|
|
561
605
|
}
|
|
@@ -564,6 +608,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
564
608
|
return held;
|
|
565
609
|
if (item.waitingOnBusy) {
|
|
566
610
|
delete item.waitingOnBusy;
|
|
611
|
+
delete item.busySince;
|
|
567
612
|
savePending();
|
|
568
613
|
}
|
|
569
614
|
if (sender.approvePeerComms && !item.approvalAlreadyGranted) {
|
|
@@ -582,22 +627,26 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
582
627
|
toBotName: target.name,
|
|
583
628
|
status: "dropped",
|
|
584
629
|
result: "the peer or source conversation no longer exists",
|
|
630
|
+
...(verdict !== "allow" ? { approvalOutcome: verdict } : {}),
|
|
585
631
|
});
|
|
586
632
|
return "settled";
|
|
587
633
|
}
|
|
588
634
|
if (verdict !== "allow") {
|
|
635
|
+
const failure = peerApprovalFailure(verdict);
|
|
589
636
|
recordDelegationReceipt({
|
|
590
637
|
id: item.id,
|
|
591
638
|
sourceThreadId,
|
|
592
639
|
toBotId: target.id,
|
|
593
640
|
toBotName: target.name,
|
|
594
|
-
status: "denied",
|
|
595
|
-
|
|
641
|
+
status: verdict === "deny" ? "denied" : verdict,
|
|
642
|
+
approvalOutcome: verdict,
|
|
643
|
+
result: verdict === "deny" ? "the user denied this handoff" : failure.error,
|
|
596
644
|
});
|
|
597
645
|
bus.store.appendMessage(sourceThreadId, {
|
|
598
646
|
role: "bot",
|
|
599
647
|
kind: "activity",
|
|
600
|
-
tool: { name: `Delegation to @${target.name} denied by user
|
|
648
|
+
tool: { name: verdict === "deny" ? `Delegation to @${target.name} denied by user`
|
|
649
|
+
: `Delegation to @${target.name}: ${failure.error}`, ok: false },
|
|
601
650
|
});
|
|
602
651
|
return "settled";
|
|
603
652
|
}
|
|
@@ -614,9 +663,10 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
614
663
|
}
|
|
615
664
|
// Approval may have waited for minutes (or, with approvalAlreadyGranted,
|
|
616
665
|
// up to 24h since the original ask_bot approval) — recheck the same
|
|
617
|
-
// free/busy-gated expiry as the pre-approval path
|
|
666
|
+
// free/busy-gated, busy-hold-capped expiry as the pre-approval path
|
|
667
|
+
// before dispatching.
|
|
618
668
|
const canTakeTurnAfterApproval = targetCanTakeTurn(bus, current, item);
|
|
619
|
-
if (!canTakeTurnAfterApproval && isExpired(item, Date.now())) {
|
|
669
|
+
if (!canTakeTurnAfterApproval && (isExpired(item, Date.now()) || busyHoldExpired(item, Date.now()))) {
|
|
620
670
|
expireDelegation(bus, sourceThreadId, item, currentSender.id);
|
|
621
671
|
return "settled";
|
|
622
672
|
}
|
|
@@ -642,30 +692,33 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
642
692
|
await runTarget(item.toBotId, prefixed, item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId);
|
|
643
693
|
return "dispatched";
|
|
644
694
|
}
|
|
645
|
-
/** "Is the target free to take this handoff right now?"
|
|
646
|
-
*
|
|
647
|
-
* target's
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
* delivered right now. */
|
|
695
|
+
/** "Is the target free to take this handoff right now?" Both shapes ask for
|
|
696
|
+
* a free capacity slot, never whole-bot idleness: a classic delegation lands
|
|
697
|
+
* in the target's standing thread and applies the same admission startTurn
|
|
698
|
+
* uses for a direct turn there, while a fresh-thread handoff needs any free
|
|
699
|
+
* slot. This is the single free/busy test shared by the expiry decision in
|
|
700
|
+
* `processOne` and the hold decision below, so the two can never disagree
|
|
701
|
+
* about whether a handoff could have been delivered right now. */
|
|
652
702
|
function targetCanTakeTurn(bus, target, item) {
|
|
653
703
|
return item.targetThreadId
|
|
654
704
|
? (bus.threadSlotFree ? bus.threadSlotFree(target.id) : !target.busy)
|
|
655
|
-
:
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
705
|
+
: bus.canAdmitDirectTurn
|
|
706
|
+
? bus.canAdmitDirectTurn(target.id, target.threadId)
|
|
707
|
+
: !target.busy;
|
|
708
|
+
}
|
|
709
|
+
/** A busy target holds the handoff. Neither counts busy periods — the bounds
|
|
710
|
+
* are the busy-hold cap and DELEGATION_TTL_MS, checked in processOne before
|
|
711
|
+
* this runs (using the same `targetCanTakeTurn` test, passed in as
|
|
712
|
+
* `canTakeTurn` when the caller already computed it so the two checks can't
|
|
713
|
+
* disagree). One waiting chip per handoff, worded for what the target is
|
|
714
|
+
* actually doing. Returns null when the target can take the turn now. */
|
|
663
715
|
function holdWhileTargetBusy(bus, target, sourceThreadId, item, canTakeTurn = targetCanTakeTurn(bus, target, item)) {
|
|
664
716
|
if (canTakeTurn)
|
|
665
717
|
return null;
|
|
666
718
|
if (item.waitingOnBusy)
|
|
667
719
|
return "requeued";
|
|
668
720
|
item.waitingOnBusy = true;
|
|
721
|
+
item.busySince = Date.now();
|
|
669
722
|
if (!item.waitAnnounced) {
|
|
670
723
|
item.waitAnnounced = true;
|
|
671
724
|
bus.store.appendMessage(sourceThreadId, {
|
|
@@ -724,7 +777,7 @@ function sourceThreadBelongsToBot(store, botId, threadId) {
|
|
|
724
777
|
* have moved either bot, or narrowed the sender's peers, in between. */
|
|
725
778
|
function dropIfUnreachable(bus, sender, target, sourceThreadId, item) {
|
|
726
779
|
const sectionsDiffer = !canAccessTeam(sender, target.section);
|
|
727
|
-
if (!sectionsDiffer && !target.hidden && peerAllowed(sender, target
|
|
780
|
+
if (!sectionsDiffer && !target.hidden && peerAllowed(sender, target))
|
|
728
781
|
return false;
|
|
729
782
|
const reason = sectionsDiffer
|
|
730
783
|
? "bots now belong to different sections"
|