openmausbot 0.1.85 → 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-CTNNoqSJ.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/container-mcp.js +19 -10
- package/dist-server/drivers/agents-proxy.js +16159 -1596
- package/dist-server/enterprise/LICENSE +32 -0
- package/dist-server/enterprise/server/index.js +5 -5
- package/dist-server/index.js +4779 -1667
- package/dist-server/mcp-server.js +3 -1
- package/dist-server/openmausbot.js +4679 -4266
- package/dist-server/pair-cli.js +4679 -4266
- package/dist-server/server/admin-activity.js +410 -0
- package/dist-server/server/bot-visibility.js +462 -0
- package/dist-server/server/box.js +2 -1
- package/dist-server/server/browser-runtime.js +1 -1
- package/dist-server/server/channel-queue.js +2 -0
- package/dist-server/server/cli-prompts.js +24 -2
- package/dist-server/server/cli.js +79 -10
- package/dist-server/server/composio.js +49 -11
- package/dist-server/server/config.js +79 -13
- 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/drivers/acp/core.js +110 -22
- 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 -1722
- package/dist-server/server/drivers/chat-mcp-tools.js +17 -2
- package/dist-server/server/drivers/claude.js +21 -6
- package/dist-server/server/drivers/codex-mcp-names.js +44 -0
- package/dist-server/server/drivers/codex.js +26 -4
- package/dist-server/server/drivers/grok.js +2 -1
- package/dist-server/server/drivers/openai-compat.js +15 -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/hosted-models.js +151 -0
- package/dist-server/server/hosted-slack.js +11 -0
- package/dist-server/server/index.js +1718 -348
- package/dist-server/server/managed-desktop.js +79 -14
- package/dist-server/server/managed-policy.js +153 -0
- 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/notify.js +10 -1
- package/dist-server/server/peer-roster.js +13 -2
- 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 +25 -11
- package/dist-server/server/routes/hosted-slack.js +20 -0
- package/dist-server/server/routes/table.js +14 -0
- package/dist-server/server/routines.js +22 -0
- package/dist-server/server/spend.js +111 -16
- package/dist-server/server/steer-queue.js +6 -1
- package/dist-server/server/store.js +292 -21
- package/dist-server/server/team-backup.js +10 -1
- package/dist-server/server/thread-starters.js +53 -0
- package/dist-server/server/usage-ledger.js +31 -8
- package/dist-server/server/workspace-backup-policy.js +9 -2
- package/dist-server/server/workspace-backup.js +7 -4
- package/dist-server/shared/mention-boundary.js +15 -0
- package/dist-server/shared/routines.js +7 -1
- package/dist-server/shared/team-backup.js +2 -0
- package/dist-server/src/lib/composer-attachments.js +16 -0
- package/dist-server/vps-container-mcp.js +19 -10
- 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,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"
|
|
@@ -29,7 +29,7 @@ import { homedir } from "node:os";
|
|
|
29
29
|
import { lstat, mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
30
30
|
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
31
31
|
import { createHash } from "node:crypto";
|
|
32
|
-
import { PROVIDER_CREDENTIAL_ENV, WORKSPACE_CREDENTIAL_ENV } from "../../config.js";
|
|
32
|
+
import { PROVIDER_CREDENTIAL_ENV, stripControlPlaneEnv, WORKSPACE_CREDENTIAL_ENV } from "../../config.js";
|
|
33
33
|
import { decodeInjectId } from "../local-inject.js";
|
|
34
34
|
import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../../procs.js";
|
|
35
35
|
/**
|
|
@@ -45,6 +45,33 @@ import { supportsApprovalMode } from "../../../shared/approval-mode.js";
|
|
|
45
45
|
import { appendNative } from "../native.js";
|
|
46
46
|
import { commandSummary, toolDetailPreview } from "../../tool-summary.js";
|
|
47
47
|
import { extractMcpImages } from "../../mcp-tool-images.js";
|
|
48
|
+
import { redactSecretsInText } from "../../redact.js";
|
|
49
|
+
import { recoveryPromptFor } from "../../resume-recovery.js";
|
|
50
|
+
/** ACP vendors put the actionable cause in error.data while keeping the
|
|
51
|
+
* JSON-RPC message generic. Only surface known text fields, never a response
|
|
52
|
+
* body/config dump, and redact before bounding the displayed diagnostic. */
|
|
53
|
+
function acpRpcError(value, method) {
|
|
54
|
+
const message = typeof value?.message === "string" ? value.message : "ACP request failed";
|
|
55
|
+
const data = value?.data;
|
|
56
|
+
const detail = typeof data === "string" ? data
|
|
57
|
+
: typeof data?.details === "string" ? data.details
|
|
58
|
+
: typeof data?.message === "string" ? data.message
|
|
59
|
+
: typeof data?.error?.message === "string" ? data.error.message
|
|
60
|
+
: "";
|
|
61
|
+
const context = [
|
|
62
|
+
method,
|
|
63
|
+
typeof data?.service === "string" ? `service: ${data.service}` : "",
|
|
64
|
+
typeof data?.errorName === "string" ? data.errorName : "",
|
|
65
|
+
].filter(Boolean).join(", ");
|
|
66
|
+
const diagnostic = `${detail && detail !== message ? `${message}: ${detail}` : message} (${context})`;
|
|
67
|
+
const error = new Error(redactSecretsInText(diagnostic).slice(0, 1500));
|
|
68
|
+
return Object.assign(error, { code: value?.code, data,
|
|
69
|
+
// -32603 is JSON-RPC's internal error. Invalid params, unsupported
|
|
70
|
+
// methods and auth refusals are user/configuration issues, not evidence
|
|
71
|
+
// of a broken process. Preserve their session instead of retrying them.
|
|
72
|
+
acpSessionFailure: value?.code === -32603,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
48
75
|
const envOr = (key, fallback) => Number(process.env[key] ?? fallback);
|
|
49
76
|
const INIT_TIMEOUT = envOr("OPENMAUS_ACP_INIT_TIMEOUT_MS", 300_000);
|
|
50
77
|
const SESSION_CONFIG_TIMEOUT = envOr("OPENMAUS_ACP_SESSION_CONFIG_TIMEOUT_MS", 300_000); // configureSession's per-request default
|
|
@@ -171,6 +198,17 @@ function sessionOperationKey(toolCall) {
|
|
|
171
198
|
locations: toolCall?.locations,
|
|
172
199
|
}));
|
|
173
200
|
}
|
|
201
|
+
/** The banner an ACP CLI prints for `--version`. Hermes writes its whole banner
|
|
202
|
+
* to stderr with an empty stdout, so an stdout-only read reports a perfectly
|
|
203
|
+
* good install as "CLI not found". Prefer stdout; fall back to the first
|
|
204
|
+
* stderr line; null when both are empty. */
|
|
205
|
+
export function versionFromProbe(stdout, stderr) {
|
|
206
|
+
const out = (stdout ?? "").trim();
|
|
207
|
+
if (out)
|
|
208
|
+
return out;
|
|
209
|
+
const err = (stderr ?? "").trim().split(/\r\n|\n|\r/, 1)[0]?.trim() ?? "";
|
|
210
|
+
return err || null;
|
|
211
|
+
}
|
|
174
212
|
export function createAcpDriver(support) {
|
|
175
213
|
const DRIVER_KIND = support.driverKind;
|
|
176
214
|
const SOURCE = support.nativeSource;
|
|
@@ -205,6 +243,8 @@ export function createAcpDriver(support) {
|
|
|
205
243
|
if (!allowedCredentials.has(key))
|
|
206
244
|
delete env[key];
|
|
207
245
|
}
|
|
246
|
+
// The operator's own secrets are outside any driver's allowlist.
|
|
247
|
+
stripControlPlaneEnv(env);
|
|
208
248
|
support.transformEnv?.(env, activeConfig, instanceId);
|
|
209
249
|
return env;
|
|
210
250
|
};
|
|
@@ -385,7 +425,25 @@ export function createAcpDriver(support) {
|
|
|
385
425
|
session.current = null;
|
|
386
426
|
active.delete(threadId);
|
|
387
427
|
current.flushAssistantText();
|
|
388
|
-
|
|
428
|
+
// `end_turn` with nothing to show for it — no reply, no image, no
|
|
429
|
+
// tool result — is a lost turn, not a success. An engine can report
|
|
430
|
+
// exactly that (a provider may cut a reasoning-only stream and
|
|
431
|
+
// still answer end_turn), and ok:true would end the thread quietly
|
|
432
|
+
// while the person's message went unanswered. Keep the completion,
|
|
433
|
+
// but report it as a failure so terminal chips, incidents and
|
|
434
|
+
// follow-ups see what happened.
|
|
435
|
+
let finalOk = ok;
|
|
436
|
+
let finalStopReason = stopReason;
|
|
437
|
+
if (finalOk && finalStopReason === null && !current.state.producedItem) {
|
|
438
|
+
finalOk = false;
|
|
439
|
+
finalStopReason = "empty_turn";
|
|
440
|
+
emit({
|
|
441
|
+
...base(threadId, current.turnId),
|
|
442
|
+
type: "runtime.error",
|
|
443
|
+
message: `${DRIVER_KIND} ended the turn with no reply, image, or tool result`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
emit({ ...base(threadId, current.turnId), type: "turn.completed", ok: finalOk, stopReason: finalStopReason, cost: null });
|
|
389
447
|
if (session.child.exitCode === null && !session.closing && !session.dead) {
|
|
390
448
|
armIdle(threadId);
|
|
391
449
|
}
|
|
@@ -426,7 +484,7 @@ export function createAcpDriver(support) {
|
|
|
426
484
|
if (timeoutMs) {
|
|
427
485
|
timer = setTimeout(() => {
|
|
428
486
|
rpcPending.delete(id);
|
|
429
|
-
reject(new Error(`${method} timed out`));
|
|
487
|
+
reject(Object.assign(new Error(`${method} timed out`), { acpSessionFailure: true }));
|
|
430
488
|
}, timeoutMs);
|
|
431
489
|
timer.unref?.();
|
|
432
490
|
}
|
|
@@ -455,6 +513,7 @@ export function createAcpDriver(support) {
|
|
|
455
513
|
};
|
|
456
514
|
armIdle();
|
|
457
515
|
rpcPending.set(id, {
|
|
516
|
+
method,
|
|
458
517
|
// Consume configuration in wire order: an update following this
|
|
459
518
|
// response may arrive before the awaiting continuation resumes.
|
|
460
519
|
resolve: (result) => { receive?.(result); resolve(result); },
|
|
@@ -686,6 +745,7 @@ export function createAcpDriver(support) {
|
|
|
686
745
|
const delta = content?.text;
|
|
687
746
|
if (content?.type === "image" && typeof content.data === "string" && content.data) {
|
|
688
747
|
current.flushAssistantText();
|
|
748
|
+
current.state.producedItem = true;
|
|
689
749
|
emit({
|
|
690
750
|
...base(threadId, current.turnId),
|
|
691
751
|
type: "item.completed",
|
|
@@ -722,6 +782,7 @@ export function createAcpDriver(support) {
|
|
|
722
782
|
}
|
|
723
783
|
case "tool_call_update": {
|
|
724
784
|
if (u.status === "completed" || u.status === "failed") {
|
|
785
|
+
current.state.producedItem = true;
|
|
725
786
|
emit({
|
|
726
787
|
...base(threadId, current.turnId),
|
|
727
788
|
type: "item.completed",
|
|
@@ -788,9 +849,7 @@ export function createAcpDriver(support) {
|
|
|
788
849
|
if (pend.idleTimer)
|
|
789
850
|
clearTimeout(pend.idleTimer);
|
|
790
851
|
if (msg.error) {
|
|
791
|
-
|
|
792
|
-
Object.assign(error, { code: msg.error.code, data: msg.error.data });
|
|
793
|
-
pend.reject(error);
|
|
852
|
+
pend.reject(acpRpcError(msg.error, pend.method));
|
|
794
853
|
}
|
|
795
854
|
else {
|
|
796
855
|
pend.resolve(msg.result);
|
|
@@ -923,6 +982,10 @@ export function createAcpDriver(support) {
|
|
|
923
982
|
const envFingerprint = createHash("sha256").update(JSON.stringify(spawnEnv)).digest("hex").slice(0, 16);
|
|
924
983
|
const contractKey = JSON.stringify([launch.command, launch.args ?? [], spawnArgs, cwd, turnConfig.fullAuto === true, envFingerprint]);
|
|
925
984
|
const sessionKey = JSON.stringify(mcpServers);
|
|
985
|
+
if (turn.sessionReset) {
|
|
986
|
+
closeSession(threadId, "reset");
|
|
987
|
+
sessionAllows.delete(threadId);
|
|
988
|
+
}
|
|
926
989
|
const pooled = sessions.get(threadId);
|
|
927
990
|
let session;
|
|
928
991
|
if (pooled && !pooled.dead && !pooled.closing && pooled.contractKey === contractKey) {
|
|
@@ -948,7 +1011,7 @@ export function createAcpDriver(support) {
|
|
|
948
1011
|
// respawns the child, every wire call must reach the live record, so
|
|
949
1012
|
// nothing captures the connection off it.
|
|
950
1013
|
const request = (method, params, timeoutMs, receive, idleMs, idleMessage) => session.acp.request(method, params, timeoutMs, receive, idleMs, idleMessage);
|
|
951
|
-
const state = { settled: false, promptSent: false, text: "" };
|
|
1014
|
+
const state = { settled: false, promptSent: false, text: "", producedItem: false };
|
|
952
1015
|
const asks = new Map();
|
|
953
1016
|
const modelOf = (result) => {
|
|
954
1017
|
const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find((entry) => entry?.id === (support.selectModel?.configId ?? "model"));
|
|
@@ -987,6 +1050,7 @@ export function createAcpDriver(support) {
|
|
|
987
1050
|
state.text = "";
|
|
988
1051
|
if (!text.trim())
|
|
989
1052
|
return;
|
|
1053
|
+
state.producedItem = true;
|
|
990
1054
|
emit({ ...base(threadId, turnId), type: "item.completed", itemType: "assistant_text", text });
|
|
991
1055
|
};
|
|
992
1056
|
const current = {
|
|
@@ -1073,8 +1137,10 @@ export function createAcpDriver(support) {
|
|
|
1073
1137
|
};
|
|
1074
1138
|
let runtimeAcceptsImages = await handshake();
|
|
1075
1139
|
let init = session.initResult;
|
|
1076
|
-
const cursor = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
|
|
1140
|
+
const cursor = !turn.sessionReset && typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
|
|
1077
1141
|
let sessionResult = null;
|
|
1142
|
+
let promptTurn = turn;
|
|
1143
|
+
let rebuiltFromReplay = false;
|
|
1078
1144
|
for (;;) {
|
|
1079
1145
|
const liveSessionId = session.sessionId;
|
|
1080
1146
|
if (liveSessionId !== null && session.sessionKey === sessionKey && (cursor === null || cursor === liveSessionId)) {
|
|
@@ -1100,7 +1166,17 @@ export function createAcpDriver(support) {
|
|
|
1100
1166
|
}
|
|
1101
1167
|
});
|
|
1102
1168
|
}
|
|
1103
|
-
catch {
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
const classification = support.classifyError?.(error);
|
|
1171
|
+
// OpenCode encodes ACPSessionNotFoundError as invalidParams
|
|
1172
|
+
// with just the rejected sessionId. Other invalidParams
|
|
1173
|
+
// responses (model/config errors) must not erase history.
|
|
1174
|
+
const data = error?.data;
|
|
1175
|
+
const missingSession = data && typeof data === "object" && !Array.isArray(data)
|
|
1176
|
+
&& data.sessionId === cursor && Object.keys(data).length === 1;
|
|
1177
|
+
if (classification === "invalid_credentials" || classification === "inactive_subscription"
|
|
1178
|
+
|| (error?.code === -32602 && !missingSession))
|
|
1179
|
+
throw error;
|
|
1104
1180
|
/* session gone, load unsupported, or too slow — the
|
|
1105
1181
|
* fallbacks below choose between one fresh process and a
|
|
1106
1182
|
* genuinely new session */
|
|
@@ -1127,8 +1203,16 @@ export function createAcpDriver(support) {
|
|
|
1127
1203
|
}
|
|
1128
1204
|
// a genuinely fresh native session forgets what the previous
|
|
1129
1205
|
// one allowed
|
|
1130
|
-
|
|
1131
|
-
|
|
1206
|
+
sessionAllows.delete(threadId);
|
|
1207
|
+
if (cursor) {
|
|
1208
|
+
const recovery = recoveryPromptFor({
|
|
1209
|
+
recoveryText: turn.recoveryText,
|
|
1210
|
+
currentText: turn.text,
|
|
1211
|
+
failure: "before-accept",
|
|
1212
|
+
});
|
|
1213
|
+
promptTurn = { ...turn, text: recovery.text };
|
|
1214
|
+
rebuiltFromReplay = recovery.replayed;
|
|
1215
|
+
}
|
|
1132
1216
|
sessionResult = await request("session/new", { cwd, mcpServers: sessionServers }, NEW_SESSION_TIMEOUT, (result) => {
|
|
1133
1217
|
session.sessionId = typeof result?.sessionId === "string" ? result.sessionId : null;
|
|
1134
1218
|
session.sessionKey = sessionKey;
|
|
@@ -1151,6 +1235,7 @@ export function createAcpDriver(support) {
|
|
|
1151
1235
|
type: "session.started",
|
|
1152
1236
|
sessionId,
|
|
1153
1237
|
model: selectedModel ?? init?._meta?.modelState?.currentModelId ?? cliTurn.model ?? null,
|
|
1238
|
+
...(rebuiltFromReplay ? { rebuilt: true } : {}),
|
|
1154
1239
|
});
|
|
1155
1240
|
};
|
|
1156
1241
|
try {
|
|
@@ -1177,7 +1262,7 @@ export function createAcpDriver(support) {
|
|
|
1177
1262
|
? sessionResult.models.availableModels
|
|
1178
1263
|
: [],
|
|
1179
1264
|
});
|
|
1180
|
-
// initialize's currentModelId is the CLI default
|
|
1265
|
+
// initialize's currentModelId is the CLI default,
|
|
1181
1266
|
// not the model this turn asked for. After a successful pin,
|
|
1182
1267
|
// report the slug we set so the UI does not claim otherwise.
|
|
1183
1268
|
if (!selectedModel && cliTurn.model)
|
|
@@ -1199,10 +1284,10 @@ export function createAcpDriver(support) {
|
|
|
1199
1284
|
}
|
|
1200
1285
|
emitSessionStarted();
|
|
1201
1286
|
const text = support.buildPromptText
|
|
1202
|
-
? support.buildPromptText(
|
|
1203
|
-
:
|
|
1204
|
-
? `${
|
|
1205
|
-
:
|
|
1287
|
+
? support.buildPromptText(promptTurn)
|
|
1288
|
+
: promptTurn.system
|
|
1289
|
+
? `${promptTurn.system}\n\n${promptTurn.text}`
|
|
1290
|
+
: promptTurn.text;
|
|
1206
1291
|
const imageBlocks = support.images === true && runtimeAcceptsImages
|
|
1207
1292
|
? await readAcpImageBlocks(turn.images ?? [])
|
|
1208
1293
|
: [];
|
|
@@ -1261,11 +1346,14 @@ export function createAcpDriver(support) {
|
|
|
1261
1346
|
message,
|
|
1262
1347
|
...(needsAuth ? { setup: true } : {}),
|
|
1263
1348
|
});
|
|
1264
|
-
//
|
|
1265
|
-
//
|
|
1266
|
-
//
|
|
1267
|
-
|
|
1268
|
-
|
|
1349
|
+
// Internal RPC failures can leave a live child poisoned just
|
|
1350
|
+
// like a silent prompt. Evict before completion listeners can
|
|
1351
|
+
// start the next turn. Never replay this accepted prompt: it
|
|
1352
|
+
// may already have executed tools. The next explicit turn can
|
|
1353
|
+
// resume the recorded session on a fresh process.
|
|
1354
|
+
if (!needsAuth && (e?.acpPromptStall === true || e?.acpSessionFailure === true
|
|
1355
|
+
|| code === "upstream_outage") && session.child.exitCode === null && !session.closing) {
|
|
1356
|
+
closeSession(threadId, e?.acpPromptStall === true ? "prompt-stall" : "rpc-failure");
|
|
1269
1357
|
}
|
|
1270
1358
|
settle(threadId, session, false, needsAuth ? "auth_required" : "rpc_error");
|
|
1271
1359
|
}
|
|
@@ -1278,7 +1366,7 @@ export function createAcpDriver(support) {
|
|
|
1278
1366
|
if (support.snapshot)
|
|
1279
1367
|
return support.snapshot(env, config, instanceId);
|
|
1280
1368
|
const version = await new Promise((resolve) => {
|
|
1281
|
-
execCli(config.cli, ["--version"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout
|
|
1369
|
+
execCli(config.cli, ["--version"], { timeout: 8000, env }, (err, stdout, stderr) => resolve(err ? null : versionFromProbe(stdout, stderr)));
|
|
1282
1370
|
});
|
|
1283
1371
|
if (!version)
|
|
1284
1372
|
return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
|