openmausbot 0.1.57 → 0.1.59

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 (30) hide show
  1. package/dist/assets/index-Cvk0KgFW.js +264 -0
  2. package/dist/assets/{index-jWqt5dmY.js → index-D914UwuG.js} +1 -1
  3. package/dist/assets/index-oPMv6a_k.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/dist-server/container-mcp.js +1 -2
  6. package/dist-server/index.js +1089 -825
  7. package/dist-server/openmausbot.js +15219 -305
  8. package/dist-server/pair-cli.js +15219 -305
  9. package/dist-server/proxy-paths.js +0 -1
  10. package/dist-server/server/auto-approve.js +18 -4
  11. package/dist-server/server/browser-engine-release.js +44 -0
  12. package/dist-server/server/browser-engine.js +216 -0
  13. package/dist-server/server/cli.js +55 -1
  14. package/dist-server/server/composio.js +43 -21
  15. package/dist-server/server/container-computer.js +32 -7
  16. package/dist-server/server/drivers/antigravity-acp.js +61 -11
  17. package/dist-server/server/drivers/antigravity-runtime.js +2 -2
  18. package/dist-server/server/drivers/antigravity.js +23 -2
  19. package/dist-server/server/drivers/codex-instructions.js +66 -0
  20. package/dist-server/server/drivers/codex.js +84 -49
  21. package/dist-server/server/index.js +198 -183
  22. package/dist-server/server/procs.js +34 -22
  23. package/dist-server/server/proxy-paths.js +0 -1
  24. package/dist-server/vps-container-mcp.js +1 -2
  25. package/package.json +1 -1
  26. package/dist/assets/index-B433sv8K.css +0 -1
  27. package/dist/assets/index-DSh8AIUy.js +0 -264
  28. package/dist-server/drivers/browser-proxy.js +0 -15037
  29. package/dist-server/server/browser-connection.js +0 -187
  30. package/dist-server/server/drivers/browser-proxy.js +0 -520
@@ -48,6 +48,24 @@ function takeLines(buffer) {
48
48
  const rest = parts.pop() ?? "";
49
49
  return { lines: parts.map((line) => line.replace(/\r$/u, "")), rest };
50
50
  }
51
+ /** Only fixed, allowlisted startup hints leave stderr. Native output can
52
+ * contain OAuth codes and credentials in arbitrary formats, so generic
53
+ * text redaction is not enough to safely echo a diagnostic tail. */
54
+ function startupHint(line) {
55
+ if (/no space left|not enough (?:space|disk)|disk (?:is )?full|winerror\s*112/iu.test(line)) {
56
+ return "The runtime reported insufficient disk space while starting.";
57
+ }
58
+ if (/failed to (?:extract|load (?:python|embedded python))|could not load python/iu.test(line)) {
59
+ return "The runtime reported a problem unpacking or loading its bundled Python runtime.";
60
+ }
61
+ if (/permission denied|access is denied|winerror\s*5\b/iu.test(line)) {
62
+ return "The runtime reported a file or process permission failure.";
63
+ }
64
+ if (/address family not supported|failed to create.*(?:socket|listener)|failed to bind/iu.test(line)) {
65
+ return "The runtime reported a local network listener failure.";
66
+ }
67
+ return undefined;
68
+ }
51
69
  /** The sign-in link in a server output line, or null for anything else.
52
70
  * Google announces it two ways and both land on stderr: it hands the link to
53
71
  * $BROWSER — our helper re-emits it JSON-encoded behind a private marker — and
@@ -125,6 +143,10 @@ export class AntigravityAcpClient {
125
143
  pending = new Map();
126
144
  buffer = "";
127
145
  diagnosticBuffer = "";
146
+ initializationComplete = false;
147
+ startupOutputBytes = 0;
148
+ startupDiagnosticBytes = 0;
149
+ nativeStartupHint;
128
150
  closed = false;
129
151
  onAuthorizationUrl;
130
152
  /** Settles once the runtime process is gone. On Windows a running
@@ -136,16 +158,17 @@ export class AntigravityAcpClient {
136
158
  this.child = spawnCli(runtime.executablePath, process.platform === "linux" ? ["--uid="] : [], { cwd, env: profile.environment, stdio: ["pipe", "pipe", "pipe"] });
137
159
  this.child.stdout.setEncoding("utf8");
138
160
  this.child.stdout.on("data", (chunk) => this.consume(chunk));
139
- // Google announces the sign-in link on stderr, not stdout. Scan for that
140
- // one line and drop every other byte: stderr also carries authorization
141
- // codes, so nothing else is retained or surfaced.
161
+ // Keep only sign-in announcements and fixed startup failure categories;
162
+ // never surface raw stderr, which can contain authorization codes.
142
163
  this.child.stderr.setEncoding("utf8");
143
164
  this.child.stderr.on("data", (chunk) => this.consumeDiagnostics(chunk));
144
165
  this.child.once("error", (error) => this.failAll(error));
145
166
  this.exited = new Promise((resolve) => {
146
- this.child.once("close", (code) => {
167
+ this.child.once("close", (code, signal) => {
168
+ this.noteStartupDiagnostic(this.diagnosticBuffer);
169
+ this.diagnosticBuffer = "";
147
170
  if (!this.closed)
148
- this.failAll(new Error(`Antigravity ACP exited ${code ?? "unexpectedly"}.`));
171
+ this.failAll(new Error(`Antigravity ACP exited ${code ?? signal ?? "unexpectedly"}.${this.nativeStartupHint ? ` ${this.nativeStartupHint}` : ""}`));
149
172
  resolve();
150
173
  });
151
174
  // Failed spawns also emit `close`. An `error` alone can instead mean
@@ -153,6 +176,8 @@ export class AntigravityAcpClient {
153
176
  });
154
177
  }
155
178
  consume(chunk) {
179
+ if (!this.initializationComplete)
180
+ this.startupOutputBytes += Buffer.byteLength(chunk);
156
181
  this.buffer += chunk;
157
182
  if (Buffer.byteLength(this.buffer) > MAX_PROTOCOL_LINE_BYTES) {
158
183
  this.failAll(new Error("Antigravity sent a protocol line that is too large."));
@@ -206,11 +231,19 @@ export class AntigravityAcpClient {
206
231
  * and released immediately and the tail is capped, so no authorization code
207
232
  * is ever held. Draining also keeps the pipe from stalling the server. */
208
233
  consumeDiagnostics(chunk) {
234
+ if (!this.initializationComplete)
235
+ this.startupDiagnosticBytes += Buffer.byteLength(chunk);
209
236
  const { lines, rest } = takeLines(this.diagnosticBuffer + chunk);
210
237
  // A partial line already longer than any legal link cannot become one.
211
238
  this.diagnosticBuffer = rest.length > MAX_DIAGNOSTIC_LINE_CHARS ? "" : rest;
212
- for (const line of lines)
213
- this.announceAuthorizationUrl(line);
239
+ for (const line of lines) {
240
+ if (!this.announceAuthorizationUrl(line))
241
+ this.noteStartupDiagnostic(line);
242
+ }
243
+ }
244
+ noteStartupDiagnostic(line) {
245
+ if (!this.initializationComplete)
246
+ this.nativeStartupHint ??= startupHint(line);
214
247
  }
215
248
  failAll(error) {
216
249
  for (const pending of this.pending.values()) {
@@ -227,7 +260,16 @@ export class AntigravityAcpClient {
227
260
  return new Promise((resolveRequest, reject) => {
228
261
  const timer = setTimeout(() => {
229
262
  this.pending.delete(id);
230
- reject(new Error(`${method} timed out.`));
263
+ if (method === "initialize") {
264
+ this.noteStartupDiagnostic(this.diagnosticBuffer);
265
+ reject(new Error(`Antigravity initialization timed out after ${Math.ceil(timeoutMs / 1_000)} seconds (${process.platform}-${process.arch}). ` +
266
+ "The executable was found, but did not finish starting. " +
267
+ (this.nativeStartupHint ? `${this.nativeStartupHint} ` : "") +
268
+ `Startup output: ${this.startupOutputBytes} bytes; diagnostic output: ${this.startupDiagnosticBytes} bytes. ` +
269
+ "Retry setup. If it still fails, share this error and your OpenMausBot version; do not paste Google sign-in links or tokens."));
270
+ }
271
+ else
272
+ reject(new Error(`${method} timed out.`));
231
273
  }, timeoutMs);
232
274
  timer.unref?.();
233
275
  this.pending.set(id, { resolve: resolveRequest, reject, timer });
@@ -235,11 +277,14 @@ export class AntigravityAcpClient {
235
277
  });
236
278
  }
237
279
  async initialize(timeoutMs = STARTUP_TIMEOUT_MS) {
238
- return this.request("initialize", {
280
+ const initialized = await this.request("initialize", {
239
281
  protocolVersion: 1,
240
282
  clientInfo: { name: "openmausbot", version: "0.0.0" },
241
283
  clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
242
284
  }, timeoutMs);
285
+ this.initializationComplete = true;
286
+ this.nativeStartupHint = undefined;
287
+ return initialized;
243
288
  }
244
289
  close() {
245
290
  if (this.closed)
@@ -248,8 +293,7 @@ export class AntigravityAcpClient {
248
293
  this.failAll(new Error("Antigravity ACP was closed."));
249
294
  killCliTree(this.child);
250
295
  }
251
- /** Close, then wait (bounded) for the process to be gone. `killCliTree`
252
- * on Windows asks taskkill and returns at once; the exit lands later. */
296
+ /** Close, then wait (bounded) for the process to be gone. */
253
297
  async closeAndWait(timeoutMs = 5_000) {
254
298
  this.close();
255
299
  let timer;
@@ -440,6 +484,12 @@ export async function validateAntigravityRuntime(runtime, expectedVersion) {
440
484
  instanceId: `verify-${randomUUID()}`,
441
485
  runtime,
442
486
  profileDirectory,
487
+ // Google's Windows one-file executable expands a large Python runtime
488
+ // into TEMP. Forced shutdown skips its own cleanup. Keep verification's
489
+ // extraction inside the profile we already remove after confirmed close.
490
+ baseEnv: process.platform === "win32"
491
+ ? { ...process.env, TEMP: profileDirectory, TMP: profileDirectory }
492
+ : undefined,
443
493
  });
444
494
  client = new AntigravityAcpClient(runtime, profile, profileDirectory);
445
495
  const initialized = await client.initialize();
@@ -14,8 +14,8 @@ const DOWNLOAD_TIMEOUT_MS = 45 * 60_000;
14
14
  const STAGING_PREFIX = ".install-";
15
15
  /** Windows refuses to rename or delete a file something still holds open,
16
16
  * with EPERM/EBUSY/EACCES rather than a clear "in use": the runtime the
17
- * validator just stopped (taskkill is asynchronous) or an antivirus scan of
18
- * a brand-new executable. Retry briefly; persistent refusals still fail. */
17
+ * validator just stopped or an antivirus scan of a brand-new executable.
18
+ * Retry briefly; persistent refusals still fail. */
19
19
  export function transientWindowsFileError(error) {
20
20
  const code = typeof error === "object" && error !== null ? Reflect.get(error, "code") : undefined;
21
21
  return code === "EPERM" || code === "EBUSY" || code === "EACCES" || code === "ENOTEMPTY";
@@ -2,6 +2,7 @@ import { createAcpDriver } from "./acp/core.js";
2
2
  import { AntigravityAuthController, antigravityProfileAuthenticated, catalogFromAntigravityConfigOptions, prepareAntigravityProfile, probeAntigravityModels, validateAntigravityRuntime, } from "./antigravity-acp.js";
3
3
  import { antigravityManagedInstallAvailable, installAntigravityRuntime, resolveAntigravityRuntime, } from "./antigravity-runtime.js";
4
4
  import { resolveAntigravityReleaseAsset } from "./antigravity-release.js";
5
+ import { augmentedPath } from "../env-path.js";
5
6
  export const STATIC_ANTIGRAVITY_MODELS = {
6
7
  default: "gemini-3.8-flash-high",
7
8
  options: [
@@ -107,8 +108,11 @@ export const AntigravityDriver = {
107
108
  async create(input) {
108
109
  const base = await AcpAntigravityDriver.create(input);
109
110
  const auth = new AntigravityAuthController();
111
+ let installFailure;
110
112
  const runtimeAndProfile = async () => {
111
- const environment = { ...process.env, ...input.environment };
113
+ // Match the ACP driver's discovery/chat environment. A GUI launch's
114
+ // raw PATH may omit an official runtime that the model picker found.
115
+ const environment = { ...process.env, ...input.environment, PATH: augmentedPath() };
112
116
  const runtime = await resolveAntigravityRuntime(input.config.cli, environment);
113
117
  const profile = await prepareAntigravityProfile({
114
118
  instanceId: input.instanceId,
@@ -122,10 +126,27 @@ export const AntigravityDriver = {
122
126
  get models() {
123
127
  return base.models;
124
128
  },
129
+ snapshot: async () => {
130
+ const snapshot = await base.snapshot();
131
+ if (snapshot.state === "available")
132
+ installFailure = undefined;
133
+ // A failed first install has no promoted executable yet. Preserve
134
+ // why it failed across closing/reopening setup in this app session.
135
+ return snapshot.state === "unavailable" && installFailure
136
+ ? { ...snapshot, reason: installFailure }
137
+ : snapshot;
138
+ },
125
139
  ...(antigravityManagedInstallAvailable()
126
140
  ? {
127
141
  installRuntime: async () => {
128
- await installAntigravityRuntime({ validate: validateAntigravityRuntime });
142
+ installFailure = undefined;
143
+ try {
144
+ await installAntigravityRuntime({ validate: validateAntigravityRuntime });
145
+ }
146
+ catch (error) {
147
+ installFailure = error instanceof Error ? error.message : String(error);
148
+ throw error;
149
+ }
129
150
  },
130
151
  }
131
152
  : {}),
@@ -0,0 +1,66 @@
1
+ // Codex owns history; this receipt only remembers which bot rules it accepted.
2
+ import { createHash } from "node:crypto";
3
+ import { mkdirSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { DATA_DIR } from "../config.js";
6
+ import { writeFileAtomic } from "../atomic.js";
7
+ const digest = (value) => createHash("sha256").update(value).digest("hex");
8
+ /** Preserve native configured rules when overriding Codex's developer slot. */
9
+ export function codexDeveloperInstructions(config, botInstructions) {
10
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
11
+ throw new Error("Codex returned no effective configuration; cannot safely update bot instructions.");
12
+ }
13
+ const configured = config.developer_instructions;
14
+ if (configured != null && typeof configured !== "string") {
15
+ throw new Error("Codex returned invalid developer instructions; cannot safely update bot instructions.");
16
+ }
17
+ // Native rules previously outranked the bot's user-message prefix. Keep
18
+ // them last in the combined developer block to preserve that precedence.
19
+ return configured
20
+ ? `${botInstructions || "No OpenMausBot bot-specific instructions remain."}\n\n${configured}`
21
+ : botInstructions;
22
+ }
23
+ export async function syncCodexInstructions(key, nativeThreadId, instructions, resumed, request) {
24
+ const directory = join(DATA_DIR, "codex-instructions");
25
+ const path = join(directory, `${digest(JSON.stringify([key, nativeThreadId]))}.sha256`);
26
+ const fingerprint = digest(instructions);
27
+ let previous;
28
+ try {
29
+ previous = readFileSync(path, "utf8");
30
+ }
31
+ catch (error) {
32
+ if (error.code !== "ENOENT")
33
+ throw error;
34
+ }
35
+ if (resumed && previous !== fingerprint) {
36
+ // thread/resume config overrides are used after compaction, but Codex
37
+ // 0.153.4 keeps the old initial developer message until then. Append a
38
+ // developer update ONLY on change (or first adoption of an old session).
39
+ // inject_items flushes native history before acknowledging; only then
40
+ // persist our receipt. Unknown-method errors must fail, never lose rules.
41
+ try {
42
+ await request("thread/inject_items", {
43
+ threadId: nativeThreadId,
44
+ items: [{
45
+ type: "message",
46
+ role: "developer",
47
+ content: [{
48
+ type: "input_text",
49
+ text: "The following replaces the previous developer instruction block supplied by OpenMausBot, including its native configured rules and bot-specific instructions. Other Codex instructions and permissions still apply.\n\n"
50
+ + (instructions || "No OpenMausBot bot-specific instructions remain."),
51
+ }],
52
+ }],
53
+ });
54
+ }
55
+ catch (error) {
56
+ if (error instanceof Error && /method not found|unknown method/i.test(error.message)) {
57
+ throw new Error(`This Codex version cannot update bot instructions in a resumed session. Update Codex and retry: ${error.message}`, { cause: error });
58
+ }
59
+ throw error;
60
+ }
61
+ }
62
+ if (!resumed || previous !== fingerprint) {
63
+ mkdirSync(directory, { recursive: true });
64
+ writeFileAtomic(path, fingerprint, { mode: 0o600 });
65
+ }
66
+ }
@@ -8,7 +8,7 @@
8
8
  // codex-cli 0.144.4 by agentcal.
9
9
  //
10
10
  // resumeCursor is the codex thread id; a later turn tries thread/resume
11
- // and falls back to a fresh thread/start.
11
+ // and preserves that history or reports a failed resume.
12
12
  import { existsSync } from "node:fs";
13
13
  import { homedir } from "node:os";
14
14
  import { stripWorkspaceCredentialEnv } from "../config.js";
@@ -22,6 +22,7 @@ import { codexLocalProviderArgs } from "./local-inject.js";
22
22
  import { augmentedPath, splitCliString } from "../env-path.js";
23
23
  import { classifyError, computeBackoff, RETRY_MAX_ATTEMPTS } from "./retry.js";
24
24
  import { appendNative } from "./native.js";
25
+ import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instructions.js";
25
26
  export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
26
27
  const DRIVER_KIND = "codex";
27
28
  const ASTRA_MODEL_ID = "gpt-6-astra";
@@ -300,18 +301,24 @@ function permissionProfileUnsupported(error) {
300
301
  const message = error instanceof Error ? error.message : String(error);
301
302
  return /(?:experimental api|invalid params|unknown field|unknown.*permissions|permissions.*(?:unsupported|sandbox)|cannot.*permissions)/i.test(message);
302
303
  }
303
- /** Keep private host paths out of diagnostics while preserving enough of the
304
- * app-server input shape to debug image delivery. The unmodified request is
304
+ /** Keep native instructions and private host paths out of diagnostics while
305
+ * preserving enough of the input shape to debug delivery. The unmodified request is
305
306
  * still written to the provider immediately after this log copy is made. */
306
307
  function codexNativeLogMessage(message) {
307
308
  if (!message || typeof message !== "object" || Array.isArray(message))
308
309
  return message;
309
310
  const record = message;
310
- if (record.method !== "turn/start")
311
- return message;
312
311
  const params = record.params;
313
312
  if (!params || typeof params !== "object" || Array.isArray(params))
314
313
  return message;
314
+ if (record.method === "thread/start" || record.method === "thread/resume") {
315
+ return { ...record, params: { ...params, developerInstructions: "[developer instructions omitted]" } };
316
+ }
317
+ if (record.method === "thread/inject_items") {
318
+ return { ...record, params: { ...params, items: "[developer instruction update omitted]" } };
319
+ }
320
+ if (record.method !== "turn/start")
321
+ return message;
315
322
  const input = params.input;
316
323
  if (!Array.isArray(input))
317
324
  return message;
@@ -547,11 +554,13 @@ export const CodexDriver = {
547
554
  });
548
555
  send({ jsonrpc: "2.0", id, method, params });
549
556
  });
557
+ let stopping;
558
+ const terminate = () => stopping ??= killCliTree(child);
550
559
  const stop = () => {
551
560
  stopRequested = true;
552
- killCliTree(child);
561
+ return terminate();
553
562
  };
554
- const settle = (ok, stopReason) => {
563
+ const settle = async (ok, stopReason) => {
555
564
  if (state.settled)
556
565
  return;
557
566
  state.settled = true;
@@ -560,9 +569,22 @@ export const CodexDriver = {
560
569
  for (const p of rpcPending.values())
561
570
  p.reject(new Error("turn settled"));
562
571
  rpcPending.clear();
563
- active.delete(threadId);
564
- emit({ ...base(threadId, turnId), type: "turn.completed", ok, stopReason, cost: null, ...(state.usage ? { usage: state.usage } : {}) });
565
- stop(); // the app-server never exits on its own
572
+ const complete = () => {
573
+ if (active.get(threadId)?.stop !== stop)
574
+ return;
575
+ active.delete(threadId);
576
+ emit({ ...base(threadId, turnId), type: "turn.completed", ok, stopReason, cost: null, ...(state.usage ? { usage: state.usage } : {}) });
577
+ };
578
+ if (await stop()) {
579
+ complete();
580
+ }
581
+ else {
582
+ emit({ ...base(threadId, turnId), type: "runtime.error", message: "codex did not shut down after termination was requested" });
583
+ if (child.exitCode !== null || child.signalCode !== null)
584
+ complete();
585
+ else
586
+ child.once("close", complete);
587
+ }
566
588
  };
567
589
  // server→client approval request → canonical request.opened
568
590
  // Host-scope tagging mirrors claude.ts: when this turn mounts the real
@@ -789,7 +811,7 @@ export const CodexDriver = {
789
811
  }
790
812
  case "turn/completed": {
791
813
  const t = p.turn ?? {};
792
- settle(t.status === "completed", t.status === "completed" ? null : (t.error?.message ?? t.status ?? "failed"));
814
+ void settle(t.status === "completed", t.status === "completed" ? null : (t.error?.message ?? t.status ?? "failed"));
793
815
  break;
794
816
  }
795
817
  case "error":
@@ -808,9 +830,11 @@ export const CodexDriver = {
808
830
  // multibyte characters that straddle two reads and corrupts the text
809
831
  child.stdout.setEncoding("utf8");
810
832
  child.stdout.on("data", (chunk) => {
833
+ if (abandoned || state.settled)
834
+ return;
811
835
  buf += chunk;
812
836
  let nl;
813
- while ((nl = buf.indexOf("\n")) !== -1) {
837
+ while (!state.settled && (nl = buf.indexOf("\n")) !== -1) {
814
838
  const line = buf.slice(0, nl);
815
839
  buf = buf.slice(nl + 1);
816
840
  if (!line.trim())
@@ -852,7 +876,7 @@ export const CodexDriver = {
852
876
  if (abandoned)
853
877
  return;
854
878
  emit({ ...base(threadId, turnId), type: "runtime.error", ...describeSpawnFailure(e, config.cli) });
855
- settle(false, "spawn_error");
879
+ void settle(false, "spawn_error");
856
880
  });
857
881
  child.on("close", (code) => {
858
882
  if (abandoned)
@@ -863,7 +887,7 @@ export const CodexDriver = {
863
887
  type: "runtime.error",
864
888
  message: `codex exited ${code} before turn/completed${stderr ? `: ${stderr.trim().slice(-300)}` : ""}`,
865
889
  });
866
- settle(false, "exit_before_result");
890
+ void settle(false, "exit_before_result");
867
891
  }
868
892
  });
869
893
  active.set(threadId, { stop, turnId, asks });
@@ -883,26 +907,28 @@ export const CodexDriver = {
883
907
  capabilities: { experimentalApi: true },
884
908
  });
885
909
  send({ jsonrpc: "2.0", method: "initialized", params: {} });
910
+ // developerInstructions replaces, rather than appends to, native
911
+ // config. Read it for every approval mode so existing rules survive.
912
+ // request() already redacts config/read responses from native logs.
913
+ let effectiveConfig;
914
+ try {
915
+ const configured = await request("config/read", {
916
+ cwd: turn.cwd ?? homedir(),
917
+ includeLayers: false,
918
+ });
919
+ effectiveConfig = configured?.config;
920
+ }
921
+ catch {
922
+ // Do not expose a possibly secret-bearing native config error or
923
+ // overwrite unknown instructions with an empty fallback.
924
+ throw new Error("Could not read Codex configuration; cannot safely update bot instructions. Retry after checking Codex.");
925
+ }
926
+ const developerInstructions = codexDeveloperInstructions(effectiveConfig, turn.system ?? "");
886
927
  let approvalParams;
887
928
  if (approvalMode === "custom") {
888
929
  // config/read returns the effective global + project config for this
889
930
  // cwd. Reasserting those values is essential: simply omitting them
890
931
  // on a resumed thread would keep the previous named mode sticky.
891
- let effectiveConfig;
892
- try {
893
- const configured = await request("config/read", {
894
- cwd: turn.cwd ?? homedir(),
895
- includeLayers: false,
896
- });
897
- effectiveConfig = configured?.config;
898
- }
899
- catch {
900
- // Older app-servers and transient failures cannot prove the
901
- // user's configured sandbox. Fall back to interactive read-only
902
- // instead of inheriting stale Full or silently broadening a
903
- // possibly read-only config to workspace write.
904
- effectiveConfig = {};
905
- }
906
932
  approvalParams = customApprovalParams(effectiveConfig);
907
933
  }
908
934
  else {
@@ -913,6 +939,9 @@ export const CodexDriver = {
913
939
  // mode may synthesize approvals; Custom must preserve the sandbox
914
940
  // boundary from config.toml (for example never + read-only).
915
941
  autoAcceptPermissions = approvalMode === "full";
942
+ // Each turn launches a new app-server. Reassert current bot instructions
943
+ // on start AND resume so Codex owns their lifetime through compaction.
944
+ // Removed bot rules are cleared without dropping native configured rules.
916
945
  const cursor = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
917
946
  let codexThreadId = null;
918
947
  let startedModel = null;
@@ -920,6 +949,7 @@ export const CodexDriver = {
920
949
  try {
921
950
  const resumed = await request("thread/resume", {
922
951
  threadId: cursor,
952
+ developerInstructions,
923
953
  ...approvalParams.thread,
924
954
  });
925
955
  codexThreadId = resumed?.thread?.id ?? cursor;
@@ -930,23 +960,22 @@ export const CodexDriver = {
930
960
  // profile selector. Retry the same resume safely rather than
931
961
  // losing the native thread or inheriting its previous mode.
932
962
  approvalParams = approvalParams.fallback;
933
- try {
934
- const resumed = await request("thread/resume", {
935
- threadId: cursor,
936
- ...approvalParams.thread,
937
- });
938
- codexThreadId = resumed?.thread?.id ?? cursor;
939
- }
940
- catch {
941
- /* thread gone or resume unsupported — start fresh below */
942
- }
963
+ const resumed = await request("thread/resume", {
964
+ threadId: cursor,
965
+ developerInstructions,
966
+ ...approvalParams.thread,
967
+ });
968
+ codexThreadId = resumed?.thread?.id ?? cursor;
969
+ }
970
+ else {
971
+ throw error;
943
972
  }
944
- /* thread gone or resume unsupported — start fresh below */
945
973
  }
946
974
  }
947
975
  if (!codexThreadId) {
948
976
  const selection = decodeCodexSelection(turn.model);
949
977
  const startThread = () => request("thread/start", {
978
+ developerInstructions,
950
979
  cwd: turn.cwd ?? homedir(),
951
980
  model: selection.model,
952
981
  ...(selection.modelProvider ? { modelProvider: selection.modelProvider } : {}),
@@ -966,8 +995,11 @@ export const CodexDriver = {
966
995
  codexThreadId = started?.thread?.id ?? null;
967
996
  startedModel = started?.model ?? null;
968
997
  }
998
+ if (!codexThreadId)
999
+ throw new Error("Codex did not return a native thread id");
1000
+ await syncCodexInstructions(threadId, codexThreadId, developerInstructions, Boolean(cursor), request);
969
1001
  emit({ ...base(threadId, turnId), type: "session.started", sessionId: codexThreadId, model: startedModel ?? turn.model ?? null });
970
- const promptText = turn.system ? `${turn.system}\n\n${turn.text}` : turn.text;
1002
+ const promptText = turn.text;
971
1003
  const turnInput = [
972
1004
  ...(promptText ? [{ type: "text", text: promptText }] : []),
973
1005
  ...(turn.images ?? []).map((image) => ({ type: "localImage", path: image.path })),
@@ -1015,7 +1047,10 @@ export const CodexDriver = {
1015
1047
  // This app-server never exits by itself. Retire the failed attempt
1016
1048
  // and silence its late handlers before the replacement launches.
1017
1049
  abandoned = true;
1018
- killCliTree(child);
1050
+ if (!await terminate()) {
1051
+ void settle(false, "shutdown_timeout");
1052
+ return;
1053
+ }
1019
1054
  await new Promise((resolve) => {
1020
1055
  const timer = setTimeout(resolve, Math.max(1, Math.round(delayMs * retryScale)));
1021
1056
  timer.unref?.();
@@ -1024,7 +1059,7 @@ export const CodexDriver = {
1024
1059
  void launchAttempt(attempt).catch(() => { });
1025
1060
  }
1026
1061
  else {
1027
- settle(false, "interrupted");
1062
+ await settle(false, "interrupted");
1028
1063
  }
1029
1064
  return;
1030
1065
  }
@@ -1035,7 +1070,7 @@ export const CodexDriver = {
1035
1070
  message,
1036
1071
  ...(needsAuth ? { setup: true } : {}),
1037
1072
  });
1038
- settle(false, needsAuth ? "auth_required" : "rpc_error");
1073
+ await settle(false, needsAuth ? "auth_required" : "rpc_error");
1039
1074
  }
1040
1075
  }
1041
1076
  };
@@ -1087,7 +1122,9 @@ export const CodexDriver = {
1087
1122
  effortLevels: ["low", "medium", "high", "xhigh", "max"],
1088
1123
  },
1089
1124
  sendTurn,
1090
- interruptTurn: async (threadId) => active.get(threadId)?.stop(),
1125
+ interruptTurn: async (threadId) => {
1126
+ await active.get(threadId)?.stop();
1127
+ },
1091
1128
  respondToRequest: async (threadId, requestId, decision) => {
1092
1129
  const turn = active.get(threadId);
1093
1130
  const finish = turn?.asks.get(requestId);
@@ -1098,8 +1135,7 @@ export const CodexDriver = {
1098
1135
  },
1099
1136
  hasSession: (threadId) => active.has(threadId),
1100
1137
  stopAll: async () => {
1101
- for (const { stop } of active.values())
1102
- stop();
1138
+ await Promise.all([...active.values()].map(({ stop }) => stop()));
1103
1139
  },
1104
1140
  onEvent: (listener) => {
1105
1141
  listeners.add(listener);
@@ -1107,8 +1143,7 @@ export const CodexDriver = {
1107
1143
  },
1108
1144
  },
1109
1145
  dispose: async () => {
1110
- for (const { stop } of active.values())
1111
- stop();
1146
+ await Promise.all([...active.values()].map(({ stop }) => stop()));
1112
1147
  listeners.clear();
1113
1148
  },
1114
1149
  };