openmausbot 0.1.86 → 0.1.88
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 +4 -3
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-CHnODCr2.js +583 -0
- package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +1032 -165
- package/dist-server/drivers/agents-proxy.js +338 -29
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +40590 -31811
- package/dist-server/local-computer-proxy.js +5 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/mcp-gate.js +2 -0
- package/dist-server/mcp-server.js +5 -3
- package/dist-server/message-search.worker.js +52 -0
- package/dist-server/openmausbot.js +4457 -1399
- package/dist-server/pair-cli.js +4457 -1399
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +98 -1
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +57 -55
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +37 -1
- package/dist-server/server/cli-api-setup.js +1 -0
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +84 -12
- package/dist-server/server/connector-advertisement.js +165 -0
- package/dist-server/server/connector-proxy.js +14 -2
- package/dist-server/server/connector-verdict.js +168 -0
- package/dist-server/server/container-computer.js +61 -1
- package/dist-server/server/contracts.js +22 -0
- package/dist-server/server/default-model-selection.js +9 -0
- package/dist-server/server/delegations.js +85 -34
- package/dist-server/server/drivers/acp/core.js +80 -20
- package/dist-server/server/drivers/acp/cursor.js +3 -7
- package/dist-server/server/drivers/acp/droid.js +4 -7
- package/dist-server/server/drivers/acp/gemini.js +4 -4
- package/dist-server/server/drivers/acp/grok.js +4 -9
- package/dist-server/server/drivers/acp/hermes.js +3 -5
- package/dist-server/server/drivers/acp/opencode-go.js +2 -5
- package/dist-server/server/drivers/acp/qwen.js +9 -15
- package/dist-server/server/drivers/agents-call.js +179 -13
- package/dist-server/server/drivers/agents-catalog.js +134 -10
- package/dist-server/server/drivers/boxagent.js +160 -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 +38 -8
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +38 -19
- package/dist-server/server/drivers/codex-catalog.js +14 -6
- package/dist-server/server/drivers/codex-identity.js +2 -1
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +102 -55
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +180 -33
- package/dist-server/server/drivers/openai-compat.js +1 -0
- 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 +306 -28
- package/dist-server/server/drivers/prompt-split.js +110 -0
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -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 +2 -2
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +2396 -505
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +1 -1
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/message-db.js +22 -42
- package/dist-server/server/message-file.js +9 -1
- package/dist-server/server/message-search-query.js +43 -0
- package/dist-server/server/message-search-worker.js +80 -0
- package/dist-server/server/message-search.worker.js +21 -0
- package/dist-server/server/model-requests.js +248 -0
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +1065 -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-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +67 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/profile-requests.js +66 -39
- package/dist-server/server/profile-revision.js +9 -1
- package/dist-server/server/profile-versions.js +6 -3
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/room-handoffs.js +19 -3
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routine-requests.js +35 -10
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +35 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +291 -17
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +38 -6
- package/dist-server/server/team-backup.js +6 -2
- 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 +139 -27
- package/dist-server/server/thread-events.js +20 -2
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- 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 +19 -2
- 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 +12 -0
- package/dist-server/server/workspace-backup.js +42 -5
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +207 -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 +6 -2
- package/dist-server/shared/line-diff.js +23 -0
- package/dist-server/shared/model-request.js +1 -0
- package/dist-server/shared/options-card.js +51 -0
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +1132 -171
- package/dist-server/workspace-backup.worker.js +19474 -0
- package/enterprise/README.md +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-BXFkgus9.js +0 -1
- package/dist/assets/index-BGgqlWPJ.css +0 -1
- package/dist/assets/index-BtHo8ZKl.js +0 -577
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
|
@@ -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
|
}
|
|
@@ -656,18 +706,19 @@ function targetCanTakeTurn(bus, target, item) {
|
|
|
656
706
|
? bus.canAdmitDirectTurn(target.id, target.threadId)
|
|
657
707
|
: !target.busy;
|
|
658
708
|
}
|
|
659
|
-
/** A busy target holds the handoff. Neither counts busy periods — the
|
|
660
|
-
*
|
|
661
|
-
* the same `targetCanTakeTurn` test, passed in as
|
|
662
|
-
* caller already computed it so the two checks can't
|
|
663
|
-
* chip per handoff, worded for what the target is
|
|
664
|
-
* null when the target can take the turn now. */
|
|
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. */
|
|
665
715
|
function holdWhileTargetBusy(bus, target, sourceThreadId, item, canTakeTurn = targetCanTakeTurn(bus, target, item)) {
|
|
666
716
|
if (canTakeTurn)
|
|
667
717
|
return null;
|
|
668
718
|
if (item.waitingOnBusy)
|
|
669
719
|
return "requeued";
|
|
670
720
|
item.waitingOnBusy = true;
|
|
721
|
+
item.busySince = Date.now();
|
|
671
722
|
if (!item.waitAnnounced) {
|
|
672
723
|
item.waitAnnounced = true;
|
|
673
724
|
bus.store.appendMessage(sourceThreadId, {
|
|
@@ -31,6 +31,7 @@ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
|
31
31
|
import { createHash } from "node:crypto";
|
|
32
32
|
import { PROVIDER_CREDENTIAL_ENV, stripControlPlaneEnv, WORKSPACE_CREDENTIAL_ENV } from "../../config.js";
|
|
33
33
|
import { decodeInjectId } from "../local-inject.js";
|
|
34
|
+
import { promptHalves, readPromptSplitReceipt, splitSessionPrompt, writePromptSplitReceipt } from "../prompt-split.js";
|
|
34
35
|
import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../../procs.js";
|
|
35
36
|
/**
|
|
36
37
|
* A `host::model` pick talks to a loopback server with its own key.
|
|
@@ -42,11 +43,14 @@ export function skipSubscriptionAuthForLocalInject(model) {
|
|
|
42
43
|
import { newEventId, newId } from "../../contracts.js";
|
|
43
44
|
import { augmentedPath } from "../../env-path.js";
|
|
44
45
|
import { supportsApprovalMode } from "../../../shared/approval-mode.js";
|
|
46
|
+
import { parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../../shared/ask-question.js";
|
|
45
47
|
import { appendNative } from "../native.js";
|
|
48
|
+
import { acpPermissionCommand, permissionLaunchCwd } from "../permission-command.js";
|
|
46
49
|
import { commandSummary, toolDetailPreview } from "../../tool-summary.js";
|
|
47
50
|
import { extractMcpImages } from "../../mcp-tool-images.js";
|
|
48
51
|
import { redactSecretsInText } from "../../redact.js";
|
|
49
52
|
import { recoveryPromptFor } from "../../resume-recovery.js";
|
|
53
|
+
import { sessionIdlePolicy } from "../session-idle.js";
|
|
50
54
|
/** ACP vendors put the actionable cause in error.data while keeping the
|
|
51
55
|
* JSON-RPC message generic. Only surface known text fields, never a response
|
|
52
56
|
* body/config dump, and redact before bounding the displayed diagnostic. */
|
|
@@ -77,6 +81,9 @@ const INIT_TIMEOUT = envOr("OPENMAUS_ACP_INIT_TIMEOUT_MS", 300_000);
|
|
|
77
81
|
const SESSION_CONFIG_TIMEOUT = envOr("OPENMAUS_ACP_SESSION_CONFIG_TIMEOUT_MS", 300_000); // configureSession's per-request default
|
|
78
82
|
const NEW_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_NEW_SESSION_TIMEOUT_MS", 300_000);
|
|
79
83
|
const LOAD_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_LOAD_SESSION_TIMEOUT_MS", 120_000); // history replay on a long thread is slow
|
|
84
|
+
/** ACP agents may compact their own history without telling the client;
|
|
85
|
+
* re-send the full prompt after this many bare turns as a backstop. */
|
|
86
|
+
const ACP_PROMPT_RE_ANCHOR_TURNS = 8;
|
|
80
87
|
// Read lazily (not at import) so a test can shorten the window. Unlike the
|
|
81
88
|
// setup calls above, session/prompt legitimately streams for minutes, so a
|
|
82
89
|
// wall-clock deadline would false-positive: this guard only trips when the
|
|
@@ -209,6 +216,24 @@ export function versionFromProbe(stdout, stderr) {
|
|
|
209
216
|
const err = (stderr ?? "").trim().split(/\r\n|\n|\r/, 1)[0]?.trim() ?? "";
|
|
210
217
|
return err || null;
|
|
211
218
|
}
|
|
219
|
+
/** The first dotted numeric triple in a version banner, or null when there is
|
|
220
|
+
* none. A wrapper that prints its own banner first still parses: the triple
|
|
221
|
+
* is the version wherever it appears. */
|
|
222
|
+
export function parseVersionTriple(value) {
|
|
223
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(value);
|
|
224
|
+
if (!match)
|
|
225
|
+
return null;
|
|
226
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
227
|
+
}
|
|
228
|
+
/** Whether `installed` compares at or above `floor`, componentwise. Every
|
|
229
|
+
* version floor in the tree is a dotted triple. */
|
|
230
|
+
export function versionAtLeast(installed, floor) {
|
|
231
|
+
for (let i = 0; i < 3; i += 1) {
|
|
232
|
+
if (installed[i] !== floor[i])
|
|
233
|
+
return installed[i] > floor[i];
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
212
237
|
export function createAcpDriver(support) {
|
|
213
238
|
const DRIVER_KIND = support.driverKind;
|
|
214
239
|
const SOURCE = support.nativeSource;
|
|
@@ -271,11 +296,7 @@ export function createAcpDriver(support) {
|
|
|
271
296
|
// contract prompts the live session instead of paying the handshake
|
|
272
297
|
// again. An idle session closes after SESSION_IDLE_MS of quiet.
|
|
273
298
|
const sessions = new Map();
|
|
274
|
-
const
|
|
275
|
-
const sessionIdleMinimum = Number.isFinite(configuredIdleMinimum) && configuredIdleMinimum > 0
|
|
276
|
-
? configuredIdleMinimum
|
|
277
|
-
: 10_000;
|
|
278
|
-
const SESSION_IDLE_MS = Math.max(sessionIdleMinimum, Number(process.env.OMB_ACP_SESSION_IDLE_MS) || 10 * 60_000);
|
|
299
|
+
const { idleMs: SESSION_IDLE_MS } = sessionIdlePolicy("ACP");
|
|
279
300
|
const closeSession = (threadId, why) => {
|
|
280
301
|
const session = sessions.get(threadId);
|
|
281
302
|
if (!session || session.closing)
|
|
@@ -458,6 +479,7 @@ export function createAcpDriver(support) {
|
|
|
458
479
|
* session.current, so a request that lands between turns is answered
|
|
459
480
|
* (never brokered) instead of left hanging. */
|
|
460
481
|
const openSession = (threadId, launch, argv, env, cwd, contractKey) => {
|
|
482
|
+
const commandCwd = permissionLaunchCwd(cwd);
|
|
461
483
|
const child = spawnCli(launch.command, argv, {
|
|
462
484
|
cwd,
|
|
463
485
|
env,
|
|
@@ -660,16 +682,34 @@ export function createAcpDriver(support) {
|
|
|
660
682
|
}
|
|
661
683
|
}
|
|
662
684
|
const tool = kind === "execute" ? "shell" : kind === "edit" ? "edit" : kind || "tool";
|
|
685
|
+
const isShellCommand = !isQuestion && kind === "execute" && !/^mcp(?:__|[.:])/i.test(String(toolCall.title ?? ""));
|
|
663
686
|
const summary = String(toolCall.rawInput?.command ?? toolCall.title ?? tool).slice(0, 200);
|
|
687
|
+
// One structured question beside the flat choices: the richer card
|
|
688
|
+
// renders from it while older clients keep answering through
|
|
689
|
+
// `choices`. Built once here so the emit and the answer path can
|
|
690
|
+
// never disagree. parseAskQuestions enforces the shared caps.
|
|
691
|
+
const questionChoices = isQuestion
|
|
692
|
+
? options.flatMap((option) => typeof option.name === "string" && option.name.trim() ? [option.name.trim()] : [])
|
|
693
|
+
: [];
|
|
694
|
+
const askQuestions = isQuestion && questionChoices.length
|
|
695
|
+
? parseAskQuestions({ questions: [{ question: summary, options: questionChoices }] }) ?? undefined
|
|
696
|
+
: undefined;
|
|
664
697
|
const requestId = newId();
|
|
665
698
|
const finish = (behavior, source = "user", message, always) => {
|
|
666
699
|
if (!current.asks.delete(requestId))
|
|
667
|
-
return;
|
|
700
|
+
return "unavailable";
|
|
668
701
|
clearTimeout(timer);
|
|
669
702
|
const want = behavior === "allow" ? "allow" : "reject";
|
|
670
703
|
const forSession = want === "allow" && always === true && !isQuestion && !current.controlsHost;
|
|
704
|
+
// A structured card replies in the Q:/A: block format; recover the
|
|
705
|
+
// picked label from it so exact-match keeps working. Flat clients
|
|
706
|
+
// send the bare label, which the single-question fallback inside
|
|
707
|
+
// questionAnswersByQuestion already returns unchanged.
|
|
708
|
+
const picked = askQuestions
|
|
709
|
+
? questionAnswersByQuestion(message ?? "", askQuestions)[askQuestions[0].question] ?? message
|
|
710
|
+
: message;
|
|
671
711
|
const named = isQuestion && behavior === "answer"
|
|
672
|
-
? options.filter((option) => option.optionId ===
|
|
712
|
+
? options.filter((option) => option.optionId === picked || parseChoices([option.name], 1)?.[0] === picked)
|
|
673
713
|
: [];
|
|
674
714
|
const optionId = behavior === "cancel"
|
|
675
715
|
? null
|
|
@@ -700,6 +740,7 @@ export function createAcpDriver(support) {
|
|
|
700
740
|
source: optionId ? source : "system",
|
|
701
741
|
approvalScope: current.controlsHost ? "local-computer" : undefined,
|
|
702
742
|
});
|
|
743
|
+
return !optionId ? "rejected" : isQuestion ? "answered" : behavior === "allow" ? "allowed-once" : "rejected";
|
|
703
744
|
};
|
|
704
745
|
const timer = setTimeout(() => {
|
|
705
746
|
emit({ ...base(threadId, current.turnId), type: "runtime.error", message: DENY_TIMEOUT_NOTE });
|
|
@@ -714,9 +755,10 @@ export function createAcpDriver(support) {
|
|
|
714
755
|
requestType: isQuestion ? "question" : "permission",
|
|
715
756
|
tool,
|
|
716
757
|
summary,
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
758
|
+
command: isShellCommand ? acpPermissionCommand(toolCall.rawInput, commandCwd) : undefined,
|
|
759
|
+
requiresExplicitApproval: isShellCommand && (toolCall.rawInput?.dangerouslyDisableSandbox === true || toolCall.rawInput?.sandbox_permissions === "require_escalated") || undefined,
|
|
760
|
+
choices: askQuestions?.[0]?.options.map(option => option.label) ?? (isQuestion ? questionChoices : undefined),
|
|
761
|
+
...(askQuestions ? { questions: askQuestions } : {}),
|
|
720
762
|
approvalScope: current.controlsHost ? "local-computer" : undefined,
|
|
721
763
|
// the driver can honor a session-wide allow either way
|
|
722
764
|
allowSession: !isQuestion && !current.controlsHost ? true : undefined,
|
|
@@ -1283,11 +1325,28 @@ export function createAcpDriver(support) {
|
|
|
1283
1325
|
throw error;
|
|
1284
1326
|
}
|
|
1285
1327
|
emitSessionStarted();
|
|
1328
|
+
// The stable/volatile split: the full prompt rides only the turn
|
|
1329
|
+
// that establishes - or re-instructs, after a soul edit - this
|
|
1330
|
+
// native session. Later turns go through bare unless the volatile
|
|
1331
|
+
// half changed, so a memory edit neither appends a second copy of
|
|
1332
|
+
// the prompt to the agent's session history nor re-prices the
|
|
1333
|
+
// prefix its provider cached. Receipts are durable because the
|
|
1334
|
+
// native session outlives this process; an un-split turn (a direct
|
|
1335
|
+
// adapter call) keeps the legacy full-prompt shape.
|
|
1336
|
+
const halves = promptHalves(turn);
|
|
1337
|
+
let promptInput = promptTurn;
|
|
1338
|
+
let pendingSplitReceipt = null;
|
|
1339
|
+
if (halves.stable !== null) {
|
|
1340
|
+
const receiptKey = JSON.stringify([threadId, sessionId]);
|
|
1341
|
+
const composed = splitSessionPrompt(halves.stable, halves.volatile, readPromptSplitReceipt(DRIVER_KIND, receiptKey), promptTurn.system, promptTurn.text, Boolean(turn.mentionTurn), ACP_PROMPT_RE_ANCHOR_TURNS);
|
|
1342
|
+
promptInput = { ...promptTurn, system: "", text: composed.text };
|
|
1343
|
+
pendingSplitReceipt = { key: receiptKey, receipt: composed.receipt };
|
|
1344
|
+
}
|
|
1286
1345
|
const text = support.buildPromptText
|
|
1287
|
-
? support.buildPromptText(
|
|
1288
|
-
:
|
|
1289
|
-
? `${
|
|
1290
|
-
:
|
|
1346
|
+
? support.buildPromptText(promptInput)
|
|
1347
|
+
: promptInput.system
|
|
1348
|
+
? `${promptInput.system}\n\n${promptInput.text}`
|
|
1349
|
+
: promptInput.text;
|
|
1291
1350
|
const imageBlocks = support.images === true && runtimeAcceptsImages
|
|
1292
1351
|
? await readAcpImageBlocks(turn.images ?? [])
|
|
1293
1352
|
: [];
|
|
@@ -1301,6 +1360,12 @@ export function createAcpDriver(support) {
|
|
|
1301
1360
|
const promptIdleMs = promptIdleTimeoutMs();
|
|
1302
1361
|
const result = await request("session/prompt", { sessionId, prompt: [{ type: "text", text }, ...imageBlocks] }, undefined, undefined, promptIdleMs, `${DRIVER_KIND} went fully silent ${Math.round(promptIdleMs / 1000)} s after the message and the turn was stopped. ` +
|
|
1303
1362
|
"Raise OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS if this model legitimately takes longer to answer.");
|
|
1363
|
+
if (pendingSplitReceipt) {
|
|
1364
|
+
// session/prompt resolving is the acceptance boundary: a
|
|
1365
|
+
// rejected prompt leaves the receipt unwritten, so the next
|
|
1366
|
+
// turn redelivers what this one never received.
|
|
1367
|
+
writePromptSplitReceipt(DRIVER_KIND, pendingSplitReceipt.key, pendingSplitReceipt.receipt);
|
|
1368
|
+
}
|
|
1304
1369
|
// opencode 1.18.18 reports usage at the result root; grok and
|
|
1305
1370
|
// gemini put it under _meta. Read both rather than lose the count.
|
|
1306
1371
|
const usage = result?.usage ?? result?._meta ?? {};
|
|
@@ -1407,12 +1472,7 @@ export function createAcpDriver(support) {
|
|
|
1407
1472
|
const finish = turn?.asks.get(requestId);
|
|
1408
1473
|
if (!finish)
|
|
1409
1474
|
return "unavailable"; // settled, timed out, or turn gone
|
|
1410
|
-
finish(decision.behavior, "user", decision.message, decision.always === true);
|
|
1411
|
-
return decision.behavior === "allow"
|
|
1412
|
-
? "allowed-once"
|
|
1413
|
-
: decision.behavior === "answer"
|
|
1414
|
-
? "answered"
|
|
1415
|
-
: "rejected";
|
|
1475
|
+
return finish(decision.behavior, "user", decision.message, decision.always === true);
|
|
1416
1476
|
},
|
|
1417
1477
|
hasSession: (threadId) => active.has(threadId),
|
|
1418
1478
|
stopAll: async () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { titleCaseModelId } from "../../contracts.js";
|
|
1
2
|
import { execCli } from "../../procs.js";
|
|
2
3
|
import { createAcpDriver } from "./core.js";
|
|
3
4
|
/** Translate an argv `--model` slug into the id this ACP session will accept.
|
|
@@ -86,13 +87,8 @@ function asRecord(value) {
|
|
|
86
87
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
87
88
|
}
|
|
88
89
|
function labelFor(id, explicit) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return id
|
|
92
|
-
.split(/[-_./]+/g)
|
|
93
|
-
.filter(Boolean)
|
|
94
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
95
|
-
.join(" ");
|
|
90
|
+
const trimmed = explicit?.trim();
|
|
91
|
+
return trimmed || titleCaseModelId(id, /[-_./]+/);
|
|
96
92
|
}
|
|
97
93
|
function pushModel(options, seen, id, label, custom = false) {
|
|
98
94
|
if (!SLUG.test(id) || seen.has(id))
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// options set over the wire (session/set_model, session/set_mode), which is
|
|
17
17
|
// why both live in configureSession() below and NOT in spawnArgs.
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
-
import { homedir } from "node:os";
|
|
20
19
|
import { join } from "node:path";
|
|
20
|
+
import { userHome } from "../../env-path.js";
|
|
21
21
|
import { decodeInjectId, hostApiKey, localHost, mergeLocalInject } from "../local-inject.js";
|
|
22
22
|
import { createAcpDriver } from "./core.js";
|
|
23
23
|
// FACTORY_HOME_OVERRIDE replaces the HOME the CLI resolves, NOT the data root:
|
|
@@ -34,7 +34,7 @@ import { createAcpDriver } from "./core.js";
|
|
|
34
34
|
// out. Names read from the 0.196.0 binary.
|
|
35
35
|
const AUTH_FILES = ["auth.v2.file", "auth.v2.loginkeychain", "auth.v2.keyring"];
|
|
36
36
|
function authFilePaths(env) {
|
|
37
|
-
const home = env.FACTORY_HOME_OVERRIDE || env
|
|
37
|
+
const home = env.FACTORY_HOME_OVERRIDE || userHome(env);
|
|
38
38
|
return AUTH_FILES.map((name) => join(home, ".factory", name));
|
|
39
39
|
}
|
|
40
40
|
const INJECT_ID_PREFIX = "custom:openmausbot-";
|
|
@@ -42,9 +42,6 @@ export function droidInjectId(host, model) {
|
|
|
42
42
|
const safe = `${host}-${model}`.replace(/[^a-zA-Z0-9._+-]+/g, "-").replace(/-+/g, "-");
|
|
43
43
|
return `${INJECT_ID_PREFIX}${safe}`;
|
|
44
44
|
}
|
|
45
|
-
function factoryHome(env) {
|
|
46
|
-
return env.FACTORY_HOME_OVERRIDE || env.HOME || env.USERPROFILE || homedir();
|
|
47
|
-
}
|
|
48
45
|
/** Upsert a BYOK custom model so session/set_model can reach the local host. */
|
|
49
46
|
export function ensureDroidInjectModel(modelId, env = process.env) {
|
|
50
47
|
const inject = decodeInjectId(modelId);
|
|
@@ -54,7 +51,7 @@ export function ensureDroidInjectModel(modelId, env = process.env) {
|
|
|
54
51
|
if (!host)
|
|
55
52
|
return modelId;
|
|
56
53
|
const id = droidInjectId(inject.host, inject.model);
|
|
57
|
-
const dir = join(
|
|
54
|
+
const dir = join(env.FACTORY_HOME_OVERRIDE || userHome(env), ".factory");
|
|
58
55
|
mkdirSync(dir, { recursive: true });
|
|
59
56
|
const path = join(dir, "settings.json");
|
|
60
57
|
let settings = {};
|
|
@@ -105,7 +102,7 @@ export function applyDroidLocalAuthEnv(env, modelId) {
|
|
|
105
102
|
env.FACTORY_API_KEY = "openmausbot-local";
|
|
106
103
|
}
|
|
107
104
|
function readSettings(env) {
|
|
108
|
-
return JSON.parse(readFileSync(join(
|
|
105
|
+
return JSON.parse(readFileSync(join(env.FACTORY_HOME_OVERRIDE || userHome(env), ".factory", "settings.json"), "utf8"));
|
|
109
106
|
}
|
|
110
107
|
async function resolveModels(env) {
|
|
111
108
|
let catalog = MODELS;
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
// the ACP flag + auth method ids follow the published Gemini CLI ACP contract
|
|
27
27
|
// and should be re-verified end-to-end once the CLI is present.
|
|
28
28
|
import { existsSync, readFileSync } from "node:fs";
|
|
29
|
-
import { homedir } from "node:os";
|
|
30
29
|
import { join } from "node:path";
|
|
30
|
+
import { userHome } from "../../env-path.js";
|
|
31
31
|
import { createAcpDriver } from "./core.js";
|
|
32
32
|
// Prefer an explicit key method, then personal OAuth, then Vertex — but fall
|
|
33
33
|
// back to whatever the CLI advertises so a new method id still works.
|
|
@@ -37,9 +37,9 @@ const AUTH_PREFERENCE = ["gemini-api-key", "oauth-personal", "vertex-ai"];
|
|
|
37
37
|
* an enterprise licence may still redeem. A stale consumer credential —
|
|
38
38
|
* the common case since 2026-06-18 — reads as not signed in, which is the
|
|
39
39
|
* truthful answer for an engine that would fail its first turn anyway. */
|
|
40
|
-
function liveOauthCredential() {
|
|
40
|
+
function liveOauthCredential(env) {
|
|
41
41
|
try {
|
|
42
|
-
const creds = JSON.parse(readFileSync(join(
|
|
42
|
+
const creds = JSON.parse(readFileSync(join(userHome(env), ".gemini", "oauth_creds.json"), "utf8"));
|
|
43
43
|
if (typeof creds.access_token !== "string" || creds.access_token.trim().length === 0)
|
|
44
44
|
return false;
|
|
45
45
|
const hasRefresh = typeof creds.refresh_token === "string" && creds.refresh_token.trim().length > 0;
|
|
@@ -62,7 +62,7 @@ const nonBlank = (value) => Boolean(value?.trim());
|
|
|
62
62
|
export function geminiIsAuthenticated(env) {
|
|
63
63
|
return (nonBlank(env.GEMINI_API_KEY) ||
|
|
64
64
|
nonBlank(env.GOOGLE_API_KEY) ||
|
|
65
|
-
(existsSync(join(
|
|
65
|
+
(existsSync(join(userHome(env), ".gemini", "oauth_creds.json")) && liveOauthCredential(env)));
|
|
66
66
|
}
|
|
67
67
|
/** Gemini CLI's approval modes, passed through: `--approval-mode auto_edit`
|
|
68
68
|
* approves file edits, `--yolo` approves everything; Gemini has no
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// The generic protocol runtime lives in acp/core.ts; this file is only the
|
|
5
5
|
// per-harness quirks. Verified against grok 1.0.0.
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { homedir } from "node:os";
|
|
8
7
|
import { join } from "node:path";
|
|
8
|
+
import { harnessHome } from "../../env-path.js";
|
|
9
9
|
import { decodeInjectId, hostApiKey, localHost, mergeLocalInject } from "../local-inject.js";
|
|
10
10
|
import { createAcpDriver } from "./core.js";
|
|
11
11
|
export const STATIC_GROK_MODELS = {
|
|
@@ -17,11 +17,6 @@ export const STATIC_GROK_MODELS = {
|
|
|
17
17
|
],
|
|
18
18
|
};
|
|
19
19
|
const SLUG = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
20
|
-
function grokHome(env) {
|
|
21
|
-
if (env.GROK_HOME)
|
|
22
|
-
return env.GROK_HOME;
|
|
23
|
-
return join(env.HOME || env.USERPROFILE || homedir(), ".grok");
|
|
24
|
-
}
|
|
25
20
|
function unquote(raw) {
|
|
26
21
|
const value = raw.trim();
|
|
27
22
|
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
@@ -32,7 +27,7 @@ function unquote(raw) {
|
|
|
32
27
|
/** Local slugs from ~/.grok/config.toml, plus the cloud defaults.
|
|
33
28
|
* `grok -m <slug>` already accepts these; the picker just didn't list them. */
|
|
34
29
|
export function readGrokModelCatalog(env = process.env) {
|
|
35
|
-
const path = join(
|
|
30
|
+
const path = join(env.GROK_HOME || harnessHome("grok", env), "config.toml");
|
|
36
31
|
let text = "";
|
|
37
32
|
try {
|
|
38
33
|
text = readFileSync(path, "utf8");
|
|
@@ -114,7 +109,7 @@ export function ensureGrokInjectSlug(modelId, env = process.env) {
|
|
|
114
109
|
const host = localHost(inject.host);
|
|
115
110
|
if (!host)
|
|
116
111
|
return modelId;
|
|
117
|
-
const path = join(
|
|
112
|
+
const path = join(env.GROK_HOME || harnessHome("grok", env), "config.toml");
|
|
118
113
|
let text = "";
|
|
119
114
|
try {
|
|
120
115
|
text = readFileSync(path, "utf8");
|
|
@@ -258,7 +253,7 @@ const support = {
|
|
|
258
253
|
// an unauthenticated CLI is a user action, not something to paper over.
|
|
259
254
|
pickAuthMethod: (methods) => (methods.some((m) => m.id === "cached_token") ? "cached_token" : null),
|
|
260
255
|
authFailure: "fail",
|
|
261
|
-
isAuthenticated: () => existsSync(join(
|
|
256
|
+
isAuthenticated: (env) => existsSync(join(harnessHome("grok", env), "auth.json")),
|
|
262
257
|
// `--append-system-prompt`/`--rules` are accepted by the CLI but do NOT
|
|
263
258
|
// reach the agent-stdio system prompt (verified against 1.0.0), so the
|
|
264
259
|
// persona is prepended codex-style.
|
|
@@ -9,6 +9,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { parse as parseYaml } from "yaml";
|
|
12
|
+
import { harnessHome } from "../../env-path.js";
|
|
12
13
|
import { resolveCli } from "../../procs.js";
|
|
13
14
|
import { decodeInjectId, hostApiKey, INJECT_SEP, localHost, mergeLocalInject } from "../local-inject.js";
|
|
14
15
|
import { createAcpDriver } from "./core.js";
|
|
@@ -25,9 +26,6 @@ export function bindHermesScreenshotCompat(env, modelId) {
|
|
|
25
26
|
env[HERMES_OPENMAUS_SCREENSHOT_COMPAT] = "1";
|
|
26
27
|
env[HERMES_OPENMAUS_SCREENSHOT_COMPAT_MODEL] = inject.model;
|
|
27
28
|
}
|
|
28
|
-
function hermesHome(env) {
|
|
29
|
-
return env.HERMES_HOME || join(env.HOME || env.USERPROFILE || homedir(), ".hermes");
|
|
30
|
-
}
|
|
31
29
|
function quoteYaml(value) {
|
|
32
30
|
if (/^[\w./:+-]+$/.test(value))
|
|
33
31
|
return value;
|
|
@@ -69,7 +67,7 @@ export function ensureHermesInjectProvider(modelId, env = process.env) {
|
|
|
69
67
|
const host = localHost(inject.host);
|
|
70
68
|
if (!host)
|
|
71
69
|
return modelId;
|
|
72
|
-
const dir =
|
|
70
|
+
const dir = env.HERMES_HOME || harnessHome("hermes", env);
|
|
73
71
|
mkdirSync(dir, { recursive: true });
|
|
74
72
|
const path = join(dir, "config.yaml");
|
|
75
73
|
let text = "";
|
|
@@ -199,7 +197,7 @@ function hermesConfigDefault(text) {
|
|
|
199
197
|
* catalog exactly as it was for local-only setups.
|
|
200
198
|
*/
|
|
201
199
|
export function hermesConfiguredModel(env = process.env) {
|
|
202
|
-
const dir =
|
|
200
|
+
const dir = env.HERMES_HOME || harnessHome("hermes", env);
|
|
203
201
|
let secrets = "";
|
|
204
202
|
try {
|
|
205
203
|
secrets = readFileSync(join(dir, ".env"), "utf8");
|