openmausbot 0.1.87 → 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.
Files changed (149) hide show
  1. package/README.md +2 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-BTiuL79F.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
  3. package/dist/assets/{arc-C88Blc2v.js → arc-3RwkUjRe.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-BZsJIZrb.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CVeMe9Ah.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-Cedk3Bhx.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
  7. package/dist/assets/channel-CnRnO3tF.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-D-VRqzTB.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BChov01Y.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-C-TfwvHy.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-Db7CGj5Y.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CNvIfqKT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-C_bEp5tq.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-Coi4BOls.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CzylEuL3.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-m7p7ztaD.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BMy6o9x2.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BDQ__0_J.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-Djv4ghUb.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-BULrlGYH.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-D-jrT_hU.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-p2UhQmfL.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-DHvGWZ7q.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-ChhKDot-.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-B-T7A43R.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-CJm8ENNY.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-510tlza8.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-PkvRPHel.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-BVBVPoX3.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-BgYMVm7L.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-BGqsnc_3.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-D_-UeReH.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-D0uooONx.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
  34. package/dist/assets/index-BIMdIbaw.css +1 -0
  35. package/dist/assets/index-CHnODCr2.js +583 -0
  36. package/dist/assets/{index-zRsJERGJ.js → index-CIaHn5NS.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-B9e1z77f.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BB4vNwvD.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DpkosOTB.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DbDP4kUu.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
  41. package/dist/assets/{linear-DBJGPmjT.js → linear-C2pWPSxO.js} +1 -1
  42. package/dist/assets/{mermaid.core-DWPRi-ue.js → mermaid.core-CR-j5WQU.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-nszBbJmx.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-CWykJWu_.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-g3Da-Vnv.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-Wzrlpzgh.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-moHTFMC0.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-CwfXdFH8.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-3ZROYvnq.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-E1TBebP8.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-DHKEU5pu.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BguEj09D.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-C7IDYnhd.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-DCW-F5PW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-xBMFtWYV.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-B5nIkuH8.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DYFAhdbr.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-DeXwX6Cz.js → xychartDiagram-PMCCYNJV-BYu1Q_at.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 +3 -1
  64. package/dist-server/drivers/agents-proxy.js +277 -22
  65. package/dist-server/index.js +23150 -19947
  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 +540 -298
  71. package/dist-server/pair-cli.js +540 -298
  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/bot-attachment.js +107 -0
  76. package/dist-server/server/bot-overview.js +19 -0
  77. package/dist-server/server/cli-api-setup.js +1 -0
  78. package/dist-server/server/composio.js +297 -1
  79. package/dist-server/server/config.js +25 -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 +60 -0
  84. package/dist-server/server/contracts.js +22 -0
  85. package/dist-server/server/drivers/acp/core.js +24 -6
  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 +9 -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 +2 -16
  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 +26 -2
  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 +46 -0
  108. package/dist-server/server/index.js +1037 -209
  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 +26 -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/workspace-backup.js +40 -3
  134. package/dist-server/server/workspace-backup.worker.js +10 -0
  135. package/dist-server/shared/ask-question.js +19 -0
  136. package/dist-server/shared/credential-request.js +5 -1
  137. package/dist-server/shared/line-diff.js +23 -0
  138. package/dist-server/shared/model-request.js +1 -0
  139. package/dist-server/shared/options-card.js +51 -0
  140. package/dist-server/shared/profile-request.js +5 -4
  141. package/dist-server/shared/tightening-request.js +87 -0
  142. package/dist-server/src/lib/composer-attachments.js +25 -3
  143. package/dist-server/vps-container-mcp.js +3 -1
  144. package/dist-server/workspace-backup.worker.js +19474 -0
  145. package/package.json +1 -1
  146. package/dist/assets/channel-taCNRlUU.js +0 -1
  147. package/dist/assets/index-BwdMAQ6p.css +0 -1
  148. package/dist/assets/index-ByJennr7.js +0 -581
  149. package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +0 -8
@@ -808,6 +808,66 @@ export async function containerComputerScreenshot(runner = sh, platform = proces
808
808
  }
809
809
  const screenshotStatusCache = new Map();
810
810
  const containerMcpPath = SPAWNED_PROXIES.containerMcp;
811
+ const EXEC_DEFAULT_SECONDS = 60;
812
+ const EXEC_MAX_SECONDS = 300;
813
+ const EXEC_OUTPUT_LIMIT = 20_000;
814
+ /** Keep the start and the end: a failure message is nearly always at the end. */
815
+ export function clipExecOutput(text, limit = EXEC_OUTPUT_LIMIT) {
816
+ if (text.length <= limit)
817
+ return text;
818
+ const head = Math.floor(limit / 5);
819
+ return `${text.slice(0, head)}\n… ${text.length - limit} characters omitted …\n${text.slice(text.length - (limit - head))}`;
820
+ }
821
+ const defaultExecRunner = async (command, args, options) => {
822
+ const resolved = resolveCliSpawn(command, args);
823
+ try {
824
+ const { stdout, stderr } = await run(resolved.command, resolved.args, {
825
+ timeout: options.timeout,
826
+ encoding: "utf8",
827
+ maxBuffer: 64 * 1024 * 1024,
828
+ env: { ...process.env, PATH: augmentedPath() },
829
+ });
830
+ return { stdout, stderr, code: 0 };
831
+ }
832
+ catch (error) {
833
+ const failure = error;
834
+ // A non-zero exit still carries the output; anything else (runtime missing,
835
+ // client killed) is a real failure to run at all.
836
+ if (typeof failure.code === "number") {
837
+ return { stdout: failure.stdout ?? "", stderr: failure.stderr ?? "", code: failure.code };
838
+ }
839
+ throw Object.assign(new Error(failure.killed ? "the command client timed out" : failure.message ?? "could not run the command"), { status: 502 });
840
+ }
841
+ };
842
+ /** Run one shell command inside a Local VM as the desktop user, in its durable
843
+ * workspace, and return the exit code and text output. Bots use this instead of
844
+ * typing into a terminal window and reading screenshots. The limit is enforced
845
+ * inside the container so a runaway process is really stopped. */
846
+ export async function containerExec(target, command, options = {}) {
847
+ if (!command.trim())
848
+ throw Object.assign(new Error("command is required"), { status: 400 });
849
+ if (command.length > 20_000)
850
+ throw Object.assign(new Error("command is too long"), { status: 400 });
851
+ if (options.timeoutSeconds !== undefined && !Number.isFinite(options.timeoutSeconds)) {
852
+ throw Object.assign(new Error("timeout_seconds must be finite"), { status: 400 });
853
+ }
854
+ const runtime = options.runtime ?? (await containerRuntimeStatus()).runtime;
855
+ if (!runtime)
856
+ throw Object.assign(new Error("No container runtime is available for the Local VM"), { status: 409 });
857
+ const seconds = Math.min(Math.max(Math.floor(options.timeoutSeconds ?? EXEC_DEFAULT_SECONDS), 1), EXEC_MAX_SECONDS);
858
+ const result = await (options.exec ?? defaultExecRunner)(runtime, [
859
+ "exec",
860
+ "-u", "cua",
861
+ "-w", VM_WORKSPACE_GUEST,
862
+ "-e", "HOME=/home/cua",
863
+ "-e", `DISPLAY=${DISPLAY}`,
864
+ target.containerName,
865
+ "timeout", "-k", "5", String(seconds),
866
+ "sh", "-lc", command,
867
+ ], { timeout: (seconds + 20) * 1000 });
868
+ const timedOut = result.code === 124 || result.code === 137;
869
+ return { exitCode: result.code, stdout: clipExecOutput(result.stdout), stderr: clipExecOutput(result.stderr), timedOut };
870
+ }
811
871
  export function containerComputerMcp(runtime, control, target = SHARED_LOCAL_VM_TARGET) {
812
872
  return {
813
873
  command: process.execPath,
@@ -17,6 +17,28 @@ export function isModelVariant(value) {
17
17
  return typeof value === "string" && value.length > 0 && value.length <= 256 &&
18
18
  value.trim() === value && !/\p{Cc}/u.test(value);
19
19
  }
20
+ /** The picker label for a model id when the catalog row carries no display
21
+ * name: split on the word breaks the catalog treats as separators and
22
+ * capitalize each part ("gpt-5.4-mini" → "Gpt 5.4 Mini"). Each harness
23
+ * passes its own break class so existing labels stay byte-for-byte. */
24
+ export function titleCaseModelId(id, wordBreaks) {
25
+ return id
26
+ .split(wordBreaks)
27
+ .filter(Boolean)
28
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
29
+ .join(" ");
30
+ }
31
+ /** A picker label that carries a second facet — a display name, a provider
32
+ * host — beside the model id, unless the facet is already what the id says.
33
+ * `redundantWhen` decides what counts as "already said" and `decorate` how
34
+ * the facet is appended, so every harness's labels stay exactly what they
35
+ * were while the shape is written once. */
36
+ export function qualifiedModelLabel(id, qualifier, options = {}) {
37
+ const { redundantWhen = () => false, decorate = (facet) => ` — ${facet}` } = options;
38
+ if (!qualifier || redundantWhen(id, qualifier))
39
+ return id;
40
+ return `${id}${decorate(qualifier)}`;
41
+ }
20
42
  let eventCounter = 0;
21
43
  export const newEventId = () => `ev-${Date.now().toString(36)}-${(eventCounter++).toString(36)}`;
22
44
  export const newId = () => crypto.randomUUID();
@@ -50,6 +50,7 @@ import { commandSummary, toolDetailPreview } from "../../tool-summary.js";
50
50
  import { extractMcpImages } from "../../mcp-tool-images.js";
51
51
  import { redactSecretsInText } from "../../redact.js";
52
52
  import { recoveryPromptFor } from "../../resume-recovery.js";
53
+ import { sessionIdlePolicy } from "../session-idle.js";
53
54
  /** ACP vendors put the actionable cause in error.data while keeping the
54
55
  * JSON-RPC message generic. Only surface known text fields, never a response
55
56
  * body/config dump, and redact before bounding the displayed diagnostic. */
@@ -80,6 +81,9 @@ const INIT_TIMEOUT = envOr("OPENMAUS_ACP_INIT_TIMEOUT_MS", 300_000);
80
81
  const SESSION_CONFIG_TIMEOUT = envOr("OPENMAUS_ACP_SESSION_CONFIG_TIMEOUT_MS", 300_000); // configureSession's per-request default
81
82
  const NEW_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_NEW_SESSION_TIMEOUT_MS", 300_000);
82
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;
83
87
  // Read lazily (not at import) so a test can shorten the window. Unlike the
84
88
  // setup calls above, session/prompt legitimately streams for minutes, so a
85
89
  // wall-clock deadline would false-positive: this guard only trips when the
@@ -212,6 +216,24 @@ export function versionFromProbe(stdout, stderr) {
212
216
  const err = (stderr ?? "").trim().split(/\r\n|\n|\r/, 1)[0]?.trim() ?? "";
213
217
  return err || null;
214
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
+ }
215
237
  export function createAcpDriver(support) {
216
238
  const DRIVER_KIND = support.driverKind;
217
239
  const SOURCE = support.nativeSource;
@@ -274,11 +296,7 @@ export function createAcpDriver(support) {
274
296
  // contract prompts the live session instead of paying the handshake
275
297
  // again. An idle session closes after SESSION_IDLE_MS of quiet.
276
298
  const sessions = new Map();
277
- const configuredIdleMinimum = Number(process.env.OMB_ACP_SESSION_IDLE_MIN_MS);
278
- const sessionIdleMinimum = Number.isFinite(configuredIdleMinimum) && configuredIdleMinimum > 0
279
- ? configuredIdleMinimum
280
- : 10_000;
281
- 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");
282
300
  const closeSession = (threadId, why) => {
283
301
  const session = sessions.get(threadId);
284
302
  if (!session || session.closing)
@@ -1320,7 +1338,7 @@ export function createAcpDriver(support) {
1320
1338
  let pendingSplitReceipt = null;
1321
1339
  if (halves.stable !== null) {
1322
1340
  const receiptKey = JSON.stringify([threadId, sessionId]);
1323
- const composed = splitSessionPrompt(halves.stable, halves.volatile, readPromptSplitReceipt(DRIVER_KIND, receiptKey), promptTurn.system, promptTurn.text, Boolean(turn.mentionTurn));
1341
+ const composed = splitSessionPrompt(halves.stable, halves.volatile, readPromptSplitReceipt(DRIVER_KIND, receiptKey), promptTurn.system, promptTurn.text, Boolean(turn.mentionTurn), ACP_PROMPT_RE_ANCHOR_TURNS);
1324
1342
  promptInput = { ...promptTurn, system: "", text: composed.text };
1325
1343
  pendingSplitReceipt = { key: receiptKey, receipt: composed.receipt };
1326
1344
  }
@@ -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
- if (explicit?.trim())
90
- return explicit.trim();
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.HOME || homedir();
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(factoryHome(env), ".factory");
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(factoryHome(env), ".factory", "settings.json"), "utf8"));
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(homedir(), ".gemini", "oauth_creds.json"), "utf8"));
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(homedir(), ".gemini", "oauth_creds.json")) && liveOauthCredential()));
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(grokHome(env), "config.toml");
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(grokHome(env), "config.toml");
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(homedir(), ".grok", "auth.json")),
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 = hermesHome(env);
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 = hermesHome(env);
200
+ const dir = env.HERMES_HOME || harnessHome("hermes", env);
203
201
  let secrets = "";
204
202
  try {
205
203
  secrets = readFileSync(join(dir, ".env"), "utf8");
@@ -6,6 +6,7 @@ import { homedir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import { decodeInjectId, hostApiKey, localHost, mergeLocalInject } from "../local-inject.js";
8
8
  import { createAcpDriver } from "./core.js";
9
+ import { titleCaseModelId } from "../../contracts.js";
9
10
  import { execCli } from "../../procs.js";
10
11
  const STATIC_MODELS = {
11
12
  default: "opencode/x-preview-f-free",
@@ -21,11 +22,7 @@ let lastSuccessfulCatalog = null;
21
22
  const MODEL_PROBE_TTL_MS = 30_000;
22
23
  const modelProbeCache = new Map();
23
24
  function labelForModel(id) {
24
- return id
25
- .split(/[-_.]+/g)
26
- .filter(Boolean)
27
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
28
- .join(" ");
25
+ return titleCaseModelId(id, /[-_.]+/);
29
26
  }
30
27
  function providerLabel(id) {
31
28
  if (id === "opencode")
@@ -3,30 +3,20 @@
3
3
  // Custom and are written into ~/.qwen/settings.json modelProviders.
4
4
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { createHash } from "node:crypto";
6
- import { homedir } from "node:os";
7
6
  import { join } from "node:path";
7
+ import { qualifiedModelLabel } from "../../contracts.js";
8
+ import { harnessHome } from "../../env-path.js";
8
9
  import { decodeInjectId, hostApiKey, localHost, mergeLocalInject } from "../local-inject.js";
9
10
  import { createAcpDriver } from "./core.js";
10
11
  const EMPTY = { default: "", options: [] };
11
- function qwenHome(env) {
12
- const home = process.platform === "win32"
13
- ? env.USERPROFILE || env.HOME || homedir()
14
- : env.HOME || env.USERPROFILE || homedir();
15
- return join(home, ".qwen");
16
- }
17
12
  function isRecord(value) {
18
13
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
19
14
  }
20
- function qwenModelLabel(id, name) {
21
- if (!name || name.toLocaleLowerCase().endsWith(id.toLocaleLowerCase()))
22
- return id;
23
- return `${id} — ${name}`;
24
- }
25
15
  const PROTOCOLS = new Set(["openai", "anthropic", "gemini", "vertex-ai"]);
26
16
  function readQwenRoutes(env) {
27
17
  let settings;
28
18
  try {
29
- settings = JSON.parse(readFileSync(join(qwenHome(env), "settings.json"), "utf8"));
19
+ settings = JSON.parse(readFileSync(join(harnessHome("qwen", env), "settings.json"), "utf8"));
30
20
  }
31
21
  catch {
32
22
  return [];
@@ -102,7 +92,11 @@ function readQwenRoutes(env) {
102
92
  /** Public metadata only. Use Qwen's provider-qualified ACP selectors, not raw model IDs. */
103
93
  export function readQwenModelCatalog(env = process.env) {
104
94
  const options = readQwenRoutes(env).map(({ id, model, label, provider }) => ({
105
- id, label: qwenModelLabel(model, label), custom: true, provider,
95
+ id,
96
+ label: qualifiedModelLabel(model, label, {
97
+ redundantWhen: (id, name) => name.toLocaleLowerCase().endsWith(id.toLocaleLowerCase()),
98
+ }),
99
+ custom: true, provider,
106
100
  }));
107
101
  return { default: options[0]?.id ?? "", options };
108
102
  }
@@ -117,7 +111,7 @@ export function ensureQwenInjectModel(modelId, env = process.env) {
117
111
  const host = localHost(inject.host);
118
112
  if (!host)
119
113
  return modelId;
120
- const dir = qwenHome(env);
114
+ const dir = harnessHome("qwen", env);
121
115
  mkdirSync(dir, { recursive: true });
122
116
  const path = join(dir, "settings.json");
123
117
  let settings = {};