openmausbot 0.1.87 → 0.1.89

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.
Files changed (150) hide show
  1. package/README.md +2 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-BTiuL79F.js → abnfDiagram-O67JEVCF-BwaMaZEm.js} +1 -1
  3. package/dist/assets/{arc-C88Blc2v.js → arc-Be-8GKxd.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-BZsJIZrb.js → architectureDiagram-NJMV4G6O-Dp_U2Kv1.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CVeMe9Ah.js → blockDiagram-BEXU5L5S-DQjUjYUk.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-Cedk3Bhx.js → c4Diagram-YGBWAQC7-Bd3aE4Kc.js} +1 -1
  7. package/dist/assets/channel-DqY_gMLg.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-D-VRqzTB.js → chunk-2Q5K7J3B-BijjTBeJ.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BChov01Y.js → chunk-3FUC2YCW-CMGdwvzW.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-C-TfwvHy.js → chunk-5VM5RSS4-0G09BfsZ.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-Db7CGj5Y.js → chunk-GTNCS2PH-FOp3vSHe.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CNvIfqKT.js → chunk-GWA4HPMP-Dv-20VIn.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-C_bEp5tq.js → chunk-JWPE2WC7-CRgrdFcz.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-Coi4BOls.js → chunk-NETBCI7D-3hP4f7QB.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CzylEuL3.js → chunk-WEXAMYUT-CG98IjlG.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-m7p7ztaD.js → chunk-XXDRQBXY-BrSTC9xG.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BMy6o9x2.js → classDiagram-v2-NBCMYWYE-rpQQQDFX.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BDQ__0_J.js → cose-bilkent-JH36ORCC-G6RSafHA.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-Djv4ghUb.js → cynefin-OW5HDTMX-CXjv-IM2.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-BULrlGYH.js → cynefinDiagram-VND7K2PF-Dw07MR9f.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-D-jrT_hU.js → dagre-6A5THRUB-BwPnlTg2.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-p2UhQmfL.js → diagram-22UHCM2B-Cm6xcMiz.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-DHvGWZ7q.js → diagram-3UASUU5V-CDpogPbE.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-ChhKDot-.js → diagram-ATOU4E4O-BV8z3Pnc.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-B-T7A43R.js → diagram-CDSNMT55-CKCQhOOC.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-CJm8ENNY.js → diagram-MLGK6HIB-FGvJg69C.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-510tlza8.js → diagram-MPIPVDR6-CwATvgnS.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-PkvRPHel.js → ebnfDiagram-ZINNZB2B-CH_c3SSC.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-BVBVPoX3.js → elk-276RUBZZ-3guZi71R.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-BgYMVm7L.js → erDiagram-OPXOYQCR-BMHphR2n.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-BGqsnc_3.js → flowDiagram-KWPJA3E3-Vm5DkW5x.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-D_-UeReH.js → ganttDiagram-FUAMR5RP-DO-1ct07.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-D0uooONx.js → gitGraphDiagram-X574FWY7-nxeksEIN.js} +1 -1
  34. package/dist/assets/index-BIMdIbaw.css +1 -0
  35. package/dist/assets/index-BVPdbq2r.js +583 -0
  36. package/dist/assets/{index-zRsJERGJ.js → index-Oxk9x45i.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-B9e1z77f.js → infoDiagram-VRGFBTTK-Dkcvr0p7.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BB4vNwvD.js → ishikawaDiagram-OU5B5YK6-BwVibkoO.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DpkosOTB.js → journeyDiagram-ZHPQQLJL-ByiBZqgG.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DbDP4kUu.js → kanban-definition-PNTS6WVX-qqjgXBF3.js} +1 -1
  41. package/dist/assets/{linear-DBJGPmjT.js → linear-CtJnKe6A.js} +1 -1
  42. package/dist/assets/{mermaid.core-DWPRi-ue.js → mermaid.core-BEiDklrY.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-nszBbJmx.js → mindmap-definition-NLK3R4M7-DfIYHia9.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-CWykJWu_.js → pegDiagram-GJSIUBJH-CYyRqa1a.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-g3Da-Vnv.js → pieDiagram-5QR66LMP-hOToy_zH.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-Wzrlpzgh.js → quadrantDiagram-O4NWA36T-DKAsNN9I.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-moHTFMC0.js → railroadDiagram-XR7U4H2S-BSA-d8JU.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-CwfXdFH8.js → requirementDiagram-PLB6GJNP-CSmlvb-p.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-3ZROYvnq.js → sankeyDiagram-IPEJSGJF-CD5F6fnG.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-E1TBebP8.js → sequenceDiagram-PO4LG4MO-JUq5sx6d.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-DHKEU5pu.js → sizeCapture-INFHLROL-BjIS-Stk.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BguEj09D.js → stateDiagram-v2-GCMORJYK-DklkUxOg.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-C7IDYnhd.js → swimlanes-2SLR337P-CsjIU6be.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-DCW-F5PW.js → timeline-definition-EJHVYXUP-BkFqK7AB.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-xBMFtWYV.js → usecaseDiagram-POWQR4AR-70gJB1EU.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-B5nIkuH8.js → vennDiagram-UO4OBE2U-83Tm62dN.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DYFAhdbr.js → wardleyDiagram-VNRHLVJA-tTmseoX9.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-DeXwX6Cz.js → xychartDiagram-PMCCYNJV-R1LBA55a.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/companion/src/routes.js +8 -1
  62. package/dist-server/connector-proxy.js +105 -1
  63. package/dist-server/container-mcp.js +11 -1
  64. package/dist-server/drivers/agents-proxy.js +277 -22
  65. package/dist-server/index.js +23722 -20272
  66. package/dist-server/local-computer-proxy.js +2 -0
  67. package/dist-server/mcp-gate.js +2 -0
  68. package/dist-server/mcp-server.js +5 -3
  69. package/dist-server/message-search.worker.js +52 -0
  70. package/dist-server/openmausbot.js +755 -404
  71. package/dist-server/pair-cli.js +755 -404
  72. package/dist-server/server/admission.js +81 -0
  73. package/dist-server/server/aside-queue.js +294 -0
  74. package/dist-server/server/attachments.js +43 -1
  75. package/dist-server/server/automatic-recovery.js +15 -0
  76. package/dist-server/server/bot-attachment.js +107 -0
  77. package/dist-server/server/bot-overview.js +19 -0
  78. package/dist-server/server/composio.js +297 -1
  79. package/dist-server/server/config.js +34 -3
  80. package/dist-server/server/connector-advertisement.js +165 -0
  81. package/dist-server/server/connector-proxy.js +14 -2
  82. package/dist-server/server/connector-verdict.js +31 -4
  83. package/dist-server/server/container-computer.js +70 -0
  84. package/dist-server/server/contracts.js +32 -0
  85. package/dist-server/server/drivers/acp/core.js +148 -23
  86. package/dist-server/server/drivers/acp/cursor.js +3 -7
  87. package/dist-server/server/drivers/acp/droid.js +4 -7
  88. package/dist-server/server/drivers/acp/gemini.js +4 -4
  89. package/dist-server/server/drivers/acp/grok.js +4 -9
  90. package/dist-server/server/drivers/acp/hermes.js +3 -5
  91. package/dist-server/server/drivers/acp/opencode-go.js +2 -5
  92. package/dist-server/server/drivers/acp/qwen.js +12 -15
  93. package/dist-server/server/drivers/agents-call.js +137 -10
  94. package/dist-server/server/drivers/agents-catalog.js +100 -6
  95. package/dist-server/server/drivers/boxagent.js +3 -17
  96. package/dist-server/server/drivers/claude.js +31 -17
  97. package/dist-server/server/drivers/codex-catalog.js +14 -6
  98. package/dist-server/server/drivers/codex-identity.js +2 -1
  99. package/dist-server/server/drivers/codex-release.js +48 -0
  100. package/dist-server/server/drivers/codex.js +7 -25
  101. package/dist-server/server/drivers/openai-chat.js +31 -3
  102. package/dist-server/server/drivers/pi.js +237 -30
  103. package/dist-server/server/drivers/prompt-split.js +32 -9
  104. package/dist-server/server/drivers/session-idle.js +29 -0
  105. package/dist-server/server/env-path.js +15 -0
  106. package/dist-server/server/guarded-requests.js +2 -2
  107. package/dist-server/server/harness-capabilities.js +61 -0
  108. package/dist-server/server/index.js +1173 -262
  109. package/dist-server/server/message-db.js +22 -42
  110. package/dist-server/server/message-file.js +9 -1
  111. package/dist-server/server/message-search-query.js +43 -0
  112. package/dist-server/server/message-search-worker.js +80 -0
  113. package/dist-server/server/message-search.worker.js +21 -0
  114. package/dist-server/server/model-requests.js +248 -0
  115. package/dist-server/server/options-card.js +21 -0
  116. package/dist-server/server/org-library.js +15 -1
  117. package/dist-server/server/peer-delivery.js +60 -0
  118. package/dist-server/server/peer-roster.js +57 -0
  119. package/dist-server/server/presets.js +32 -32
  120. package/dist-server/server/profile-requests.js +66 -39
  121. package/dist-server/server/profile-revision.js +9 -1
  122. package/dist-server/server/profile-versions.js +6 -3
  123. package/dist-server/server/routine-requests.js +29 -7
  124. package/dist-server/server/skill-fetch.js +72 -14
  125. package/dist-server/server/skills.js +1 -0
  126. package/dist-server/server/store.js +32 -2
  127. package/dist-server/server/system-prompt.js +37 -2
  128. package/dist-server/server/team-setup-requests.js +124 -23
  129. package/dist-server/server/thread-events.js +3 -1
  130. package/dist-server/server/tightening-requests.js +405 -0
  131. package/dist-server/server/tts/chatterbox.js +27 -5
  132. package/dist-server/server/usage-ledger.js +7 -1
  133. package/dist-server/server/webhooks.js +21 -3
  134. package/dist-server/server/workspace-backup.js +40 -3
  135. package/dist-server/server/workspace-backup.worker.js +10 -0
  136. package/dist-server/shared/ask-question.js +19 -0
  137. package/dist-server/shared/credential-request.js +5 -1
  138. package/dist-server/shared/line-diff.js +23 -0
  139. package/dist-server/shared/model-request.js +1 -0
  140. package/dist-server/shared/options-card.js +51 -0
  141. package/dist-server/shared/profile-request.js +5 -4
  142. package/dist-server/shared/tightening-request.js +87 -0
  143. package/dist-server/src/lib/composer-attachments.js +42 -3
  144. package/dist-server/vps-container-mcp.js +11 -1
  145. package/dist-server/workspace-backup.worker.js +19482 -0
  146. package/package.json +1 -1
  147. package/dist/assets/channel-taCNRlUU.js +0 -1
  148. package/dist/assets/index-BwdMAQ6p.css +0 -1
  149. package/dist/assets/index-ByJennr7.js +0 -581
  150. package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +0 -8
@@ -26,6 +26,7 @@ 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";
@@ -37,6 +38,21 @@ const DRIVER_KIND = "piAgent";
37
38
  const PI_ARGS = ["--mode", "rpc", "--no-session"];
38
39
  const PI_MODEL_UPDATE_ARGS = ["update", "--models", "--no-approve"];
39
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
+ }
40
56
  function readPiPromptImages(turn) {
41
57
  return (turn.images ?? []).map((image) => ({
42
58
  type: "image",
@@ -431,6 +447,13 @@ export const PiDriver = {
431
447
  // closure (send, child) alive until it fires.
432
448
  const askTimers = new Set();
433
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;
434
457
  // pi's RPC surface accepts image content directly. Read before spawning
435
458
  // so an attachment that disappeared produces one clear dispatch error
436
459
  // instead of starting a child that can never receive its prompt.
@@ -493,8 +516,17 @@ export const PiDriver = {
493
516
  })();
494
517
  let buf = "";
495
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;
496
522
  // resolve one-shot RPC responses (new_session / switch_session / set_model)
497
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;
498
530
  const rejectWaiters = (err) => {
499
531
  for (const waiter of responseWaiters.values()) {
500
532
  clearTimeout(waiter.timer);
@@ -502,13 +534,13 @@ export const PiDriver = {
502
534
  }
503
535
  responseWaiters.clear();
504
536
  };
505
- const awaitResponse = (command, timeoutMs = 20_000) => new Promise((resolve, reject) => {
537
+ const awaitResponse = (command, timeoutMs = 20_000, key = command) => new Promise((resolve, reject) => {
506
538
  const timer = setTimeout(() => {
507
- responseWaiters.delete(command);
539
+ responseWaiters.delete(key);
508
540
  reject(new Error(`pi ${command} timed out`));
509
541
  }, timeoutMs);
510
542
  timer.unref?.();
511
- responseWaiters.set(command, { resolve, reject, timer });
543
+ responseWaiters.set(key, { resolve, reject, timer });
512
544
  });
513
545
  child.stdin.on("error", () => rejectWaiters(new Error("pi stdin closed")));
514
546
  const send = (obj) => {
@@ -527,6 +559,10 @@ export const PiDriver = {
527
559
  if (settled)
528
560
  return;
529
561
  settled = true;
562
+ if (agentEndTimer) {
563
+ clearTimeout(agentEndTimer);
564
+ agentEndTimer = null;
565
+ }
530
566
  // The turn is over: drop unanswered asks so their 15-minute
531
567
  // fail-safe timers are cancelled outright instead of no-oping on a
532
568
  // dead child while holding the ask closure alive.
@@ -564,6 +600,23 @@ export const PiDriver = {
564
600
  }
565
601
  active.delete(threadId);
566
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
+ };
567
620
  const stop = () => {
568
621
  try {
569
622
  send({ type: "abort" });
@@ -579,19 +632,53 @@ export const PiDriver = {
579
632
  }
580
633
  settle(true, "cancelled");
581
634
  };
582
- active.set(threadId, { stop, turnId, pending, child });
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 });
583
666
  const onEvent = (evt) => {
584
667
  appendNative(threadId, { dir: "in", source: "pi.rpc", msg: evt });
585
668
  switch (evt.type) {
586
669
  case "response": {
587
- if (evt.command && responseWaiters.has(evt.command)) {
588
- const waiter = responseWaiters.get(evt.command);
589
- responseWaiters.delete(evt.command);
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);
590
677
  clearTimeout(waiter.timer);
591
678
  if (evt.success)
592
679
  waiter.resolve(evt.data);
593
680
  else
594
- waiter.reject(new Error(`pi ${evt.command} failed`));
681
+ waiter.reject(new PiRpcRefusalError(`pi ${evt.command} failed`));
595
682
  }
596
683
  return;
597
684
  }
@@ -715,24 +802,81 @@ export const PiDriver = {
715
802
  }
716
803
  return;
717
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
+ }
718
823
  case "turn_end":
719
- case "agent_end": {
720
- const sr = evt.message?.stopReason;
721
- // toolUse means pi ran a tool and auto-continues next turn to
722
- // answer — settling now would drop the final reply.
723
- if (sr === "toolUse" || sr === "tool_use" || sr === "tool_calls")
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?.();
724
849
  return;
725
- const usage = evt.usage ?? evt.message?.usage;
726
- if (sr === "error" || sr === "failed") {
727
- emit({
728
- ...base(threadId, turnId),
729
- type: "runtime.error",
730
- message: String(evt.message?.errorMessage ?? "pi turn failed").slice(0, 2_000),
731
- });
732
- settle(false, "failed", usage);
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)
733
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;
734
878
  }
735
- settle(true, sr === "cancelled" || sr === "aborted" ? "cancelled" : "end_turn", usage);
879
+ finishRun();
736
880
  return;
737
881
  }
738
882
  default:
@@ -774,6 +918,7 @@ export const PiDriver = {
774
918
  // sessionFile, which switch_session expects as `sessionPath`.
775
919
  const sessionPath = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
776
920
  let sessionFile = sessionPath;
921
+ let sessionReady = false;
777
922
  try {
778
923
  const command = sessionPath ? "switch_session" : "new_session";
779
924
  const hsPromise = awaitResponse(command);
@@ -781,6 +926,7 @@ export const PiDriver = {
781
926
  const hs = (await hsPromise);
782
927
  if (hs?.sessionFile)
783
928
  sessionFile = hs.sessionFile;
929
+ sessionReady = true;
784
930
  emit({
785
931
  ...base(threadId, turnId),
786
932
  type: "session.started",
@@ -816,17 +962,71 @@ export const PiDriver = {
816
962
  /* keep going on the engine default */
817
963
  }
818
964
  }
819
- // pi compacts long sessions by summarizing older user messages, and
820
- // this driver delivers the prompt as the leading user message: a
821
- // receipt-based split would let a compacted session keep running
822
- // bare, without its standing instructions. Re-deliver the full prompt
823
- // every turn until pi exposes a compaction signal the harness can
824
- // watch (its extension API has session_before_compact).
825
- const message = turn.system ? `${turn.system}\n\n${turn.text}` : turn.text;
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
826
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);
827
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
+ });
828
1023
  }
829
- 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
+ });
830
1030
  settle(false);
831
1031
  }
832
1032
  return { turnId };
@@ -899,6 +1099,9 @@ export const PiDriver = {
899
1099
  // xhigh/max only land on models that expose them; pi rejects an
900
1100
  // unsupported level and the turn keeps the engine default.
901
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,
902
1105
  },
903
1106
  sendTurn,
904
1107
  interruptTurn: async (threadId) => active.get(threadId)?.stop(),
@@ -918,6 +1121,10 @@ export const PiDriver = {
918
1121
  });
919
1122
  return decision.behavior === "allow" ? "allowed-once" : decision.behavior === "answer" ? "answered" : "rejected";
920
1123
  },
1124
+ steer: async (threadId, text) => {
1125
+ const entry = active.get(threadId);
1126
+ return entry?.steer ? await entry.steer(text) : "refused";
1127
+ },
921
1128
  hasSession: (threadId) => active.has(threadId),
922
1129
  stopAll: async () => {
923
1130
  for (const { stop } of active.values())
@@ -5,7 +5,7 @@
5
5
  // work, recent work) legitimately changes mid-conversation and reaches
6
6
  // the model inside the turn that changed it, after the cacheable prefix.
7
7
  import { createHash } from "node:crypto";
8
- import { mkdirSync, readFileSync } from "node:fs";
8
+ import { mkdirSync, readFileSync, rmSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { DATA_DIR } from "../config.js";
11
11
  import { writeFileAtomic } from "../atomic.js";
@@ -53,7 +53,11 @@ export function readPromptSplitReceipt(scope, key) {
53
53
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
54
54
  const record = raw;
55
55
  if (typeof record.stable === "string" && typeof record.volatile === "string") {
56
- return { stable: record.stable, volatile: record.volatile };
56
+ return {
57
+ stable: record.stable,
58
+ volatile: record.volatile,
59
+ ...(typeof record.turnsSinceFull === "number" ? { turnsSinceFull: record.turnsSinceFull } : {}),
60
+ };
57
61
  }
58
62
  }
59
63
  }
@@ -66,6 +70,18 @@ export function writePromptSplitReceipt(scope, key, receipt) {
66
70
  mkdirSync(join(DATA_DIR, "prompt-split"), { recursive: true });
67
71
  writeFileAtomic(receiptPath(scope, key), JSON.stringify(receipt), { mode: 0o600 });
68
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
+ }
69
85
  /** Compose the prompt for a session that persists its own history (ACP
70
86
  * agents, pi): the full system block rides only when this native session
71
87
  * has not carried it - or carried a different one, which re-delivers the
@@ -74,14 +90,21 @@ export function writePromptSplitReceipt(scope, key, receipt) {
74
90
  * through bare, so an ordinary memory write neither appends a second copy
75
91
  * of the prompt to the session nor re-prices its cached prefix.
76
92
  * perTurnVolatile marks a turn whose volatile half describes this very
77
- * turn (a mention): its note is delivered even when the text is unchanged. */
78
- export function splitSessionPrompt(stable, volatile, previous, fullSystem, text, perTurnVolatile = false) {
79
- const receipt = promptSplitFingerprints(stable, volatile);
80
- if (previous === null || previous.stable !== receipt.stable) {
81
- return { text: fullSystem ? fullSystem + "\n\n" + text : text, receipt };
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 } };
82
105
  }
83
- const note = previous.volatile === receipt.volatile && !perTurnVolatile
106
+ const note = previous.volatile === fingerprints.volatile && !perTurnVolatile
84
107
  ? ""
85
108
  : volatileContextNote(volatile, previous.volatile !== EMPTY_FINGERPRINT);
86
- return { text: withContextNote(note, text), receipt };
109
+ return { text: withContextNote(note, text), receipt: { ...fingerprints, turnsSinceFull: turnsSinceFull + 1 } };
87
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
@@ -54,7 +54,7 @@ export function requestSourceForCard(messages, messageId) {
54
54
  return undefined;
55
55
  }
56
56
  export function requestNeedsInput(message) {
57
- return Boolean((message.card?.requestId && !message.card.answered && !message.card.dismissed) ||
57
+ return Boolean((message.card?.requestId && !message.card.answered && !message.card.dismissed && !message.card.expired) ||
58
58
  (message.connector && !message.connector.dismissed && !message.connector.resumed && message.connector.status !== "connected") ||
59
- (message.secret && !message.secret.provided && !message.secret.dismissed));
59
+ (message.secret && !message.secret.provided && !message.secret.dismissed && !message.secret.superseded));
60
60
  }
@@ -0,0 +1,61 @@
1
+ // What a bot gains or loses when its default engine (instanceId, hence
2
+ // driver) changes. Two per-driver facts decide it: whether the driver mounts
3
+ // the agents tool catalog at all (adapter capabilities.agentsMcp — a bot on
4
+ // a driver without it is never even told about the team tools), and which
5
+ // approval levels the destination driver implements. Turn-scoped catalog
6
+ // flags are identical on both sides of a switch, so the comparison below
7
+ // uses one envelope profile and lets the per-driver capability decide the
8
+ // delta; the tool names themselves come from the real catalog, so this
9
+ // never drifts from what a turn is actually shown.
10
+ import { APPROVAL_MODES, supportsApprovalMode as driverKindSupportsApprovalMode } from "../shared/approval-mode.js";
11
+ import { availableTools } from "./drivers/agents-catalog.js";
12
+ /** The widest turn the harness can build: every optional integration on and
13
+ * a coordinating (room) turn, where the room-only coordination tools exist.
14
+ * Both sides of a comparison get the same profile; only the driver's own
15
+ * capability can differ. */
16
+ const ENVELOPE = {
17
+ externalRuntime: false,
18
+ coordinating: true,
19
+ ownThreadCreation: true,
20
+ skillAuthoring: true,
21
+ sharedComputers: true,
22
+ voiceNotes: true,
23
+ botId: "",
24
+ };
25
+ /** Extra card lines for an engine switch, empty when nothing changes. */
26
+ export function harnessCapabilityLines(from, to) {
27
+ if (!from || !to)
28
+ return [];
29
+ const lines = [];
30
+ if (from.agentsMcp !== to.agentsMcp) {
31
+ const names = availableTools(ENVELOPE).map((tool) => tool.name);
32
+ const coordination = ["coordinate_bots", "ask_bot", "delegate_bot"].filter((name) => names.includes(name));
33
+ lines.push(to.agentsMcp
34
+ ? "Gains peer coordination and the other team tools: " + names.length + " tools including " + coordination.join(", ") + "."
35
+ : "Loses peer coordination and every team tool: " + names.length + " tools including " + coordination.join(", ") + " go away.");
36
+ }
37
+ if (from.driverKind !== to.driverKind) {
38
+ const lost = APPROVAL_MODES.filter((mode) => driverKindSupportsApprovalMode(from.driverKind, mode) && !driverKindSupportsApprovalMode(to.driverKind, mode));
39
+ const gained = APPROVAL_MODES.filter((mode) => !driverKindSupportsApprovalMode(from.driverKind, mode) && driverKindSupportsApprovalMode(to.driverKind, mode));
40
+ if (lost.length)
41
+ lines.push("Loses approval levels: " + lost.join(", ") + ".");
42
+ if (gained.length)
43
+ lines.push("Gains approval levels: " + gained.join(", ") + ".");
44
+ }
45
+ return lines;
46
+ }
47
+ /** Binds the registry to the approval-support check so each gate passes its
48
+ * model selection — or a driver view it already holds — and stops
49
+ * unwrapping registry.cliTarget(...)?.driverKind at every call site. The
50
+ * lookup prefers shadow state, so a mid-reconfig instance answers with the
51
+ * driver it would actually run. */
52
+ export function createApprovalModeSupport(registry) {
53
+ return function supportsApprovalMode(target, mode) {
54
+ const driverKind = target === undefined
55
+ ? undefined
56
+ : "driverKind" in target
57
+ ? target.driverKind
58
+ : registry.cliTarget(target.instanceId)?.driverKind;
59
+ return driverKindSupportsApprovalMode(driverKind, mode);
60
+ };
61
+ }