openmausbot 0.1.84 → 0.1.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/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-DpUZNnFD.js +1 -0
- package/dist/assets/arc-DW7mrI4p.js +1 -0
- package/dist/assets/architectureDiagram-NJMV4G6O-CyeZ5WLZ.js +36 -0
- package/dist/assets/blockDiagram-BEXU5L5S-CBVaNjO7.js +140 -0
- package/dist/assets/c4Diagram-YGBWAQC7-sf8d41uT.js +38 -0
- package/dist/assets/channel-BXFkgus9.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-BOGvy7Y_.js +1 -0
- package/dist/assets/chunk-3FUC2YCW-BiF9zyiz.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-EFuZ1SBZ.js +15 -0
- package/dist/assets/chunk-GTNCS2PH-pJLNSAT7.js +88 -0
- package/dist/assets/chunk-GWA4HPMP-Cb-3_3IT.js +1 -0
- package/dist/assets/chunk-JWPE2WC7-DAf5aogn.js +1 -0
- package/dist/assets/chunk-NETBCI7D-DR3kiwZz.js +1 -0
- package/dist/assets/chunk-WEXAMYUT-BmU8PeyM.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-Buh8g8OT.js +1 -0
- package/dist/assets/classDiagram-v2-NBCMYWYE-BurfmMWM.js +217 -0
- package/dist/assets/cose-bilkent-JH36ORCC-KkGZUEkU.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-C3PpV96d.js +166 -0
- package/dist/assets/cynefinDiagram-VND7K2PF-Dt8JwJaI.js +62 -0
- package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
- package/dist/assets/dagre-6A5THRUB-DkjN8266.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-22UHCM2B-DUAJS0JV.js +200 -0
- package/dist/assets/diagram-3UASUU5V-Dw4KhF1a.js +24 -0
- package/dist/assets/diagram-ATOU4E4O-Dd1gmQqf.js +3 -0
- package/dist/assets/diagram-CDSNMT55-CjwGs0Yw.js +30 -0
- package/dist/assets/diagram-MLGK6HIB-BL-69Zrq.js +24 -0
- package/dist/assets/diagram-MPIPVDR6-5Zi2N7wo.js +41 -0
- package/dist/assets/ebnfDiagram-ZINNZB2B-D9NekWUX.js +1 -0
- package/dist/assets/elk-276RUBZZ-CkaSQjxw.js +27 -0
- package/dist/assets/erDiagram-OPXOYQCR-CdAVBV8-.js +99 -0
- package/dist/assets/flowDiagram-KWPJA3E3-Bm_lWWH7.js +213 -0
- package/dist/assets/ganttDiagram-FUAMR5RP-FZ8ejYWJ.js +292 -0
- package/dist/assets/gitGraphDiagram-X574FWY7-Dlv9rizZ.js +106 -0
- package/dist/assets/graph-D8UmR1vK.js +1 -0
- package/dist/assets/index-BGgqlWPJ.css +1 -0
- package/dist/assets/index-BtHo8ZKl.js +577 -0
- package/dist/assets/{index-DNa2umw-.js → index-h9FFNwTu.js} +1 -1
- package/dist/assets/infoDiagram-VRGFBTTK-BdQ_gW5B.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-OU5B5YK6-EwssEp2X.js +70 -0
- package/dist/assets/journeyDiagram-ZHPQQLJL-BX1G1vOd.js +139 -0
- package/dist/assets/kanban-definition-PNTS6WVX-BB4kAsyC.js +89 -0
- package/dist/assets/linear-3su2TG93.js +1 -0
- package/dist/assets/mermaid.core-CDD1E0s9.js +311 -0
- package/dist/assets/mindmap-definition-NLK3R4M7-Ba50MsNK.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pegDiagram-GJSIUBJH-DZLkkZlM.js +1 -0
- package/dist/assets/pieDiagram-5QR66LMP-DaTz77Uq.js +39 -0
- package/dist/assets/quadrantDiagram-O4NWA36T-CpaiidpR.js +7 -0
- package/dist/assets/railroadDiagram-XR7U4H2S-mN4JrMeI.js +1 -0
- package/dist/assets/requirementDiagram-PLB6GJNP-BSNDRpg7.js +84 -0
- package/dist/assets/sankeyDiagram-IPEJSGJF-FngzGNug.js +40 -0
- package/dist/assets/sequenceDiagram-PO4LG4MO-CM4pCC3L.js +169 -0
- package/dist/assets/sizeCapture-INFHLROL-CRICHXkw.js +1 -0
- package/dist/assets/stateDiagram-v2-GCMORJYK-oyTWpxH2.js +291 -0
- package/dist/assets/swimlanes-2SLR337P-D9h9mPBq.js +1 -0
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +8 -0
- package/dist/assets/timeline-definition-EJHVYXUP-BJUlN9bW.js +120 -0
- package/dist/assets/usecaseDiagram-POWQR4AR-3uBNkbAb.js +370 -0
- package/dist/assets/vennDiagram-UO4OBE2U-D2odalzT.js +34 -0
- package/dist/assets/wardleyDiagram-VNRHLVJA-DHgFaB9S.js +78 -0
- package/dist/assets/xychartDiagram-PMCCYNJV-w5GYM2h0.js +7 -0
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +1 -0
- package/dist-server/container-mcp.js +231 -17
- package/dist-server/drivers/agents-proxy.js +16097 -1411
- package/dist-server/enterprise/LICENSE +32 -0
- package/dist-server/enterprise/server/index.js +6 -5
- package/dist-server/hooks/omb-hook.js +74 -0
- package/dist-server/index.js +12495 -7584
- package/dist-server/local-computer-proxy.js +83 -1
- package/dist-server/local-computer.js +14 -2
- package/dist-server/mcp-server.js +3 -1
- package/dist-server/openmausbot.js +5458 -4587
- package/dist-server/pair-cli.js +5458 -4587
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/admin-activity.js +410 -0
- package/dist-server/server/agent-tool-policy.js +1 -0
- package/dist-server/server/bot-package.js +2 -0
- package/dist-server/server/bot-visibility.js +462 -0
- package/dist-server/server/box.js +25 -5
- package/dist-server/server/browser-engine.js +87 -10
- package/dist-server/server/browser-live.js +7 -5
- package/dist-server/server/browser-runtime.js +66 -11
- package/dist-server/server/channel-queue.js +2 -0
- package/dist-server/server/checkpoints.js +62 -18
- package/dist-server/server/cli-prompts.js +26 -2
- package/dist-server/server/cli.js +79 -10
- package/dist-server/server/commands.js +27 -0
- package/dist-server/server/compaction-summary.js +78 -0
- package/dist-server/server/composio.js +49 -11
- package/dist-server/server/config.js +137 -14
- package/dist-server/server/context-budget.js +29 -0
- package/dist-server/server/context-rebuild.js +72 -0
- package/dist-server/server/decision-log.js +209 -48
- package/dist-server/server/default-model-selection.js +20 -3
- package/dist-server/server/delegations.js +11 -9
- package/dist-server/server/digest.js +130 -0
- package/dist-server/server/drivers/acp/core.js +637 -238
- package/dist-server/server/drivers/acp/grok.js +4 -3
- package/dist-server/server/drivers/agents-call.js +1123 -0
- package/dist-server/server/drivers/agents-catalog.js +659 -0
- package/dist-server/server/drivers/agents-client.js +28 -0
- package/dist-server/server/drivers/agents-proxy.js +18 -1683
- package/dist-server/server/drivers/agents-result.js +23 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +17 -2
- package/dist-server/server/drivers/claude.js +83 -16
- package/dist-server/server/drivers/codex-mcp-names.js +44 -0
- package/dist-server/server/drivers/codex.js +32 -4
- package/dist-server/server/drivers/grok.js +2 -1
- package/dist-server/server/drivers/openai-chat.js +66 -45
- package/dist-server/server/drivers/openai-compat.js +16 -8
- package/dist-server/server/enterprise.js +97 -16
- package/dist-server/server/external-runtime.js +83 -0
- package/dist-server/server/guarded-requests.js +60 -0
- package/dist-server/server/harness/http.js +61 -0
- package/dist-server/server/hooks/omb-hook.js +103 -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 +2810 -678
- package/dist-server/server/local-computer.js +21 -2
- 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 +10 -2
- package/dist-server/server/mcp-tool-images.js +37 -0
- package/dist-server/server/mcp-tool-schema.js +108 -0
- package/dist-server/server/message-db.js +108 -21
- package/dist-server/server/message-file.js +3 -2
- package/dist-server/server/model-context-window.js +7 -0
- package/dist-server/server/model-prices.js +121 -0
- package/dist-server/server/notify.js +10 -1
- package/dist-server/server/package-export.js +1 -0
- package/dist-server/server/peer-roster.js +17 -4
- package/dist-server/server/provider-icon.js +20 -0
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/recent-work.js +15 -0
- package/dist-server/server/request-auth.js +97 -0
- package/dist-server/server/room-handoffs.js +41 -13
- 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 +39 -1
- package/dist-server/server/routines.js +65 -9
- package/dist-server/server/shared-computer-control.js +31 -4
- package/dist-server/server/spend.js +111 -16
- package/dist-server/server/steer-queue.js +12 -1
- package/dist-server/server/store.js +384 -33
- package/dist-server/server/system-prompt.js +4 -4
- package/dist-server/server/team-backup.js +19 -2
- package/dist-server/server/thread-starters.js +53 -0
- package/dist-server/server/tool-results.js +72 -0
- package/dist-server/server/tts/grok.js +74 -0
- package/dist-server/server/tts/index.js +19 -1
- package/dist-server/server/usage-ledger.js +31 -8
- package/dist-server/server/webhooks.js +28 -0
- package/dist-server/server/workspace-backup-policy.js +9 -2
- package/dist-server/server/workspace-backup.js +7 -4
- package/dist-server/shared/approval-mode.js +7 -1
- package/dist-server/shared/digest.js +1 -0
- package/dist-server/shared/markdown-windows-paths.js +36 -0
- package/dist-server/shared/mention-boundary.js +15 -0
- package/dist-server/shared/provider-icon.js +110 -0
- package/dist-server/shared/routines.js +7 -1
- package/dist-server/shared/team-backup.js +5 -0
- package/dist-server/src/lib/composer-attachments.js +16 -0
- package/dist-server/vps-container-mcp.js +236 -22
- package/enterprise/README.md +33 -12
- package/enterprise/server/index.js +6 -5
- package/package.json +1 -1
- package/dist/assets/index-BbU5REzd.js +0 -310
- package/dist/assets/index-CLGfYlx_.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,20 @@ 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
|
}
|
|
@@ -642,17 +642,19 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
642
642
|
await runTarget(item.toBotId, prefixed, item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId);
|
|
643
643
|
return "dispatched";
|
|
644
644
|
}
|
|
645
|
-
/** "Is the target free to take this handoff right now?"
|
|
646
|
-
*
|
|
647
|
-
* target's
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
* delivered right now. */
|
|
645
|
+
/** "Is the target free to take this handoff right now?" Both shapes ask for
|
|
646
|
+
* a free capacity slot, never whole-bot idleness: a classic delegation lands
|
|
647
|
+
* in the target's standing thread and applies the same admission startTurn
|
|
648
|
+
* uses for a direct turn there, while a fresh-thread handoff needs any free
|
|
649
|
+
* slot. This is the single free/busy test shared by the expiry decision in
|
|
650
|
+
* `processOne` and the hold decision below, so the two can never disagree
|
|
651
|
+
* about whether a handoff could have been delivered right now. */
|
|
652
652
|
function targetCanTakeTurn(bus, target, item) {
|
|
653
653
|
return item.targetThreadId
|
|
654
654
|
? (bus.threadSlotFree ? bus.threadSlotFree(target.id) : !target.busy)
|
|
655
|
-
:
|
|
655
|
+
: bus.canAdmitDirectTurn
|
|
656
|
+
? bus.canAdmitDirectTurn(target.id, target.threadId)
|
|
657
|
+
: !target.busy;
|
|
656
658
|
}
|
|
657
659
|
/** A busy target holds the handoff. Neither counts busy periods — the only
|
|
658
660
|
* bound is DELEGATION_TTL_MS, checked in processOne before this runs (using
|
|
@@ -724,7 +726,7 @@ function sourceThreadBelongsToBot(store, botId, threadId) {
|
|
|
724
726
|
* have moved either bot, or narrowed the sender's peers, in between. */
|
|
725
727
|
function dropIfUnreachable(bus, sender, target, sourceThreadId, item) {
|
|
726
728
|
const sectionsDiffer = !canAccessTeam(sender, target.section);
|
|
727
|
-
if (!sectionsDiffer && !target.hidden && peerAllowed(sender, target
|
|
729
|
+
if (!sectionsDiffer && !target.hidden && peerAllowed(sender, target))
|
|
728
730
|
return false;
|
|
729
731
|
const reason = sectionsDiffer
|
|
730
732
|
? "bots now belong to different sections"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** Evidence is observed, not inferred from a provider's brand. In particular,
|
|
2
|
+
* OpenAI-compatible and Grok turns now expose structured tool activity too. */
|
|
3
|
+
export function coverageForDriver(_driverKind, fullResults = false, observedTools = false) {
|
|
4
|
+
return fullResults ? "full" : observedTools ? "preview" : "none";
|
|
5
|
+
}
|
|
6
|
+
/** True when every tool row of the turn carries a delivered (untruncated)
|
|
7
|
+
* result — what a PostToolUse hook gives us — so the digest may claim
|
|
8
|
+
* "full" evidence. One preview-only row means the turn is "preview". */
|
|
9
|
+
export function toolEvidence(activities, turnId) {
|
|
10
|
+
const rows = activities.filter((m) => m.kind === "activity" && m.tool?.itemId && m.turnId === turnId);
|
|
11
|
+
return rows.length > 0 && rows.every((m) => m.tool?.fullResult === true);
|
|
12
|
+
}
|
|
13
|
+
export const MAX_TOOLS = 8;
|
|
14
|
+
export const MAX_FILES = 20;
|
|
15
|
+
export const MAX_MEMORY = 20;
|
|
16
|
+
export const REPLY_CHARS = 200;
|
|
17
|
+
export const RENDER_BYTES = 1_500;
|
|
18
|
+
export function buildTurnDigest(input) {
|
|
19
|
+
const byName = new Map();
|
|
20
|
+
for (const m of input.activities) {
|
|
21
|
+
if (m.kind !== "activity" || !m.tool?.itemId || m.turnId !== input.turnId)
|
|
22
|
+
continue;
|
|
23
|
+
const entry = byName.get(m.tool.name) ?? { name: fitBytes(m.tool.name, 160), count: 0, failed: 0 };
|
|
24
|
+
entry.count += 1;
|
|
25
|
+
if (m.tool.ok === false)
|
|
26
|
+
entry.failed += 1;
|
|
27
|
+
if (entry.sample === undefined && m.tool.summary)
|
|
28
|
+
entry.sample = fitBytes(m.tool.summary.replace(/\s+/g, " "), 300);
|
|
29
|
+
byName.set(m.tool.name, entry);
|
|
30
|
+
}
|
|
31
|
+
// busiest first; ties keep first-seen order (Map preserves insertion)
|
|
32
|
+
const ranked = [...byName.values()].sort((a, b) => b.count - a.count);
|
|
33
|
+
const tools = ranked.slice(0, MAX_TOOLS);
|
|
34
|
+
const toolsDropped = ranked.length - tools.length;
|
|
35
|
+
const memory = input.memory.slice(0, MAX_MEMORY).map((row) => ({
|
|
36
|
+
path: fitBytes(row.path, 512),
|
|
37
|
+
kind: (row.kind === "edited" ? "updated" : row.kind),
|
|
38
|
+
}));
|
|
39
|
+
const digest = {
|
|
40
|
+
turnId: input.turnId,
|
|
41
|
+
botId: input.botId,
|
|
42
|
+
threadId: input.threadId,
|
|
43
|
+
at: input.at,
|
|
44
|
+
durationMs: input.durationMs,
|
|
45
|
+
tools,
|
|
46
|
+
toolCalls: ranked.reduce((sum, tool) => sum + tool.count, 0),
|
|
47
|
+
...(toolsDropped > 0 ? { toolsDropped } : {}),
|
|
48
|
+
...(input.files ? { files: boundFiles(input.files) } : {}),
|
|
49
|
+
memory,
|
|
50
|
+
...(input.memory.length > memory.length ? { memoryDropped: input.memory.length - memory.length } : {}),
|
|
51
|
+
reply: firstSentence(input.reply),
|
|
52
|
+
...(input.usage ? { usage: input.usage } : {}),
|
|
53
|
+
hookCoverage: input.hookCoverage,
|
|
54
|
+
};
|
|
55
|
+
return digest;
|
|
56
|
+
}
|
|
57
|
+
function boundFiles(files) {
|
|
58
|
+
let budget = MAX_FILES;
|
|
59
|
+
let truncated = files.truncated ?? 0;
|
|
60
|
+
const take = (list) => {
|
|
61
|
+
const kept = list.slice(0, Math.max(0, budget)).map(path => fitBytes(path, 512));
|
|
62
|
+
truncated += list.length - kept.length;
|
|
63
|
+
budget -= kept.length;
|
|
64
|
+
return kept;
|
|
65
|
+
};
|
|
66
|
+
const changed = take(files.changed);
|
|
67
|
+
const added = take(files.added);
|
|
68
|
+
const deleted = take(files.deleted);
|
|
69
|
+
return { changed, added, deleted, ...(truncated > 0 ? { truncated } : {}) };
|
|
70
|
+
}
|
|
71
|
+
function firstSentence(reply) {
|
|
72
|
+
const flat = reply.replace(/\s+/g, " ").trim();
|
|
73
|
+
const end = flat.search(/[.!?](\s|$)/);
|
|
74
|
+
const sentence = end === -1 ? flat : flat.slice(0, end + 1);
|
|
75
|
+
return sentence.slice(0, REPLY_CHARS);
|
|
76
|
+
}
|
|
77
|
+
/** The transcript row's text: one paragraph, FTS-indexed, ≤ RENDER_BYTES. */
|
|
78
|
+
export function renderDigest(d) {
|
|
79
|
+
const parts = ["[digest]"];
|
|
80
|
+
if (d.tools.length) {
|
|
81
|
+
const tools = d.tools.map((t) => `${t.name} ×${t.count}${t.failed ? ` (${t.failed} failed)` : ""}`).join(", ");
|
|
82
|
+
parts.push(`tools: ${tools}${d.toolsDropped ? ` +${d.toolsDropped} more` : ""}${d.hookCoverage === "preview" ? " (from tool previews)" : ""}`);
|
|
83
|
+
}
|
|
84
|
+
else if (d.hookCoverage === "none") {
|
|
85
|
+
parts.push("no tool activity observed in this turn");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
parts.push("no tool calls");
|
|
89
|
+
}
|
|
90
|
+
if (d.files) {
|
|
91
|
+
const files = [];
|
|
92
|
+
if (d.files.changed.length)
|
|
93
|
+
files.push(`changed ${d.files.changed.join(", ")}`);
|
|
94
|
+
if (d.files.added.length)
|
|
95
|
+
files.push(`added ${d.files.added.join(", ")}`);
|
|
96
|
+
if (d.files.deleted.length)
|
|
97
|
+
files.push(`deleted ${d.files.deleted.join(", ")}`);
|
|
98
|
+
if (d.files.truncated)
|
|
99
|
+
files.push(`+${d.files.truncated} more paths`);
|
|
100
|
+
parts.push(files.length ? `files: ${files.join("; ")}` : "files: none changed");
|
|
101
|
+
}
|
|
102
|
+
if (d.memory.length)
|
|
103
|
+
parts.push(`memory: ${d.memory.map((m) => `${m.kind} ${m.path}`).join(", ")}`);
|
|
104
|
+
if (d.memoryDropped)
|
|
105
|
+
parts.push(`+${d.memoryDropped} more memory changes`);
|
|
106
|
+
if (d.reply)
|
|
107
|
+
parts.push(`reply: ${d.reply}`);
|
|
108
|
+
return fitBytes(parts.join(" · "), RENDER_BYTES);
|
|
109
|
+
}
|
|
110
|
+
/** One bracketed line for a context rebuild — never multi-line, so it sits
|
|
111
|
+
* in a transcript replay or a room window like any other speaker line. */
|
|
112
|
+
export function digestPromptLine(d, botName) {
|
|
113
|
+
const body = renderDigest(d).replace(/^\[digest\]\s*/, "").replace(/\s+/g, " ").trim();
|
|
114
|
+
return `[What ${botName} did in an earlier turn: ${body}]`;
|
|
115
|
+
}
|
|
116
|
+
/** Trim to a byte budget on a character boundary, marking the cut. */
|
|
117
|
+
function fitBytes(text, max) {
|
|
118
|
+
if (Buffer.byteLength(text, "utf8") <= max)
|
|
119
|
+
return text;
|
|
120
|
+
const marker = " […]";
|
|
121
|
+
let out = "";
|
|
122
|
+
let bytes = Buffer.byteLength(marker);
|
|
123
|
+
for (const character of text) {
|
|
124
|
+
bytes += Buffer.byteLength(character);
|
|
125
|
+
if (bytes > max)
|
|
126
|
+
break;
|
|
127
|
+
out += character;
|
|
128
|
+
}
|
|
129
|
+
return out + marker;
|
|
130
|
+
}
|