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
|
@@ -26,16 +26,33 @@ import { join } from "node:path";
|
|
|
26
26
|
import { PROVIDER_CREDENTIAL_ENV, stripWorkspaceCredentialEnv } from "../config.js";
|
|
27
27
|
import { augmentedPath } from "../env-path.js";
|
|
28
28
|
import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
|
|
29
|
+
import { deletePromptSplitReceipt, promptHalves, readPromptSplitReceipt, splitSessionPrompt, writePromptSplitReceipt, } from "./prompt-split.js";
|
|
29
30
|
import { SPAWNED_PROXIES } from "../proxy-paths.js";
|
|
30
31
|
import { commandSummary, toolDetailPreview } from "../tool-summary.js";
|
|
31
32
|
import { EFFORT_LEVELS } from "../../shared/wire.js";
|
|
32
33
|
import { newEventId, newId } from "../contracts.js";
|
|
34
|
+
import { parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../shared/ask-question.js";
|
|
33
35
|
import { decodeInjectId, encodeInjectId, hostApiKey, localHost, mergeLocalInject, } from "./local-inject.js";
|
|
34
36
|
import { appendNative } from "./native.js";
|
|
35
37
|
const DRIVER_KIND = "piAgent";
|
|
36
38
|
const PI_ARGS = ["--mode", "rpc", "--no-session"];
|
|
37
39
|
const PI_MODEL_UPDATE_ARGS = ["update", "--models", "--no-approve"];
|
|
38
40
|
const NODE_ENV_FLAG = { ELECTRON_RUN_AS_NODE: "1" };
|
|
41
|
+
/** After this many bare turns on one pi session the full prompt rides
|
|
42
|
+
* again even without a compaction event: any history rewrite the events
|
|
43
|
+
* miss (an older pi, a missed line) still loses at most this many turns
|
|
44
|
+
* of standing instructions instead of the rest of the session. */
|
|
45
|
+
const PI_PROMPT_RE_ANCHOR_TURNS = 8;
|
|
46
|
+
/** How long the driver keeps listening after a finished turn_end for the
|
|
47
|
+
* run's terminal agent_end before settling anyway: a runtime that never
|
|
48
|
+
* emits agent_end still completes, while one that does cancels the wait. */
|
|
49
|
+
const PI_AGENT_END_GRACE_MS = 60_000;
|
|
50
|
+
/** pi answered a command with success:false — an explicit refusal from a
|
|
51
|
+
* live runtime, not a transport failure. Only steer branches on the
|
|
52
|
+
* difference today ("refused": provably not delivered, safe to re-queue);
|
|
53
|
+
* the handshake commands keep their catch-all ignore. */
|
|
54
|
+
class PiRpcRefusalError extends Error {
|
|
55
|
+
}
|
|
39
56
|
function readPiPromptImages(turn) {
|
|
40
57
|
return (turn.images ?? []).map((image) => ({
|
|
41
58
|
type: "image",
|
|
@@ -425,7 +442,18 @@ export const PiDriver = {
|
|
|
425
442
|
}
|
|
426
443
|
const turnId = newId();
|
|
427
444
|
const pending = new Map();
|
|
445
|
+
// Ask fail-safe timers, tracked so settle() can cancel them outright:
|
|
446
|
+
// a cleared pending map alone leaves each timer holding the ask
|
|
447
|
+
// closure (send, child) alive until it fires.
|
|
448
|
+
const askTimers = new Set();
|
|
428
449
|
let settled = false;
|
|
450
|
+
// the last turn_end's outcome, replayed when the run's terminal
|
|
451
|
+
// agent_end (or the grace fallback) settles the turn
|
|
452
|
+
let endStopReason;
|
|
453
|
+
let endUsage;
|
|
454
|
+
let endErrorMessage;
|
|
455
|
+
// hang insurance for runtimes with no agent_end frame at all
|
|
456
|
+
let agentEndTimer = null;
|
|
429
457
|
// pi's RPC surface accepts image content directly. Read before spawning
|
|
430
458
|
// so an attachment that disappeared produces one clear dispatch error
|
|
431
459
|
// instead of starting a child that can never receive its prompt.
|
|
@@ -488,8 +516,17 @@ export const PiDriver = {
|
|
|
488
516
|
})();
|
|
489
517
|
let buf = "";
|
|
490
518
|
let assistantText = "";
|
|
519
|
+
// set when a compaction event arrives this turn; gates the receipt
|
|
520
|
+
// write below so a compacted-around delivery is never claimed
|
|
521
|
+
let compactionObserved = false;
|
|
491
522
|
// resolve one-shot RPC responses (new_session / switch_session / set_model)
|
|
492
523
|
const responseWaiters = new Map();
|
|
524
|
+
// Steer frames correlate by a unique per-request id, not the shared
|
|
525
|
+
// "steer" command name: two concurrent steers would otherwise overwrite
|
|
526
|
+
// each other's waiter, and a refusal response could reject the wrong
|
|
527
|
+
// caller — requeueing words pi already accepted. Every other command
|
|
528
|
+
// keeps its command-name key.
|
|
529
|
+
let steerSeq = 0;
|
|
493
530
|
const rejectWaiters = (err) => {
|
|
494
531
|
for (const waiter of responseWaiters.values()) {
|
|
495
532
|
clearTimeout(waiter.timer);
|
|
@@ -497,13 +534,13 @@ export const PiDriver = {
|
|
|
497
534
|
}
|
|
498
535
|
responseWaiters.clear();
|
|
499
536
|
};
|
|
500
|
-
const awaitResponse = (command, timeoutMs = 20_000) => new Promise((resolve, reject) => {
|
|
537
|
+
const awaitResponse = (command, timeoutMs = 20_000, key = command) => new Promise((resolve, reject) => {
|
|
501
538
|
const timer = setTimeout(() => {
|
|
502
|
-
responseWaiters.delete(
|
|
539
|
+
responseWaiters.delete(key);
|
|
503
540
|
reject(new Error(`pi ${command} timed out`));
|
|
504
541
|
}, timeoutMs);
|
|
505
542
|
timer.unref?.();
|
|
506
|
-
responseWaiters.set(
|
|
543
|
+
responseWaiters.set(key, { resolve, reject, timer });
|
|
507
544
|
});
|
|
508
545
|
child.stdin.on("error", () => rejectWaiters(new Error("pi stdin closed")));
|
|
509
546
|
const send = (obj) => {
|
|
@@ -522,6 +559,17 @@ export const PiDriver = {
|
|
|
522
559
|
if (settled)
|
|
523
560
|
return;
|
|
524
561
|
settled = true;
|
|
562
|
+
if (agentEndTimer) {
|
|
563
|
+
clearTimeout(agentEndTimer);
|
|
564
|
+
agentEndTimer = null;
|
|
565
|
+
}
|
|
566
|
+
// The turn is over: drop unanswered asks so their 15-minute
|
|
567
|
+
// fail-safe timers are cancelled outright instead of no-oping on a
|
|
568
|
+
// dead child while holding the ask closure alive.
|
|
569
|
+
for (const timer of askTimers)
|
|
570
|
+
clearTimeout(timer);
|
|
571
|
+
askTimers.clear();
|
|
572
|
+
pending.clear();
|
|
525
573
|
flushAssistantText();
|
|
526
574
|
emit({
|
|
527
575
|
...base(threadId, turnId),
|
|
@@ -552,6 +600,23 @@ export const PiDriver = {
|
|
|
552
600
|
}
|
|
553
601
|
active.delete(threadId);
|
|
554
602
|
};
|
|
603
|
+
/** Settle from the last turn_end's recorded outcome. Errors surface
|
|
604
|
+
* here, at run completion, so a failed run reports exactly once and
|
|
605
|
+
* only after any scheduled maintenance events had their chance to
|
|
606
|
+
* arrive. */
|
|
607
|
+
const finishRun = () => {
|
|
608
|
+
const sr = endStopReason;
|
|
609
|
+
if (sr === "error" || sr === "failed") {
|
|
610
|
+
emit({
|
|
611
|
+
...base(threadId, turnId),
|
|
612
|
+
type: "runtime.error",
|
|
613
|
+
message: String(endErrorMessage ?? "pi turn failed").slice(0, 2_000),
|
|
614
|
+
});
|
|
615
|
+
settle(false, "failed", endUsage);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
settle(true, sr === "cancelled" || sr === "aborted" ? "cancelled" : "end_turn", endUsage);
|
|
619
|
+
};
|
|
555
620
|
const stop = () => {
|
|
556
621
|
try {
|
|
557
622
|
send({ type: "abort" });
|
|
@@ -567,19 +632,53 @@ export const PiDriver = {
|
|
|
567
632
|
}
|
|
568
633
|
settle(true, "cancelled");
|
|
569
634
|
};
|
|
570
|
-
|
|
635
|
+
// Mid-turn input rides pi's native steer frame — the runtime queues
|
|
636
|
+
// it into the running agent without aborting the in-flight step, and
|
|
637
|
+
// answers success only after session.steer() accepted it. A success:
|
|
638
|
+
// false response is an explicit refusal (nothing consumed the words:
|
|
639
|
+
// safe to re-queue), while a death or timeout after the frame was
|
|
640
|
+
// written leaves the outcome unknowable — those are "indeterminate"
|
|
641
|
+
// so the caller can never run the words twice. Each frame carries a
|
|
642
|
+
// unique id that pi echoes on its response, so concurrent steers on
|
|
643
|
+
// the same turn never share or overwrite a waiter.
|
|
644
|
+
const steerActiveTurn = async (text) => {
|
|
645
|
+
if (settled || child.exitCode !== null || child.signalCode !== null)
|
|
646
|
+
return "refused";
|
|
647
|
+
let ack;
|
|
648
|
+
try {
|
|
649
|
+
const id = `steer:${++steerSeq}`;
|
|
650
|
+
ack = awaitResponse("steer", 20_000, id);
|
|
651
|
+
send({ type: "steer", id, message: text });
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// stdin closed before the frame was written: provably undelivered.
|
|
655
|
+
return "refused";
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
await ack;
|
|
659
|
+
return "steered";
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
return error instanceof PiRpcRefusalError ? "refused" : "indeterminate";
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
active.set(threadId, { stop, turnId, pending, child, steer: steerActiveTurn });
|
|
571
666
|
const onEvent = (evt) => {
|
|
572
667
|
appendNative(threadId, { dir: "in", source: "pi.rpc", msg: evt });
|
|
573
668
|
switch (evt.type) {
|
|
574
669
|
case "response": {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
670
|
+
// Resolve the echoed request id first (unique per steer);
|
|
671
|
+
// fall back to the command name for the handshake-style call
|
|
672
|
+
// sites, whose responses carry no id.
|
|
673
|
+
const key = typeof evt.id === "string" && responseWaiters.has(evt.id) ? evt.id : evt.command;
|
|
674
|
+
if (key && responseWaiters.has(key)) {
|
|
675
|
+
const waiter = responseWaiters.get(key);
|
|
676
|
+
responseWaiters.delete(key);
|
|
578
677
|
clearTimeout(waiter.timer);
|
|
579
678
|
if (evt.success)
|
|
580
679
|
waiter.resolve(evt.data);
|
|
581
680
|
else
|
|
582
|
-
waiter.reject(new
|
|
681
|
+
waiter.reject(new PiRpcRefusalError(`pi ${evt.command} failed`));
|
|
583
682
|
}
|
|
584
683
|
return;
|
|
585
684
|
}
|
|
@@ -626,48 +725,158 @@ export const PiDriver = {
|
|
|
626
725
|
if (evt.method === "select" || evt.method === "confirm" || evt.method === "input") {
|
|
627
726
|
flushAssistantText();
|
|
628
727
|
const reqId = evt.id ?? newId();
|
|
629
|
-
const
|
|
728
|
+
const isSelect = evt.method === "select";
|
|
729
|
+
const isQuestion = isSelect || evt.method === "input";
|
|
730
|
+
const selectOptions = isSelect && Array.isArray(evt.options)
|
|
731
|
+
? evt.options.filter((option) => typeof option === "string") : [];
|
|
732
|
+
const summary = String(evt.title ?? (isQuestion ? "pi has a question" : "pi wants confirmation")).slice(0, 200);
|
|
733
|
+
// A select is a question with named options; the structured
|
|
734
|
+
// card renders from it while the flat choices keep older
|
|
735
|
+
// clients answering. An input has nothing to pick from and
|
|
736
|
+
// stays the free-text question it always was.
|
|
737
|
+
const question = isSelect
|
|
738
|
+
? (parseAskQuestions({
|
|
739
|
+
questions: [{ question: summary, options: selectOptions }],
|
|
740
|
+
}) ?? [])[0]
|
|
741
|
+
: undefined;
|
|
742
|
+
const choices = question?.options.length ? question.options.map((option) => option.label) : undefined;
|
|
743
|
+
let timer;
|
|
630
744
|
// Register BEFORE emitting: the harness may auto-approve from
|
|
631
745
|
// inside its synchronous request.opened listener. Emitting first
|
|
632
746
|
// made respondToRequest see no pending ask, return unavailable,
|
|
633
747
|
// then fall back to a human card on every "Always allow" call.
|
|
634
748
|
pending.set(reqId, (decision) => {
|
|
749
|
+
if (timer) {
|
|
750
|
+
clearTimeout(timer);
|
|
751
|
+
askTimers.delete(timer);
|
|
752
|
+
}
|
|
635
753
|
if (decision.behavior === "deny")
|
|
636
754
|
send({ type: "extension_ui_response", id: reqId, cancelled: true });
|
|
637
|
-
else if (isQuestion)
|
|
638
|
-
|
|
755
|
+
else if (isQuestion) {
|
|
756
|
+
// Recover the picked label from a structured card's Q:/A:
|
|
757
|
+
// reply; a flat answer or typed text passes through
|
|
758
|
+
// verbatim (questionAnswersByQuestion's single-question
|
|
759
|
+
// fallback does exactly that).
|
|
760
|
+
const value = question
|
|
761
|
+
? questionAnswersByQuestion(decision.message ?? "", [question])[question.question] ?? decision.message ?? ""
|
|
762
|
+
: decision.message ?? "";
|
|
763
|
+
// Display labels are capped/trimmed; pi expects the
|
|
764
|
+
// original option. Never guess if two normalize alike.
|
|
765
|
+
const matched = selectOptions.filter(option => parseChoices([option], 1)?.[0] === value);
|
|
766
|
+
send(matched.length > 1
|
|
767
|
+
? { type: "extension_ui_response", id: reqId, cancelled: true }
|
|
768
|
+
: { type: "extension_ui_response", id: reqId, value: matched[0] ?? value });
|
|
769
|
+
}
|
|
639
770
|
else
|
|
640
771
|
send({ type: "extension_ui_response", id: reqId, confirmed: true });
|
|
641
772
|
});
|
|
773
|
+
// The ask must never hold the turn forever: cancel it after 15
|
|
774
|
+
// minutes. The pending.delete guard makes this a no-op once the
|
|
775
|
+
// turn settled (settle clears pending) or a person answered.
|
|
776
|
+
timer = setTimeout(() => {
|
|
777
|
+
if (timer)
|
|
778
|
+
askTimers.delete(timer);
|
|
779
|
+
if (!pending.delete(reqId))
|
|
780
|
+
return;
|
|
781
|
+
send({ type: "extension_ui_response", id: reqId, cancelled: true });
|
|
782
|
+
emit({
|
|
783
|
+
...base(threadId, turnId),
|
|
784
|
+
requestId: reqId,
|
|
785
|
+
type: "request.resolved",
|
|
786
|
+
behavior: "deny",
|
|
787
|
+
source: "timeout",
|
|
788
|
+
});
|
|
789
|
+
}, 15 * 60_000);
|
|
790
|
+
askTimers.add(timer);
|
|
791
|
+
timer.unref?.();
|
|
642
792
|
emit({
|
|
643
793
|
...base(threadId, turnId),
|
|
644
794
|
requestId: reqId,
|
|
645
795
|
type: "request.opened",
|
|
646
796
|
requestType: isQuestion ? "question" : "permission",
|
|
647
797
|
tool: String(evt.title ?? "pi"),
|
|
648
|
-
summary
|
|
798
|
+
summary,
|
|
799
|
+
...(choices ? { choices } : {}),
|
|
800
|
+
...(question ? { questions: [question] } : {}),
|
|
649
801
|
});
|
|
650
802
|
}
|
|
651
803
|
return;
|
|
652
804
|
}
|
|
805
|
+
case "compaction_start":
|
|
806
|
+
case "compaction_end":
|
|
807
|
+
case "auto_compaction_start":
|
|
808
|
+
case "auto_compaction_end": {
|
|
809
|
+
// pi compaction (manual or automatic) summarizes older
|
|
810
|
+
// messages, which can absorb the turn that carried this
|
|
811
|
+
// session's standing prompt. Both events ride the RPC surface
|
|
812
|
+
// for every compaction kind, so the moment either arrives the
|
|
813
|
+
// receipt is dropped and the next turn re-establishes the full
|
|
814
|
+
// prompt. The rest of the compacting turn may still answer
|
|
815
|
+
// without its standing instructions; that single turn is the
|
|
816
|
+
// unavoidable cost of summarizing history mid-session.
|
|
817
|
+
if (sessionReady && sessionFile) {
|
|
818
|
+
compactionObserved = true;
|
|
819
|
+
deletePromptSplitReceipt("pi", JSON.stringify([threadId, sessionFile]));
|
|
820
|
+
}
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
653
823
|
case "turn_end":
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
824
|
+
{
|
|
825
|
+
// a killed child can still flush one buffered frame after the
|
|
826
|
+
// turn settled (the abort path's cancelled turn_end is the
|
|
827
|
+
// common one); arming the grace timer then would leak it past
|
|
828
|
+
// a turn that is already complete.
|
|
829
|
+
if (settled)
|
|
830
|
+
return;
|
|
831
|
+
const sr = evt.message?.stopReason;
|
|
832
|
+
// toolUse means pi ran a tool and auto-continues next turn to
|
|
833
|
+
// answer — the run is not over, and the next turn_end carries
|
|
834
|
+
// the real outcome.
|
|
835
|
+
if (sr === "toolUse" || sr === "tool_use" || sr === "tool_calls")
|
|
836
|
+
return;
|
|
837
|
+
endStopReason = sr;
|
|
838
|
+
endUsage = evt.usage ?? evt.message?.usage;
|
|
839
|
+
endErrorMessage = evt.message?.errorMessage;
|
|
840
|
+
// agent_end, not turn_end, closes the run: pi can schedule
|
|
841
|
+
// post-run maintenance (overflow-recovery compaction) after
|
|
842
|
+
// the final turn_end, and only the terminal agent_end proves
|
|
843
|
+
// the session is done rewriting itself. The grace timer
|
|
844
|
+
// settles a runtime that never sends agent_end at all.
|
|
845
|
+
if (agentEndTimer)
|
|
846
|
+
clearTimeout(agentEndTimer);
|
|
847
|
+
agentEndTimer = setTimeout(finishRun, PI_AGENT_END_GRACE_MS);
|
|
848
|
+
agentEndTimer.unref?.();
|
|
659
849
|
return;
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
850
|
+
}
|
|
851
|
+
case "agent_end": {
|
|
852
|
+
// Two dialects mark a non-terminal agent_end: upstream pi sets
|
|
853
|
+
// willRetry: true when an automatic retry follows (overflow
|
|
854
|
+
// recovery runs compaction and retries as a fresh run), and the
|
|
855
|
+
// omp fork sets isTerminal: false while maintenance or async
|
|
856
|
+
// delivery has more work scheduled. Either marker means the
|
|
857
|
+
// session may still rewrite the turn that carried this
|
|
858
|
+
// session's standing prompt — keep listening so those events
|
|
859
|
+
// can drop the prompt-split receipt.
|
|
860
|
+
if (evt.isTerminal === false || evt.willRetry === true)
|
|
668
861
|
return;
|
|
862
|
+
if (agentEndTimer) {
|
|
863
|
+
clearTimeout(agentEndTimer);
|
|
864
|
+
agentEndTimer = null;
|
|
865
|
+
}
|
|
866
|
+
finishRun();
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
case "agent_settled": {
|
|
870
|
+
// upstream pi's explicit fully-settled guarantee: no automatic
|
|
871
|
+
// retry, compaction retry, or queued continuation remains.
|
|
872
|
+
// Usually settles one frame after the final agent_end already
|
|
873
|
+
// did; kept as its own terminal signal so the driver never
|
|
874
|
+
// depends on which dialect the configured cli speaks.
|
|
875
|
+
if (agentEndTimer) {
|
|
876
|
+
clearTimeout(agentEndTimer);
|
|
877
|
+
agentEndTimer = null;
|
|
669
878
|
}
|
|
670
|
-
|
|
879
|
+
finishRun();
|
|
671
880
|
return;
|
|
672
881
|
}
|
|
673
882
|
default:
|
|
@@ -709,6 +918,7 @@ export const PiDriver = {
|
|
|
709
918
|
// sessionFile, which switch_session expects as `sessionPath`.
|
|
710
919
|
const sessionPath = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
|
|
711
920
|
let sessionFile = sessionPath;
|
|
921
|
+
let sessionReady = false;
|
|
712
922
|
try {
|
|
713
923
|
const command = sessionPath ? "switch_session" : "new_session";
|
|
714
924
|
const hsPromise = awaitResponse(command);
|
|
@@ -716,6 +926,7 @@ export const PiDriver = {
|
|
|
716
926
|
const hs = (await hsPromise);
|
|
717
927
|
if (hs?.sessionFile)
|
|
718
928
|
sessionFile = hs.sessionFile;
|
|
929
|
+
sessionReady = true;
|
|
719
930
|
emit({
|
|
720
931
|
...base(threadId, turnId),
|
|
721
932
|
type: "session.started",
|
|
@@ -751,11 +962,71 @@ export const PiDriver = {
|
|
|
751
962
|
/* keep going on the engine default */
|
|
752
963
|
}
|
|
753
964
|
}
|
|
754
|
-
|
|
965
|
+
// The stable/volatile split: the full prompt rides only the turn that
|
|
966
|
+
// establishes - or re-instructs, after a soul edit - this pi session,
|
|
967
|
+
// which previously re-sent the whole system prompt on every turn and
|
|
968
|
+
// let it accumulate in the session file once per turn. pi compacts
|
|
969
|
+
// long sessions by summarizing older messages, which can absorb the
|
|
970
|
+
// turn that carried the prompt, so the driver watches pi's
|
|
971
|
+
// compaction events and drops the receipt the moment one arrives,
|
|
972
|
+
// and re-anchors the full prompt every PI_PROMPT_RE_ANCHOR_TURNS
|
|
973
|
+
// bare turns as a backstop for anything the events miss. Receipts
|
|
974
|
+
// are durable because the session file outlives both the per-turn
|
|
975
|
+
// child and this process. Without a session the prompt is the
|
|
976
|
+
// model's only context, so that turn keeps the full block and writes
|
|
977
|
+
// no receipt.
|
|
978
|
+
const halves = promptHalves(turn);
|
|
979
|
+
let message;
|
|
980
|
+
let pendingReceipt = null;
|
|
981
|
+
if (halves.stable !== null && sessionReady && sessionFile) {
|
|
982
|
+
const receiptKey = JSON.stringify([threadId, sessionFile]);
|
|
983
|
+
const composed = splitSessionPrompt(halves.stable, halves.volatile, readPromptSplitReceipt("pi", receiptKey), turn.system, turn.text, Boolean(turn.mentionTurn), PI_PROMPT_RE_ANCHOR_TURNS);
|
|
984
|
+
message = composed.text;
|
|
985
|
+
pendingReceipt = { key: receiptKey, receipt: composed.receipt };
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
message = turn.system ? `${turn.system}\n\n${turn.text}` : turn.text;
|
|
989
|
+
}
|
|
990
|
+
// events cannot interleave with this synchronous stretch, so any
|
|
991
|
+
// compaction recorded above already invalidated what compose read
|
|
755
992
|
try {
|
|
993
|
+
// pi answers the prompt with an authoritative response only after
|
|
994
|
+
// preflight accepts it - and rejects it outright while a
|
|
995
|
+
// compaction runs - so the receipt commits only after that
|
|
996
|
+
// acceptance: a rejected or compacted-around send redelivers on
|
|
997
|
+
// the next turn. The wait is long enough to survive a compaction
|
|
998
|
+
// queue; a dead child rejects it immediately.
|
|
999
|
+
// sendTurn still resolves once the prompt is written: a child that
|
|
1000
|
+
// dies before replying settles the turn through the close handler,
|
|
1001
|
+
// whose waiter sweep can precede this registration.
|
|
1002
|
+
const accepted = awaitResponse("prompt", 120_000);
|
|
756
1003
|
send({ type: "prompt", message, ...(images.length ? { images } : {}) });
|
|
1004
|
+
void accepted.then(() => {
|
|
1005
|
+
if (!pendingReceipt || compactionObserved)
|
|
1006
|
+
return;
|
|
1007
|
+
try {
|
|
1008
|
+
writePromptSplitReceipt("pi", pendingReceipt.key, pendingReceipt.receipt);
|
|
1009
|
+
}
|
|
1010
|
+
catch {
|
|
1011
|
+
/* an unwritten receipt only re-delivers the full prompt next turn */
|
|
1012
|
+
}
|
|
1013
|
+
}, (err) => {
|
|
1014
|
+
if (settled)
|
|
1015
|
+
return;
|
|
1016
|
+
emit({
|
|
1017
|
+
...base(threadId, turnId),
|
|
1018
|
+
type: "runtime.error",
|
|
1019
|
+
message: String(err?.message ?? err).slice(0, 2_000),
|
|
1020
|
+
});
|
|
1021
|
+
settle(false);
|
|
1022
|
+
});
|
|
757
1023
|
}
|
|
758
|
-
catch {
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
emit({
|
|
1026
|
+
...base(threadId, turnId),
|
|
1027
|
+
type: "runtime.error",
|
|
1028
|
+
message: String(err?.message ?? err).slice(0, 2_000),
|
|
1029
|
+
});
|
|
759
1030
|
settle(false);
|
|
760
1031
|
}
|
|
761
1032
|
return { turnId };
|
|
@@ -828,6 +1099,9 @@ export const PiDriver = {
|
|
|
828
1099
|
// xhigh/max only land on models that expose them; pi rejects an
|
|
829
1100
|
// unsupported level and the turn keeps the engine default.
|
|
830
1101
|
effortLevels: EFFORT_LEVELS,
|
|
1102
|
+
// pi's RPC mode takes a mid-turn steer frame the runtime queues
|
|
1103
|
+
// into the running agent without aborting the current step.
|
|
1104
|
+
queueing: true,
|
|
831
1105
|
},
|
|
832
1106
|
sendTurn,
|
|
833
1107
|
interruptTurn: async (threadId) => active.get(threadId)?.stop(),
|
|
@@ -847,6 +1121,10 @@ export const PiDriver = {
|
|
|
847
1121
|
});
|
|
848
1122
|
return decision.behavior === "allow" ? "allowed-once" : decision.behavior === "answer" ? "answered" : "rejected";
|
|
849
1123
|
},
|
|
1124
|
+
steer: async (threadId, text) => {
|
|
1125
|
+
const entry = active.get(threadId);
|
|
1126
|
+
return entry?.steer ? await entry.steer(text) : "refused";
|
|
1127
|
+
},
|
|
850
1128
|
hasSession: (threadId) => active.has(threadId),
|
|
851
1129
|
stopAll: async () => {
|
|
852
1130
|
for (const { stop } of active.values())
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The stable/volatile prompt split, shared by the drivers that deliver it.
|
|
2
|
+
// The stable half is everything that must stay byte-identical for a
|
|
3
|
+
// provider's cached prefix (or a spawned CLI's session contract) to
|
|
4
|
+
// survive; the volatile half (memory, mentions, outstanding teammate
|
|
5
|
+
// work, recent work) legitimately changes mid-conversation and reaches
|
|
6
|
+
// the model inside the turn that changed it, after the cacheable prefix.
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { DATA_DIR } from "../config.js";
|
|
11
|
+
import { writeFileAtomic } from "../atomic.js";
|
|
12
|
+
/** Read the split off a turn. Both halves must be present: a driver that
|
|
13
|
+
* receives only the unsplit system field was called through a path that
|
|
14
|
+
* never split it. */
|
|
15
|
+
export function promptHalves(turn) {
|
|
16
|
+
const { systemStable, systemVolatile } = turn;
|
|
17
|
+
if (typeof systemStable !== "string" || typeof systemVolatile !== "string") {
|
|
18
|
+
return { stable: null, volatile: "" };
|
|
19
|
+
}
|
|
20
|
+
return { stable: systemStable, volatile: systemVolatile };
|
|
21
|
+
}
|
|
22
|
+
export const VOLATILE_CONTEXT_NOTE_PREFIX = "Context from OpenMausBot updated since this conversation started; it replaces any earlier copy:";
|
|
23
|
+
export const VOLATILE_CONTEXT_CLEARED_NOTE = "The OpenMausBot context notes from earlier in this conversation (memory, mentions, outstanding teammate work) have been cleared; the standing instructions still apply.";
|
|
24
|
+
/** The labelled block that carries a changed volatile half inside a user
|
|
25
|
+
* turn. A half that is empty and always was needs no note; one that was
|
|
26
|
+
* cleared announces the removal so the model stops relying on it. */
|
|
27
|
+
export function volatileContextNote(volatile, hadVolatile) {
|
|
28
|
+
const text = volatile.trim();
|
|
29
|
+
if (text)
|
|
30
|
+
return VOLATILE_CONTEXT_NOTE_PREFIX + "\n\n" + text;
|
|
31
|
+
return hadVolatile ? VOLATILE_CONTEXT_CLEARED_NOTE : "";
|
|
32
|
+
}
|
|
33
|
+
/** Prepend a delivered note to the turn text: the one composition shape
|
|
34
|
+
* every split-aware driver uses. */
|
|
35
|
+
export function withContextNote(note, text) {
|
|
36
|
+
if (!note)
|
|
37
|
+
return text;
|
|
38
|
+
return text ? note + "\n\n" + text : note;
|
|
39
|
+
}
|
|
40
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
41
|
+
const EMPTY_FINGERPRINT = digest("");
|
|
42
|
+
export function promptSplitFingerprints(stable, volatile) {
|
|
43
|
+
return { stable: digest(stable), volatile: digest(volatile) };
|
|
44
|
+
}
|
|
45
|
+
const receiptPath = (scope, key) => join(DATA_DIR, "prompt-split", digest(JSON.stringify([scope, key])) + ".json");
|
|
46
|
+
/** Which halves a native session (an ACP session id, a pi session file)
|
|
47
|
+
* last carried. Unknown - never tracked, or tracked before this file
|
|
48
|
+
* existed - reads as null, and the caller re-delivers the full prompt
|
|
49
|
+
* once. */
|
|
50
|
+
export function readPromptSplitReceipt(scope, key) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = JSON.parse(readFileSync(receiptPath(scope, key), "utf8"));
|
|
53
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
54
|
+
const record = raw;
|
|
55
|
+
if (typeof record.stable === "string" && typeof record.volatile === "string") {
|
|
56
|
+
return {
|
|
57
|
+
stable: record.stable,
|
|
58
|
+
volatile: record.volatile,
|
|
59
|
+
...(typeof record.turnsSinceFull === "number" ? { turnsSinceFull: record.turnsSinceFull } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* a missing or corrupt receipt is simply unknown */
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
export function writePromptSplitReceipt(scope, key, receipt) {
|
|
70
|
+
mkdirSync(join(DATA_DIR, "prompt-split"), { recursive: true });
|
|
71
|
+
writeFileAtomic(receiptPath(scope, key), JSON.stringify(receipt), { mode: 0o600 });
|
|
72
|
+
}
|
|
73
|
+
/** Drop a session's receipt: its history was rewritten (pi compaction
|
|
74
|
+
* summarizes older messages), so what it last carried is unknown and the
|
|
75
|
+
* next turn re-delivers the full prompt. A missing receipt already reads
|
|
76
|
+
* as unknown, so deleting one is always safe. */
|
|
77
|
+
export function deletePromptSplitReceipt(scope, key) {
|
|
78
|
+
try {
|
|
79
|
+
rmSync(receiptPath(scope, key));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* a missing receipt is already unknown */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Compose the prompt for a session that persists its own history (ACP
|
|
86
|
+
* agents, pi): the full system block rides only when this native session
|
|
87
|
+
* has not carried it - or carried a different one, which re-delivers the
|
|
88
|
+
* current copy the way a pre-split turn always did - and a changed
|
|
89
|
+
* volatile half rides as a labelled note. Otherwise the turn text goes
|
|
90
|
+
* through bare, so an ordinary memory write neither appends a second copy
|
|
91
|
+
* of the prompt to the session nor re-prices its cached prefix.
|
|
92
|
+
* perTurnVolatile marks a turn whose volatile half describes this very
|
|
93
|
+
* turn (a mention): its note is delivered even when the text is unchanged.
|
|
94
|
+
* reAnchorAfterTurns, when positive, re-sends the full prompt after that
|
|
95
|
+
* many consecutive bare turns: a session that rewrites its own history
|
|
96
|
+
* without an observable event (an older pi, a missed compaction) still
|
|
97
|
+
* gets its standing instructions back within a bounded number of turns. */
|
|
98
|
+
export function splitSessionPrompt(stable, volatile, previous, fullSystem, text, perTurnVolatile = false, reAnchorAfterTurns = 0) {
|
|
99
|
+
const fingerprints = promptSplitFingerprints(stable, volatile);
|
|
100
|
+
// Legacy receipts predate the turn counter; count them from zero.
|
|
101
|
+
const turnsSinceFull = typeof previous?.turnsSinceFull === "number" ? previous.turnsSinceFull : 0;
|
|
102
|
+
const reAnchor = reAnchorAfterTurns > 0 && previous !== null && turnsSinceFull >= reAnchorAfterTurns;
|
|
103
|
+
if (previous === null || previous.stable !== fingerprints.stable || reAnchor) {
|
|
104
|
+
return { text: fullSystem ? fullSystem + "\n\n" + text : text, receipt: { ...fingerprints, turnsSinceFull: 0 } };
|
|
105
|
+
}
|
|
106
|
+
const note = previous.volatile === fingerprints.volatile && !perTurnVolatile
|
|
107
|
+
? ""
|
|
108
|
+
: volatileContextNote(volatile, previous.volatile !== EMPTY_FINGERPRINT);
|
|
109
|
+
return { text: withContextNote(note, text), receipt: { ...fingerprints, turnsSinceFull: turnsSinceFull + 1 } };
|
|
110
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// One session-idle policy for every pooled harness process. The Claude
|
|
2
|
+
// driver and the ACP core grew byte-identical copies of this computation
|
|
3
|
+
// with different env prefixes; the policy lives here so the default, the
|
|
4
|
+
// floor, and the env names cannot drift apart again.
|
|
5
|
+
const DEFAULT_IDLE_MS = 10 * 60_000;
|
|
6
|
+
const MINIMUM_IDLE_MS = 10_000;
|
|
7
|
+
/** setTimeout treats anything above 2^31-1 ms as 1 ms, so a larger delay
|
|
8
|
+
* would close the session immediately instead of much later. */
|
|
9
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
10
|
+
function usableMilliseconds(raw) {
|
|
11
|
+
const value = Number(raw);
|
|
12
|
+
return Number.isFinite(value) && value > 0 && value <= MAX_TIMER_MS ? value : null;
|
|
13
|
+
}
|
|
14
|
+
/** The idle policy for one harness. The per-harness names that preceded the
|
|
15
|
+
* unified ones (OMB_CLAUDE_SESSION_IDLE_*, OMB_ACP_SESSION_IDLE_*) win when
|
|
16
|
+
* both are set, so a harness-specific override keeps working alongside a
|
|
17
|
+
* global OMB_SESSION_IDLE_MS / OMB_SESSION_IDLE_MIN_MS; the unified names
|
|
18
|
+
* are the default for harnesses that did not set their own. A delay that is
|
|
19
|
+
* not a positive finite number of at most 2^31-1 ms is validated away before
|
|
20
|
+
* the floor applies — the old arithmetic coerced Infinity and larger values
|
|
21
|
+
* into an immediately-firing 1 ms timer. The floor is the greater of the
|
|
22
|
+
* built-in ten seconds and any configured minimum, so a per-harness minimum
|
|
23
|
+
* can raise the floor but never lower it, and it applies to whatever delay
|
|
24
|
+
* wins. */
|
|
25
|
+
export function sessionIdlePolicy(driverPrefix, env = process.env) {
|
|
26
|
+
const minimumMs = Math.max(MINIMUM_IDLE_MS, usableMilliseconds(env[`OMB_${driverPrefix}_SESSION_IDLE_MIN_MS`] ?? env.OMB_SESSION_IDLE_MIN_MS) ?? 0);
|
|
27
|
+
const configured = usableMilliseconds(env[`OMB_${driverPrefix}_SESSION_IDLE_MS`] ?? env.OMB_SESSION_IDLE_MS) ?? DEFAULT_IDLE_MS;
|
|
28
|
+
return { idleMs: Math.max(minimumMs, configured), minimumMs };
|
|
29
|
+
}
|
|
@@ -41,6 +41,7 @@ function knownDirs() {
|
|
|
41
41
|
join(home, ".volta", "bin"),
|
|
42
42
|
join(home, ".bun", "bin"),
|
|
43
43
|
join(home, ".asdf", "shims"),
|
|
44
|
+
join(home, ".local", "share", "mise", "shims"), // mise installer
|
|
44
45
|
join(home, ".deno", "bin"),
|
|
45
46
|
join(home, "bin"),
|
|
46
47
|
...nvmBinDirs(),
|
|
@@ -144,6 +145,20 @@ export function resetPathCacheForTests() {
|
|
|
144
145
|
loginShellPath = null;
|
|
145
146
|
registeredDirs.length = 0;
|
|
146
147
|
}
|
|
148
|
+
/** The user's home directory as this platform defines it. Windows keeps the
|
|
149
|
+
* real profile in USERPROFILE; a HOME that leaks in from a POSIX-flavored
|
|
150
|
+
* shell is not where Windows CLIs keep their state, so USERPROFILE wins there
|
|
151
|
+
* and HOME wins everywhere else. */
|
|
152
|
+
export function userHome(env = process.env) {
|
|
153
|
+
return process.platform === "win32"
|
|
154
|
+
? env.USERPROFILE || env.HOME || homedir()
|
|
155
|
+
: env.HOME || env.USERPROFILE || homedir();
|
|
156
|
+
}
|
|
157
|
+
/** `<user home>/.<name>` — the per-harness state directory every CLI keeps
|
|
158
|
+
* (`.qwen`, `.grok`, `.codex`…), Windows-correct everywhere. */
|
|
159
|
+
export function harnessHome(name, env = process.env) {
|
|
160
|
+
return join(userHome(env), `.${name}`);
|
|
161
|
+
}
|
|
147
162
|
/** Every `name` binary on the augmented PATH as absolute paths, in PATH
|
|
148
163
|
* order (first = what a bare name would run). Used by the Engines panel's
|
|
149
164
|
* "detected" dropdown and the /api/cli-candidates endpoint. A path-ish
|