remote-codex 0.1.7 → 0.1.8

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.
@@ -5,15 +5,15 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // src/index.ts
8
- import fs15 from "fs";
8
+ import fs16 from "fs";
9
9
 
10
10
  // src/app.ts
11
11
  import Fastify from "fastify";
12
12
  import multipart from "@fastify/multipart";
13
13
  import websocket from "@fastify/websocket";
14
14
  import { spawn as spawn3 } from "child_process";
15
- import fs14 from "fs";
16
- import path14 from "path";
15
+ import fs15 from "fs";
16
+ import path15 from "path";
17
17
  import { ZodError } from "zod";
18
18
 
19
19
  // ../../packages/config/src/index.ts
@@ -32,7 +32,10 @@ var envSchema = z.object({
32
32
  DATABASE_URL: z.string().optional(),
33
33
  CODEX_HOME: z.string().optional(),
34
34
  CODEX_COMMAND: z.string().min(1).optional(),
35
- CODEX_APP_SERVER_START_TIMEOUT_MS: z.coerce.number().int().positive().optional()
35
+ CODEX_APP_SERVER_START_TIMEOUT_MS: z.coerce.number().int().positive().optional(),
36
+ CLAUDE_HOME: z.string().optional(),
37
+ CLAUDE_COMMAND: z.string().min(1).optional(),
38
+ REMOTE_CODEX_ENABLED_AGENT_PROVIDERS: z.string().optional()
36
39
  });
37
40
  function resolveDatabaseUrl(nodeEnv, value) {
38
41
  if (value && value.trim()) {
@@ -48,6 +51,11 @@ function loadRuntimeConfig(env = process.env) {
48
51
  const nodeEnv = parsed.NODE_ENV ?? "development";
49
52
  const workspaceRoot = parsed.WORKSPACE_ROOT?.trim() ? path.resolve(parsed.WORKSPACE_ROOT) : os.homedir();
50
53
  const disableRequestLogging = parsed.DISABLE_REQUEST_LOGGING === void 0 ? nodeEnv === "production" : ["1", "true", "yes", "on"].includes(parsed.DISABLE_REQUEST_LOGGING.toLowerCase());
54
+ const enabledProviders = new Set(
55
+ (parsed.REMOTE_CODEX_ENABLED_AGENT_PROVIDERS ?? "codex").split(",").map((provider) => provider.trim().toLowerCase()).filter(Boolean)
56
+ );
57
+ const codexHome = parsed.CODEX_HOME?.trim() ? path.resolve(parsed.CODEX_HOME) : path.join(os.homedir(), ".codex");
58
+ const claudeHome = parsed.CLAUDE_HOME?.trim() ? path.resolve(parsed.CLAUDE_HOME) : path.join(os.homedir(), ".claude");
51
59
  return {
52
60
  nodeEnv,
53
61
  host: parsed.HOST ?? "127.0.0.1",
@@ -58,9 +66,21 @@ function loadRuntimeConfig(env = process.env) {
58
66
  appVersion: parsed.APP_VERSION ?? "0.1.0",
59
67
  workspaceRoot,
60
68
  databaseUrl: resolveDatabaseUrl(nodeEnv, parsed.DATABASE_URL),
61
- codexHome: parsed.CODEX_HOME?.trim() ? path.resolve(parsed.CODEX_HOME) : path.join(os.homedir(), ".codex"),
62
- codexCommand: parsed.CODEX_COMMAND ?? "codex",
63
- codexAppServerStartTimeoutMs: parsed.CODEX_APP_SERVER_START_TIMEOUT_MS ?? 1e4
69
+ agentProviders: {
70
+ codex: {
71
+ provider: "codex",
72
+ enabled: enabledProviders.has("codex"),
73
+ home: codexHome,
74
+ command: parsed.CODEX_COMMAND ?? "codex",
75
+ appServerStartTimeoutMs: parsed.CODEX_APP_SERVER_START_TIMEOUT_MS ?? 1e4
76
+ },
77
+ claude: {
78
+ provider: "claude",
79
+ enabled: enabledProviders.has("claude"),
80
+ home: claudeHome,
81
+ command: parsed.CLAUDE_COMMAND ?? "claude"
82
+ }
83
+ }
64
84
  };
65
85
  }
66
86
 
@@ -1281,7 +1301,7 @@ Subquery.prototype.getSQL = function() {
1281
1301
  function mapResultRow(columns, row, joinsNotNullableMap) {
1282
1302
  const nullifyMap = {};
1283
1303
  const result = columns.reduce(
1284
- (result2, { path: path15, field }, columnIndex) => {
1304
+ (result2, { path: path16, field }, columnIndex) => {
1285
1305
  let decoder;
1286
1306
  if (is(field, Column)) {
1287
1307
  decoder = field;
@@ -1291,8 +1311,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
1291
1311
  decoder = field.sql.decoder;
1292
1312
  }
1293
1313
  let node = result2;
1294
- for (const [pathChunkIndex, pathChunk] of path15.entries()) {
1295
- if (pathChunkIndex < path15.length - 1) {
1314
+ for (const [pathChunkIndex, pathChunk] of path16.entries()) {
1315
+ if (pathChunkIndex < path16.length - 1) {
1296
1316
  if (!(pathChunk in node)) {
1297
1317
  node[pathChunk] = {};
1298
1318
  }
@@ -1300,8 +1320,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
1300
1320
  } else {
1301
1321
  const rawValue = row[columnIndex];
1302
1322
  const value = node[pathChunk] = rawValue === null ? null : decoder.mapFromDriverValue(rawValue);
1303
- if (joinsNotNullableMap && is(field, Column) && path15.length === 2) {
1304
- const objectName = path15[0];
1323
+ if (joinsNotNullableMap && is(field, Column) && path16.length === 2) {
1324
+ const objectName = path16[0];
1305
1325
  if (!(objectName in nullifyMap)) {
1306
1326
  nullifyMap[objectName] = value === null ? getTableName(field.table) : false;
1307
1327
  } else if (typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== getTableName(field.table)) {
@@ -5879,6 +5899,9 @@ function getActiveThreadGoalRecord(db, threadId) {
5879
5899
  ) ?? null;
5880
5900
  }
5881
5901
  function getThreadGoalRecordForUpsert(db, input) {
5902
+ if (input.createNew) {
5903
+ return null;
5904
+ }
5882
5905
  if (input.localGoalId) {
5883
5906
  const byId = db.select().from(threadGoals).where(eq(threadGoals.id, input.localGoalId)).get();
5884
5907
  if (byId?.threadId === input.threadId) {
@@ -5886,7 +5909,7 @@ function getThreadGoalRecordForUpsert(db, input) {
5886
5909
  }
5887
5910
  }
5888
5911
  const active = getActiveThreadGoalRecord(db, input.threadId);
5889
- if (active) {
5912
+ if (active && active.objective === input.objective) {
5890
5913
  return active;
5891
5914
  }
5892
5915
  const matchingObjective = db.select().from(threadGoals).where(
@@ -7527,7 +7550,7 @@ function extractFileChangeEntries(item) {
7527
7550
  isRecord2(entry.summary) ? entry.summary : null,
7528
7551
  isRecord2(entry.diff) ? entry.diff : null
7529
7552
  ].filter((candidate) => Boolean(candidate));
7530
- const path15 = uniqueStrings([
7553
+ const path16 = uniqueStrings([
7531
7554
  stringOrNull(valueFromRecords(nestedRecords, ["path", "filePath", "targetPath"])),
7532
7555
  stringOrNull(
7533
7556
  valueFromRecords(nestedRecords, [
@@ -7574,7 +7597,7 @@ function extractFileChangeEntries(item) {
7574
7597
  const diffStats = explicitAdditions === 0 && explicitDeletions === 0 && diffText ? countUnifiedDiffStats(diffText) : null;
7575
7598
  const additions = explicitAdditions || diffStats?.additions || 0;
7576
7599
  const deletions = explicitDeletions || diffStats?.deletions || 0;
7577
- const normalizedPath = path15 ?? (diffText ? extractPathFromDiffText(diffText) : null);
7600
+ const normalizedPath = path16 ?? (diffText ? extractPathFromDiffText(diffText) : null);
7578
7601
  if (!normalizedPath && additions === 0 && deletions === 0) {
7579
7602
  return null;
7580
7603
  }
@@ -8121,7 +8144,7 @@ var codexCapabilities = {
8121
8144
  planMode: true,
8122
8145
  permissionRequests: true,
8123
8146
  sandboxMode: true,
8124
- fastServiceTier: true,
8147
+ performanceMode: true,
8125
8148
  goals: true
8126
8149
  },
8127
8150
  management: {
@@ -8200,6 +8223,43 @@ function mapGoal(goal) {
8200
8223
  rawGoal: goal
8201
8224
  };
8202
8225
  }
8226
+ function serviceTierForPerformanceMode(performanceMode) {
8227
+ if (performanceMode === void 0) {
8228
+ return void 0;
8229
+ }
8230
+ return performanceMode === "fast" ? "fast" : null;
8231
+ }
8232
+ function buildSandboxPolicy(sandboxMode, workspacePath) {
8233
+ if (sandboxMode === void 0) {
8234
+ return void 0;
8235
+ }
8236
+ switch (sandboxMode) {
8237
+ case "danger-full-access":
8238
+ return {
8239
+ type: "dangerFullAccess"
8240
+ };
8241
+ case "read-only":
8242
+ return {
8243
+ type: "readOnly",
8244
+ access: {
8245
+ type: "fullAccess"
8246
+ },
8247
+ networkAccess: false
8248
+ };
8249
+ case "workspace-write":
8250
+ default:
8251
+ return {
8252
+ type: "workspaceWrite",
8253
+ writableRoots: workspacePath ? [workspacePath] : [],
8254
+ readOnlyAccess: {
8255
+ type: "fullAccess"
8256
+ },
8257
+ networkAccess: false,
8258
+ excludeTmpdirEnvVar: false,
8259
+ excludeSlashTmp: false
8260
+ };
8261
+ }
8262
+ }
8203
8263
  function mapSession(thread) {
8204
8264
  return {
8205
8265
  provider: "codex",
@@ -8465,6 +8525,8 @@ var CodexRuntimeAdapter = class extends EventEmitter3 {
8465
8525
  command: `node -e 'process.stdin.resume(); process.stdin.on("end", () => console.log(JSON.stringify({ systemMessage: "remote-codex hook ran" })))'`
8466
8526
  }
8467
8527
  ],
8528
+ providerConfigFormat: "toml",
8529
+ mcpConfigFormat: "codex-toml",
8468
8530
  configArchives: true,
8469
8531
  buildRestart: true
8470
8532
  };
@@ -8511,8 +8573,9 @@ var CodexRuntimeAdapter = class extends EventEmitter3 {
8511
8573
  if (input.sandboxMode !== void 0) {
8512
8574
  startInput.sandbox = input.sandboxMode;
8513
8575
  }
8514
- if (input.serviceTier !== void 0) {
8515
- startInput.serviceTier = input.serviceTier;
8576
+ const serviceTier = serviceTierForPerformanceMode(input.performanceMode);
8577
+ if (serviceTier !== void 0) {
8578
+ startInput.serviceTier = serviceTier;
8516
8579
  }
8517
8580
  const response = await codexRuntimeCall(() => this.manager.startThread(startInput));
8518
8581
  return {
@@ -8535,8 +8598,9 @@ var CodexRuntimeAdapter = class extends EventEmitter3 {
8535
8598
  if (input.sandboxMode !== void 0) {
8536
8599
  resumeInput.sandbox = input.sandboxMode;
8537
8600
  }
8538
- if (input.serviceTier !== void 0) {
8539
- resumeInput.serviceTier = input.serviceTier;
8601
+ const serviceTier = serviceTierForPerformanceMode(input.performanceMode);
8602
+ if (serviceTier !== void 0) {
8603
+ resumeInput.serviceTier = serviceTier;
8540
8604
  }
8541
8605
  const response = await codexRuntimeCall(() => this.manager.resumeThread(resumeInput));
8542
8606
  return {
@@ -8563,11 +8627,13 @@ var CodexRuntimeAdapter = class extends EventEmitter3 {
8563
8627
  if (input.collaborationMode !== void 0) {
8564
8628
  turnInput.collaborationMode = input.collaborationMode;
8565
8629
  }
8566
- if (input.sandboxPolicy !== void 0) {
8567
- turnInput.sandboxPolicy = input.sandboxPolicy;
8630
+ const sandboxPolicy = buildSandboxPolicy(input.sandboxMode, input.workspacePath);
8631
+ if (sandboxPolicy !== void 0) {
8632
+ turnInput.sandboxPolicy = sandboxPolicy;
8568
8633
  }
8569
- if (input.serviceTier !== void 0) {
8570
- turnInput.serviceTier = input.serviceTier;
8634
+ const serviceTier = serviceTierForPerformanceMode(input.performanceMode);
8635
+ if (serviceTier !== void 0) {
8636
+ turnInput.serviceTier = serviceTier;
8571
8637
  }
8572
8638
  return mapTurn2(await codexRuntimeCall(() => this.manager.startTurn(turnInput)));
8573
8639
  }
@@ -8640,174 +8706,713 @@ var CodexRuntimeAdapter = class extends EventEmitter3 {
8640
8706
  }
8641
8707
  };
8642
8708
 
8643
- // src/codex/local-session-store.ts
8644
- import fs4 from "fs/promises";
8645
- import path5 from "path";
8646
- import Database2 from "better-sqlite3";
8709
+ // src/codex/codex-management-service.ts
8710
+ import fs5 from "fs/promises";
8711
+ import path6 from "path";
8647
8712
 
8648
- // src/codex/thread-title.ts
8649
- var AUTO_THREAD_TITLE_MAX_CHARS = 15;
8650
- function normalizeWhitespace(value) {
8651
- return value.replace(/\s+/g, " ").trim();
8713
+ // src/codex/codexHostConfig.ts
8714
+ import fs4 from "fs";
8715
+ import fsp from "fs/promises";
8716
+ import path5 from "path";
8717
+ var SERVICE_TIER_LINE_GLOBAL_PATTERN = /^\s*service_tier\s*=\s*("fast"|"flex"|'fast'|'flex').*\n?/gm;
8718
+ function resolveCodexConfigPath(codexHome) {
8719
+ return path5.join(codexHome, "config.toml");
8652
8720
  }
8653
- function truncateAutoThreadTitle(value) {
8654
- const normalized = normalizeWhitespace(value);
8655
- if (!normalized) {
8656
- return "";
8657
- }
8658
- const characters = Array.from(normalized);
8659
- if (characters.length <= AUTO_THREAD_TITLE_MAX_CHARS) {
8660
- return normalized;
8661
- }
8662
- return `${characters.slice(0, AUTO_THREAD_TITLE_MAX_CHARS).join("")}...`;
8721
+ function parseCodexServiceTier(content) {
8722
+ const match = content.match(
8723
+ /^\s*service_tier\s*=\s*["'](?<tier>fast)["']\s*$/m
8724
+ );
8725
+ const tier = match?.groups?.tier;
8726
+ return tier === "fast" ? tier : null;
8663
8727
  }
8664
-
8665
- // src/codex/local-session-store.ts
8666
- function basenameFromPath(absPath) {
8667
- const normalized = absPath.replace(/[\\/]+$/, "");
8668
- return normalized.split(/[\\/]/).at(-1) ?? normalized;
8728
+ function isFastModeEnabledFromConfig(content) {
8729
+ return parseCodexServiceTier(content) === "fast";
8669
8730
  }
8670
- function summarizeTitleFromTurns(turns) {
8671
- const firstUserMessage = turns.flatMap((turn) => turn.items).find((item) => item.kind === "userMessage" && item.text.trim());
8672
- if (!firstUserMessage) {
8673
- return null;
8731
+ function readCodexFastModeSync(codexHome) {
8732
+ try {
8733
+ const content = fs4.readFileSync(resolveCodexConfigPath(codexHome), "utf8");
8734
+ return isFastModeEnabledFromConfig(content);
8735
+ } catch (error) {
8736
+ if (error.code === "ENOENT") {
8737
+ return false;
8738
+ }
8739
+ throw error;
8674
8740
  }
8675
- return truncateAutoThreadTitle(firstUserMessage.text);
8676
- }
8677
- function createHistoryItemId(turnId, prefix, index) {
8678
- return `${turnId}-${prefix}-${index}`;
8679
8741
  }
8680
- function finalizeTurn(turn, turns) {
8681
- if (!turn || turn.items.length === 0) {
8682
- return;
8742
+ function upsertCodexServiceTier(content, enabled) {
8743
+ const normalized = content.replace(/\r\n/g, "\n");
8744
+ const withoutServiceTier = normalized.replace(
8745
+ SERVICE_TIER_LINE_GLOBAL_PATTERN,
8746
+ ""
8747
+ );
8748
+ if (!enabled) {
8749
+ return withoutServiceTier.replace(/\n{3,}/g, "\n\n").trimEnd() + (withoutServiceTier.trim() ? "\n" : "");
8683
8750
  }
8684
- turns.push({
8685
- id: turn.id,
8686
- startedAt: turn.startedAt,
8687
- status: turn.status,
8688
- error: turn.error,
8689
- items: turn.items
8690
- });
8751
+ const nextLine = 'service_tier = "fast"';
8752
+ if (!withoutServiceTier.trim()) {
8753
+ return `${nextLine}
8754
+ `;
8755
+ }
8756
+ const firstSectionMatch = withoutServiceTier.match(/^\s*\[[^\]]+\]/m);
8757
+ if (!firstSectionMatch || firstSectionMatch.index === void 0) {
8758
+ return withoutServiceTier.endsWith("\n") ? `${withoutServiceTier}${nextLine}
8759
+ ` : `${withoutServiceTier}
8760
+ ${nextLine}
8761
+ `;
8762
+ }
8763
+ const beforeFirstSection = withoutServiceTier.slice(0, firstSectionMatch.index).trimEnd();
8764
+ const afterFirstSection = withoutServiceTier.slice(firstSectionMatch.index).replace(/^\n+/, "");
8765
+ return beforeFirstSection ? `${beforeFirstSection}
8766
+ ${nextLine}
8767
+ ${afterFirstSection}` : `${nextLine}
8768
+ ${afterFirstSection}`;
8691
8769
  }
8692
- function parseTranscript(contents) {
8693
- const turns = [];
8694
- let cwd = null;
8695
- let currentTurn = null;
8696
- let fallbackTurnCount = 0;
8697
- let agentItemCount = 0;
8698
- let userItemCount = 0;
8699
- const ensureCurrentTurn = (timestamp) => {
8700
- if (currentTurn) {
8701
- return currentTurn;
8702
- }
8703
- fallbackTurnCount += 1;
8704
- currentTurn = {
8705
- id: `local-turn-${fallbackTurnCount}`,
8706
- startedAt: timestamp ?? null,
8707
- status: "inProgress",
8708
- error: null,
8709
- items: []
8710
- };
8711
- agentItemCount = 0;
8712
- userItemCount = 0;
8713
- return currentTurn;
8714
- };
8715
- for (const line of contents.split("\n")) {
8716
- if (!line.trim()) {
8717
- continue;
8718
- }
8719
- let entry;
8720
- try {
8721
- entry = JSON.parse(line);
8722
- } catch {
8723
- continue;
8724
- }
8725
- if (entry.type === "session_meta") {
8726
- const payload2 = entry.payload ?? {};
8727
- if (typeof payload2.cwd === "string" && payload2.cwd.trim()) {
8728
- cwd = payload2.cwd;
8729
- }
8730
- continue;
8731
- }
8732
- if (entry.type !== "event_msg") {
8733
- continue;
8734
- }
8735
- const payload = entry.payload ?? {};
8736
- const payloadType = payload.type;
8737
- if (payloadType === "task_started") {
8738
- finalizeTurn(currentTurn, turns);
8739
- currentTurn = {
8740
- id: typeof payload.turn_id === "string" && payload.turn_id.trim() ? payload.turn_id : `local-turn-${fallbackTurnCount + 1}`,
8741
- startedAt: entry.timestamp ?? null,
8742
- status: "inProgress",
8743
- error: null,
8744
- items: []
8745
- };
8746
- agentItemCount = 0;
8747
- userItemCount = 0;
8748
- continue;
8749
- }
8750
- if (payloadType === "user_message" && typeof payload.message === "string") {
8751
- const turn = ensureCurrentTurn(entry.timestamp);
8752
- userItemCount += 1;
8753
- turn.items.push({
8754
- id: createHistoryItemId(turn.id, "user", userItemCount),
8755
- kind: "userMessage",
8756
- text: payload.message
8757
- });
8758
- continue;
8759
- }
8760
- if (payloadType === "agent_message" && typeof payload.message === "string") {
8761
- const turn = ensureCurrentTurn(entry.timestamp);
8762
- agentItemCount += 1;
8763
- turn.items.push({
8764
- id: createHistoryItemId(turn.id, "agent", agentItemCount),
8765
- kind: "agentMessage",
8766
- text: payload.message,
8767
- status: typeof payload.phase === "string" ? payload.phase : null
8768
- });
8769
- continue;
8770
- }
8771
- if (payloadType === "task_complete") {
8772
- const turn = ensureCurrentTurn(entry.timestamp);
8773
- turn.status = turn.error ? "failed" : "completed";
8774
- finalizeTurn(turn, turns);
8775
- currentTurn = null;
8776
- continue;
8777
- }
8778
- if (payloadType === "error") {
8779
- const turn = ensureCurrentTurn(entry.timestamp);
8780
- turn.status = "failed";
8781
- turn.error = typeof payload.message === "string" ? payload.message : "Local Codex session failed.";
8770
+ async function writeCodexFastMode(codexHome, enabled) {
8771
+ const configPath = resolveCodexConfigPath(codexHome);
8772
+ let current = "";
8773
+ try {
8774
+ current = await fsp.readFile(configPath, "utf8");
8775
+ } catch (error) {
8776
+ if (error.code !== "ENOENT") {
8777
+ throw error;
8782
8778
  }
8783
8779
  }
8784
- finalizeTurn(currentTurn, turns);
8785
- return {
8786
- cwd,
8787
- title: summarizeTitleFromTurns(turns),
8788
- turns
8789
- };
8780
+ const next = upsertCodexServiceTier(current, enabled);
8781
+ await fsp.mkdir(path5.dirname(configPath), { recursive: true });
8782
+ await fsp.writeFile(configPath, next, "utf8");
8783
+ return next;
8790
8784
  }
8791
- async function fileExists(filePath) {
8792
- try {
8793
- await fs4.access(filePath);
8794
- return true;
8795
- } catch {
8785
+ function isCodexFeatureEnabledFromConfig(content, featureName) {
8786
+ const escaped = featureName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8787
+ const featuresMatch = content.match(/^[^\S\r\n]*\[features\][^\S\r\n]*$/m);
8788
+ if (!featuresMatch || featuresMatch.index === void 0) {
8796
8789
  return false;
8797
8790
  }
8791
+ const sectionStart = featuresMatch.index + featuresMatch[0].length;
8792
+ const nextSectionMatch = content.slice(sectionStart).match(/^[^\S\r\n]*\[[^\]\r\n]+\][^\S\r\n]*$/m);
8793
+ const sectionEnd = nextSectionMatch && nextSectionMatch.index !== void 0 ? sectionStart + nextSectionMatch.index : content.length;
8794
+ const featuresBody = content.slice(sectionStart, sectionEnd);
8795
+ return new RegExp(`^\\s*${escaped}\\s*=\\s*true\\s*(?:#.*)?$`, "m").test(featuresBody);
8798
8796
  }
8799
- var LocalCodexSessionStore = class {
8800
- constructor(codexHome) {
8801
- this.codexHome = codexHome;
8802
- }
8803
- codexHome;
8804
- async findSession(sessionId) {
8797
+ async function readCodexFeatureFlag(codexHome, featureName) {
8798
+ try {
8799
+ const content = await fsp.readFile(resolveCodexConfigPath(codexHome), "utf8");
8800
+ return isCodexFeatureEnabledFromConfig(content, featureName);
8801
+ } catch (error) {
8802
+ if (error.code === "ENOENT") {
8803
+ return false;
8804
+ }
8805
+ throw error;
8806
+ }
8807
+ }
8808
+ function upsertCodexFeatureFlag(content, featureName, enabled) {
8809
+ const normalized = content.replace(/\r\n/g, "\n");
8810
+ const escaped = featureName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8811
+ const featuresMatch = normalized.match(/^[^\S\r\n]*\[features\][^\S\r\n]*$/m);
8812
+ const nextLine = `${featureName} = ${enabled ? "true" : "false"}`;
8813
+ if (!featuresMatch || featuresMatch.index === void 0) {
8814
+ const prefix = normalized.trimEnd();
8815
+ return prefix ? `${prefix}
8816
+
8817
+ [features]
8818
+ ${nextLine}
8819
+ ` : `[features]
8820
+ ${nextLine}
8821
+ `;
8822
+ }
8823
+ const sectionStart = featuresMatch.index + featuresMatch[0].length;
8824
+ const nextSectionMatch = normalized.slice(sectionStart).match(/^[^\S\r\n]*\[[^\]\r\n]+\][^\S\r\n]*$/m);
8825
+ const sectionEnd = nextSectionMatch && nextSectionMatch.index !== void 0 ? sectionStart + nextSectionMatch.index : normalized.length;
8826
+ const beforeSectionBody = normalized.slice(0, sectionStart);
8827
+ const sectionBody = normalized.slice(sectionStart, sectionEnd);
8828
+ const afterSection = normalized.slice(sectionEnd);
8829
+ const flagPattern = new RegExp(
8830
+ `(^[^\\S\\r\\n]*)${escaped}[^\\S\\r\\n]*=[^\\S\\r\\n]*(true|false)[^\\S\\r\\n]*(?:#.*)?$`,
8831
+ "m"
8832
+ );
8833
+ if (flagPattern.test(sectionBody)) {
8834
+ return beforeSectionBody + sectionBody.replace(flagPattern, `$1${nextLine}`) + afterSection;
8835
+ }
8836
+ const bodyWithFlag = `${sectionBody.replace(/\n*$/, "")}
8837
+ ${nextLine}
8838
+ `;
8839
+ const normalizedAfterSection = afterSection.startsWith("\n") ? afterSection : `
8840
+ ${afterSection}`;
8841
+ return beforeSectionBody + bodyWithFlag + normalizedAfterSection;
8842
+ }
8843
+ async function writeCodexFeatureFlag(codexHome, featureName, enabled) {
8844
+ const configPath = resolveCodexConfigPath(codexHome);
8845
+ let current = "";
8846
+ try {
8847
+ current = await fsp.readFile(configPath, "utf8");
8848
+ } catch (error) {
8849
+ if (error.code !== "ENOENT") {
8850
+ throw error;
8851
+ }
8852
+ }
8853
+ const next = upsertCodexFeatureFlag(current, featureName, enabled);
8854
+ await fsp.mkdir(path5.dirname(configPath), { recursive: true });
8855
+ await fsp.writeFile(configPath, next, "utf8");
8856
+ return next;
8857
+ }
8858
+
8859
+ // src/codex/runtime-errors.ts
8860
+ function unwrapCodexJsonRpcError(error) {
8861
+ if (error instanceof JsonRpcClientError) {
8862
+ return error;
8863
+ }
8864
+ if (error instanceof AgentRuntimeError && error.provider === "codex") {
8865
+ return error.cause instanceof JsonRpcClientError ? error.cause : null;
8866
+ }
8867
+ return null;
8868
+ }
8869
+ function isCodexRemoteError(error) {
8870
+ const codexError = unwrapCodexJsonRpcError(error);
8871
+ return codexError?.code === "remote_error" ? codexError : null;
8872
+ }
8873
+ function isCodexRuntimeRequestError(error) {
8874
+ return Boolean(unwrapCodexJsonRpcError(error));
8875
+ }
8876
+ function isRemoteThreadBootstrapError(error) {
8877
+ const codexError = isCodexRemoteError(error);
8878
+ if (!codexError) {
8879
+ return false;
8880
+ }
8881
+ return codexError.message.includes("includeTurns is unavailable before first user message") || codexError.message.includes("is not materialized yet") || codexError.message.includes("no rollout found for thread id") || codexError.message.includes("failed to load rollout") || codexError.message.includes("rollout at") && codexError.message.includes("is empty");
8882
+ }
8883
+ function isUnsupportedHooksListError(error) {
8884
+ const codexError = isCodexRemoteError(error);
8885
+ if (!codexError) {
8886
+ return false;
8887
+ }
8888
+ const remoteCode = codexError.details?.code;
8889
+ const message = codexError.message.toLowerCase();
8890
+ return remoteCode === -32601 || message.includes("endpoint not found") || message.includes("method not found") || message.includes("hooks/list") && message.includes("not found");
8891
+ }
8892
+ function parseTurnSteerRace(error) {
8893
+ const codexError = isCodexRemoteError(error);
8894
+ if (!codexError) {
8895
+ return null;
8896
+ }
8897
+ if (codexError.message === "no active turn to steer") {
8898
+ return { type: "missing" };
8899
+ }
8900
+ const mismatchPrefix = "expected active turn id `";
8901
+ const mismatchSeparator = "` but found `";
8902
+ if (!codexError.message.startsWith(mismatchPrefix)) {
8903
+ return null;
8904
+ }
8905
+ const actualTurnId = codexError.message.slice(mismatchPrefix.length).split(mismatchSeparator)[1]?.replace(/`$/, "");
8906
+ if (!actualTurnId) {
8907
+ return null;
8908
+ }
8909
+ return {
8910
+ type: "turnIdMismatch",
8911
+ actualTurnId
8912
+ };
8913
+ }
8914
+
8915
+ // src/codex/codex-management-service.ts
8916
+ var GOAL_FEATURE_DISABLED_MESSAGE = "Codex /goal is experimental. Enable it by adding `goals = true` under `[features]` in ~/.codex/config.toml, then restart the Codex app-server.";
8917
+ var HOOK_EVENT_JSON_KEYS = {
8918
+ preToolUse: "PreToolUse",
8919
+ permissionRequest: "PermissionRequest",
8920
+ postToolUse: "PostToolUse",
8921
+ preCompact: "PreCompact",
8922
+ postCompact: "PostCompact",
8923
+ sessionStart: "SessionStart",
8924
+ userPromptSubmit: "UserPromptSubmit",
8925
+ stop: "Stop"
8926
+ };
8927
+ var HOOK_EVENT_DTO_KEYS = Object.fromEntries(
8928
+ Object.entries(HOOK_EVENT_JSON_KEYS).map(([dtoKey, jsonKey]) => [jsonKey, dtoKey])
8929
+ );
8930
+ function isRecord4(value) {
8931
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8932
+ }
8933
+ function normalizeHooksJson(value) {
8934
+ if (!isRecord4(value) || !isRecord4(value.hooks)) {
8935
+ return { hooks: {} };
8936
+ }
8937
+ const hooks = {};
8938
+ for (const [eventName, groups] of Object.entries(value.hooks)) {
8939
+ hooks[eventName] = Array.isArray(groups) ? groups : [];
8940
+ }
8941
+ return { ...value, hooks };
8942
+ }
8943
+ function readJsonFileOrDefault(filePath) {
8944
+ return fs5.readFile(filePath, "utf8").then((raw) => {
8945
+ if (!raw.trim()) {
8946
+ return { hooks: {} };
8947
+ }
8948
+ return normalizeHooksJson(JSON.parse(raw));
8949
+ }).catch((error) => {
8950
+ if (error.code === "ENOENT") {
8951
+ return { hooks: {} };
8952
+ }
8953
+ throw error;
8954
+ });
8955
+ }
8956
+ function validateHookInput(input) {
8957
+ if (!HOOK_EVENT_JSON_KEYS[input.eventName]) {
8958
+ throw new HttpError(400, {
8959
+ code: "bad_request",
8960
+ message: "Unsupported hook event."
8961
+ });
8962
+ }
8963
+ if (input.scope !== "global" && input.scope !== "project") {
8964
+ throw new HttpError(400, {
8965
+ code: "bad_request",
8966
+ message: "Hook scope must be global or project."
8967
+ });
8968
+ }
8969
+ if (!input.command.trim()) {
8970
+ throw new HttpError(400, {
8971
+ code: "bad_request",
8972
+ message: "Hook command cannot be empty."
8973
+ });
8974
+ }
8975
+ if (input.timeoutSec !== void 0 && input.timeoutSec !== null && (!Number.isInteger(input.timeoutSec) || input.timeoutSec <= 0 || input.timeoutSec > 86400)) {
8976
+ throw new HttpError(400, {
8977
+ code: "bad_request",
8978
+ message: "Hook timeout must be a positive number of seconds."
8979
+ });
8980
+ }
8981
+ }
8982
+ function hooksPathForInput(codexHome, workspacePath, input) {
8983
+ return input.scope === "global" ? path6.join(codexHome, "hooks.json") : path6.join(workspacePath, ".codex", "hooks.json");
8984
+ }
8985
+ function hookInputMatches(group, handler, input) {
8986
+ if (!isRecord4(group) || !isRecord4(handler)) {
8987
+ return false;
8988
+ }
8989
+ const matcher = typeof group.matcher === "string" ? group.matcher : null;
8990
+ const handlerCommand = typeof handler.command === "string" ? handler.command : "";
8991
+ const handlerTimeout = typeof handler.timeout === "number" && Number.isFinite(handler.timeout) ? handler.timeout : null;
8992
+ const handlerStatusMessage = typeof handler.statusMessage === "string" ? handler.statusMessage : null;
8993
+ return handler.type === "command" && (input.matcher?.trim() || null) === matcher && input.command.trim() === handlerCommand && (input.timeoutSec ?? null) === handlerTimeout && (input.statusMessage?.trim() || null) === handlerStatusMessage;
8994
+ }
8995
+ function hookMatchesInput(hook, input) {
8996
+ return hook.source === input.scope && hook.eventName === input.eventName && (hook.matcher ?? null) === (input.matcher ?? null) && hook.command === input.command && (input.timeoutSec == null || hook.timeoutSec === input.timeoutSec) && (hook.statusMessage ?? null) === (input.statusMessage ?? null);
8997
+ }
8998
+ async function findOfficialHookForInput(runtime, workspacePath, input) {
8999
+ if (!runtime.listHooks) {
9000
+ return null;
9001
+ }
9002
+ const [entry] = await runtime.listHooks({
9003
+ cwds: [workspacePath]
9004
+ });
9005
+ const officialHooks = (entry?.hooks ?? []).map((hook) => ({
9006
+ key: hook.key,
9007
+ eventName: hook.eventName,
9008
+ handlerType: hook.handlerType,
9009
+ matcher: hook.matcher,
9010
+ command: hook.command,
9011
+ timeoutSec: hook.timeoutSec,
9012
+ statusMessage: hook.statusMessage,
9013
+ sourcePath: hook.sourcePath,
9014
+ source: hook.source,
9015
+ pluginId: hook.pluginId,
9016
+ displayOrder: hook.displayOrder,
9017
+ enabled: hook.enabled,
9018
+ isManaged: hook.isManaged,
9019
+ currentHash: hook.currentHash,
9020
+ trustStatus: hook.trustStatus
9021
+ }));
9022
+ return officialHooks.find((hook) => hookMatchesInput(hook, input)) ?? null;
9023
+ }
9024
+ async function trustHookForInput(runtime, workspacePath, input) {
9025
+ const hook = await findOfficialHookForInput(runtime, workspacePath, input);
9026
+ if (!runtime.setHookTrust || !hook || !hook.key || !hook.currentHash || hook.isManaged) {
9027
+ return;
9028
+ }
9029
+ await runtime.setHookTrust({
9030
+ key: hook.key,
9031
+ trustedHash: hook.currentHash
9032
+ });
9033
+ }
9034
+ var CodexManagementService = class {
9035
+ constructor(codexHome) {
9036
+ this.codexHome = codexHome;
9037
+ }
9038
+ codexHome;
9039
+ readFastMode() {
9040
+ return readCodexFastModeSync(this.codexHome);
9041
+ }
9042
+ writeFastMode(enabled) {
9043
+ return writeCodexFastMode(this.codexHome, enabled);
9044
+ }
9045
+ mapGoalError(error) {
9046
+ const codexError = unwrapCodexJsonRpcError(error);
9047
+ if (codexError) {
9048
+ const remoteMessage = codexError.message || "";
9049
+ if (remoteMessage.toLowerCase().includes("goals feature is disabled")) {
9050
+ throw new HttpError(409, {
9051
+ code: "goal_feature_disabled",
9052
+ message: GOAL_FEATURE_DISABLED_MESSAGE
9053
+ });
9054
+ }
9055
+ throw new HttpError(502, {
9056
+ code: "provider_goal_error",
9057
+ message: remoteMessage || "Provider goal operation failed.",
9058
+ details: {
9059
+ provider: "codex"
9060
+ }
9061
+ });
9062
+ }
9063
+ throw error;
9064
+ }
9065
+ async ensureGoalsFeatureEnabled(runtime) {
9066
+ try {
9067
+ if (await readCodexFeatureFlag(this.codexHome, "goals")) {
9068
+ return;
9069
+ }
9070
+ await writeCodexFeatureFlag(this.codexHome, "goals", true);
9071
+ await runtime.stop();
9072
+ await runtime.start();
9073
+ } catch (error) {
9074
+ if (isCodexRuntimeRequestError(error)) {
9075
+ throw new HttpError(409, {
9076
+ code: "goal_feature_disabled",
9077
+ message: GOAL_FEATURE_DISABLED_MESSAGE
9078
+ });
9079
+ }
9080
+ throw error;
9081
+ }
9082
+ }
9083
+ isRuntimeRequestError(error) {
9084
+ return isCodexRuntimeRequestError(error);
9085
+ }
9086
+ async writeHookEntry(runtime, workspacePath, input) {
9087
+ validateHookInput(input);
9088
+ const hooksPath = hooksPathForInput(this.codexHome, workspacePath, input);
9089
+ const config = await readJsonFileOrDefault(hooksPath);
9090
+ const eventKey = HOOK_EVENT_JSON_KEYS[input.eventName];
9091
+ const matcher = input.matcher?.trim() || null;
9092
+ const handler = {
9093
+ type: "command",
9094
+ command: input.command.trim()
9095
+ };
9096
+ if (input.timeoutSec !== void 0 && input.timeoutSec !== null) {
9097
+ handler.timeout = input.timeoutSec;
9098
+ }
9099
+ if (input.statusMessage?.trim()) {
9100
+ handler.statusMessage = input.statusMessage.trim();
9101
+ }
9102
+ const group = {
9103
+ hooks: [handler]
9104
+ };
9105
+ if (matcher) {
9106
+ group.matcher = matcher;
9107
+ }
9108
+ const currentGroups = Array.isArray(config.hooks[eventKey]) ? config.hooks[eventKey] : [];
9109
+ config.hooks[eventKey] = [...currentGroups, group];
9110
+ await fs5.mkdir(path6.dirname(hooksPath), { recursive: true });
9111
+ await fs5.writeFile(hooksPath, `${JSON.stringify(config, null, 2)}
9112
+ `, "utf8");
9113
+ await trustHookForInput(runtime, workspacePath, input);
9114
+ }
9115
+ async updateHookEntry(runtime, workspacePath, input) {
9116
+ validateHookInput(input);
9117
+ validateHookInput(input.target);
9118
+ if (input.scope !== input.target.scope) {
9119
+ throw new HttpError(400, {
9120
+ code: "bad_request",
9121
+ message: "Hook scope cannot be changed while editing."
9122
+ });
9123
+ }
9124
+ const hooksPath = hooksPathForInput(this.codexHome, workspacePath, input);
9125
+ const config = await readJsonFileOrDefault(hooksPath);
9126
+ const targetEventKey = HOOK_EVENT_JSON_KEYS[input.target.eventName];
9127
+ const nextEventKey = HOOK_EVENT_JSON_KEYS[input.eventName];
9128
+ const currentGroups = Array.isArray(config.hooks[targetEventKey]) ? config.hooks[targetEventKey] : [];
9129
+ let replacementGroup = null;
9130
+ config.hooks[targetEventKey] = currentGroups.map((group) => {
9131
+ if (replacementGroup || !isRecord4(group) || !Array.isArray(group.hooks)) {
9132
+ return group;
9133
+ }
9134
+ const hookIndex = group.hooks.findIndex(
9135
+ (handler2) => hookInputMatches(group, handler2, input.target)
9136
+ );
9137
+ if (hookIndex < 0) {
9138
+ return group;
9139
+ }
9140
+ const handler = {
9141
+ type: "command",
9142
+ command: input.command.trim()
9143
+ };
9144
+ if (input.timeoutSec !== void 0 && input.timeoutSec !== null) {
9145
+ handler.timeout = input.timeoutSec;
9146
+ }
9147
+ if (input.statusMessage?.trim()) {
9148
+ handler.statusMessage = input.statusMessage.trim();
9149
+ }
9150
+ replacementGroup = {
9151
+ hooks: [handler]
9152
+ };
9153
+ const matcher = input.matcher?.trim() || null;
9154
+ if (matcher) {
9155
+ replacementGroup.matcher = matcher;
9156
+ }
9157
+ if (targetEventKey !== nextEventKey) {
9158
+ const remainingHooks = group.hooks.filter((_, index) => index !== hookIndex);
9159
+ return {
9160
+ ...group,
9161
+ hooks: remainingHooks
9162
+ };
9163
+ }
9164
+ return {
9165
+ ...replacementGroup,
9166
+ hooks: group.hooks.map(
9167
+ (existing, index) => index === hookIndex ? replacementGroup.hooks[0] : existing
9168
+ )
9169
+ };
9170
+ }).filter((group) => {
9171
+ if (!isRecord4(group) || !Array.isArray(group.hooks)) {
9172
+ return true;
9173
+ }
9174
+ return group.hooks.length > 0;
9175
+ });
9176
+ if (!replacementGroup) {
9177
+ throw new HttpError(404, {
9178
+ code: "not_found",
9179
+ message: "Hook was not found in hooks.json."
9180
+ });
9181
+ }
9182
+ if (targetEventKey !== nextEventKey) {
9183
+ if (config.hooks[targetEventKey]?.length === 0) {
9184
+ delete config.hooks[targetEventKey];
9185
+ }
9186
+ const nextGroups = Array.isArray(config.hooks[nextEventKey]) ? config.hooks[nextEventKey] : [];
9187
+ config.hooks[nextEventKey] = [...nextGroups, replacementGroup];
9188
+ }
9189
+ await fs5.mkdir(path6.dirname(hooksPath), { recursive: true });
9190
+ await fs5.writeFile(hooksPath, `${JSON.stringify(config, null, 2)}
9191
+ `, "utf8");
9192
+ await trustHookForInput(runtime, workspacePath, input);
9193
+ }
9194
+ async readLocalHookDtos(input) {
9195
+ const config = await readJsonFileOrDefault(input.hooksPath);
9196
+ const hooks = [];
9197
+ for (const [eventKey, groups] of Object.entries(config.hooks)) {
9198
+ const eventName = HOOK_EVENT_DTO_KEYS[eventKey];
9199
+ if (!eventName || !Array.isArray(groups)) {
9200
+ continue;
9201
+ }
9202
+ groups.forEach((group, groupIndex) => {
9203
+ if (!isRecord4(group) || !Array.isArray(group.hooks)) {
9204
+ return;
9205
+ }
9206
+ const matcher = typeof group.matcher === "string" ? group.matcher : null;
9207
+ group.hooks.forEach((handler, handlerIndex) => {
9208
+ if (!isRecord4(handler) || handler.type !== "command") {
9209
+ return;
9210
+ }
9211
+ const command = typeof handler.command === "string" ? handler.command : null;
9212
+ if (!command) {
9213
+ return;
9214
+ }
9215
+ const timeoutSec = typeof handler.timeout === "number" && Number.isFinite(handler.timeout) ? handler.timeout : 600;
9216
+ const statusMessage = typeof handler.statusMessage === "string" ? handler.statusMessage : null;
9217
+ const key = `${input.source}:${input.hooksPath}:${eventKey}:${groupIndex}:${handlerIndex}`;
9218
+ hooks.push({
9219
+ key,
9220
+ eventName,
9221
+ handlerType: "command",
9222
+ matcher,
9223
+ command,
9224
+ timeoutSec,
9225
+ statusMessage,
9226
+ sourcePath: input.hooksPath,
9227
+ source: input.source,
9228
+ pluginId: null,
9229
+ displayOrder: input.displayOffset + hooks.length,
9230
+ enabled: true,
9231
+ isManaged: false,
9232
+ currentHash: "",
9233
+ trustStatus: "untrusted"
9234
+ });
9235
+ });
9236
+ });
9237
+ }
9238
+ return hooks;
9239
+ }
9240
+ hooksPaths(workspacePath) {
9241
+ return {
9242
+ globalHooksPath: path6.join(this.codexHome, "hooks.json"),
9243
+ projectHooksPath: path6.join(workspacePath, ".codex", "hooks.json")
9244
+ };
9245
+ }
9246
+ };
9247
+
9248
+ // src/codex/local-session-store.ts
9249
+ import fs6 from "fs/promises";
9250
+ import path7 from "path";
9251
+ import Database2 from "better-sqlite3";
9252
+
9253
+ // src/codex/thread-title.ts
9254
+ var AUTO_THREAD_TITLE_MAX_CHARS = 15;
9255
+ function normalizeWhitespace(value) {
9256
+ return value.replace(/\s+/g, " ").trim();
9257
+ }
9258
+ function truncateAutoThreadTitle(value) {
9259
+ const normalized = normalizeWhitespace(value);
9260
+ if (!normalized) {
9261
+ return "";
9262
+ }
9263
+ const characters = Array.from(normalized);
9264
+ if (characters.length <= AUTO_THREAD_TITLE_MAX_CHARS) {
9265
+ return normalized;
9266
+ }
9267
+ return `${characters.slice(0, AUTO_THREAD_TITLE_MAX_CHARS).join("")}...`;
9268
+ }
9269
+
9270
+ // src/codex/local-session-store.ts
9271
+ function basenameFromPath(absPath) {
9272
+ const normalized = absPath.replace(/[\\/]+$/, "");
9273
+ return normalized.split(/[\\/]/).at(-1) ?? normalized;
9274
+ }
9275
+ function summarizeTitleFromTurns(turns) {
9276
+ const firstUserMessage = turns.flatMap((turn) => turn.items).find((item) => item.kind === "userMessage" && item.text.trim());
9277
+ if (!firstUserMessage) {
9278
+ return null;
9279
+ }
9280
+ return truncateAutoThreadTitle(firstUserMessage.text);
9281
+ }
9282
+ function createHistoryItemId(turnId, prefix, index) {
9283
+ return `${turnId}-${prefix}-${index}`;
9284
+ }
9285
+ function finalizeTurn(turn, turns) {
9286
+ if (!turn || turn.items.length === 0) {
9287
+ return;
9288
+ }
9289
+ turns.push({
9290
+ id: turn.id,
9291
+ startedAt: turn.startedAt,
9292
+ status: turn.status,
9293
+ error: turn.error,
9294
+ items: turn.items
9295
+ });
9296
+ }
9297
+ function parseTranscript(contents) {
9298
+ const turns = [];
9299
+ let cwd = null;
9300
+ let currentTurn = null;
9301
+ let fallbackTurnCount = 0;
9302
+ let agentItemCount = 0;
9303
+ let userItemCount = 0;
9304
+ const ensureCurrentTurn = (timestamp) => {
9305
+ if (currentTurn) {
9306
+ return currentTurn;
9307
+ }
9308
+ fallbackTurnCount += 1;
9309
+ currentTurn = {
9310
+ id: `local-turn-${fallbackTurnCount}`,
9311
+ startedAt: timestamp ?? null,
9312
+ status: "inProgress",
9313
+ error: null,
9314
+ items: []
9315
+ };
9316
+ agentItemCount = 0;
9317
+ userItemCount = 0;
9318
+ return currentTurn;
9319
+ };
9320
+ for (const line of contents.split("\n")) {
9321
+ if (!line.trim()) {
9322
+ continue;
9323
+ }
9324
+ let entry;
9325
+ try {
9326
+ entry = JSON.parse(line);
9327
+ } catch {
9328
+ continue;
9329
+ }
9330
+ if (entry.type === "session_meta") {
9331
+ const payload2 = entry.payload ?? {};
9332
+ if (typeof payload2.cwd === "string" && payload2.cwd.trim()) {
9333
+ cwd = payload2.cwd;
9334
+ }
9335
+ continue;
9336
+ }
9337
+ if (entry.type !== "event_msg") {
9338
+ continue;
9339
+ }
9340
+ const payload = entry.payload ?? {};
9341
+ const payloadType = payload.type;
9342
+ if (payloadType === "task_started") {
9343
+ finalizeTurn(currentTurn, turns);
9344
+ currentTurn = {
9345
+ id: typeof payload.turn_id === "string" && payload.turn_id.trim() ? payload.turn_id : `local-turn-${fallbackTurnCount + 1}`,
9346
+ startedAt: entry.timestamp ?? null,
9347
+ status: "inProgress",
9348
+ error: null,
9349
+ items: []
9350
+ };
9351
+ agentItemCount = 0;
9352
+ userItemCount = 0;
9353
+ continue;
9354
+ }
9355
+ if (payloadType === "user_message" && typeof payload.message === "string") {
9356
+ const turn = ensureCurrentTurn(entry.timestamp);
9357
+ userItemCount += 1;
9358
+ turn.items.push({
9359
+ id: createHistoryItemId(turn.id, "user", userItemCount),
9360
+ kind: "userMessage",
9361
+ text: payload.message
9362
+ });
9363
+ continue;
9364
+ }
9365
+ if (payloadType === "agent_message" && typeof payload.message === "string") {
9366
+ const turn = ensureCurrentTurn(entry.timestamp);
9367
+ agentItemCount += 1;
9368
+ turn.items.push({
9369
+ id: createHistoryItemId(turn.id, "agent", agentItemCount),
9370
+ kind: "agentMessage",
9371
+ text: payload.message,
9372
+ status: typeof payload.phase === "string" ? payload.phase : null
9373
+ });
9374
+ continue;
9375
+ }
9376
+ if (payloadType === "task_complete") {
9377
+ const turn = ensureCurrentTurn(entry.timestamp);
9378
+ turn.status = turn.error ? "failed" : "completed";
9379
+ finalizeTurn(turn, turns);
9380
+ currentTurn = null;
9381
+ continue;
9382
+ }
9383
+ if (payloadType === "error") {
9384
+ const turn = ensureCurrentTurn(entry.timestamp);
9385
+ turn.status = "failed";
9386
+ turn.error = typeof payload.message === "string" ? payload.message : "Local Codex session failed.";
9387
+ }
9388
+ }
9389
+ finalizeTurn(currentTurn, turns);
9390
+ return {
9391
+ cwd,
9392
+ title: summarizeTitleFromTurns(turns),
9393
+ turns
9394
+ };
9395
+ }
9396
+ async function fileExists(filePath) {
9397
+ try {
9398
+ await fs6.access(filePath);
9399
+ return true;
9400
+ } catch {
9401
+ return false;
9402
+ }
9403
+ }
9404
+ var LocalCodexSessionStore = class {
9405
+ constructor(codexHome) {
9406
+ this.codexHome = codexHome;
9407
+ }
9408
+ codexHome;
9409
+ async findSession(sessionId) {
8805
9410
  const stateRecord = await this.findSessionInStateDatabases(sessionId);
8806
9411
  const transcriptPath = await this.resolveTranscriptPath(
8807
9412
  stateRecord?.rolloutPath ?? null,
8808
9413
  sessionId
8809
9414
  );
8810
- const transcript = transcriptPath ? parseTranscript(await fs4.readFile(transcriptPath, "utf8")) : null;
9415
+ const transcript = transcriptPath ? parseTranscript(await fs6.readFile(transcriptPath, "utf8")) : null;
8811
9416
  const cwd = stateRecord?.cwd ?? transcript?.cwd ?? null;
8812
9417
  if (!cwd) {
8813
9418
  return null;
@@ -8824,14 +9429,14 @@ var LocalCodexSessionStore = class {
8824
9429
  async findSessionInStateDatabases(sessionId) {
8825
9430
  let entries;
8826
9431
  try {
8827
- entries = await fs4.readdir(this.codexHome);
9432
+ entries = await fs6.readdir(this.codexHome);
8828
9433
  } catch {
8829
9434
  return null;
8830
9435
  }
8831
9436
  const stateFiles = await Promise.all(
8832
9437
  entries.filter((entry) => /^state_\d+\.sqlite$/i.test(entry)).map(async (entry) => {
8833
- const absPath = path5.join(this.codexHome, entry);
8834
- const stats = await fs4.stat(absPath);
9438
+ const absPath = path7.join(this.codexHome, entry);
9439
+ const stats = await fs6.stat(absPath);
8835
9440
  return {
8836
9441
  absPath,
8837
9442
  mtimeMs: stats.mtimeMs
@@ -8870,22 +9475,22 @@ var LocalCodexSessionStore = class {
8870
9475
  }
8871
9476
  async resolveTranscriptPath(rolloutPath, sessionId) {
8872
9477
  if (rolloutPath?.trim()) {
8873
- const absolutePath = path5.isAbsolute(rolloutPath) ? rolloutPath : path5.resolve(this.codexHome, rolloutPath);
9478
+ const absolutePath = path7.isAbsolute(rolloutPath) ? rolloutPath : path7.resolve(this.codexHome, rolloutPath);
8874
9479
  if (await fileExists(absolutePath)) {
8875
9480
  return absolutePath;
8876
9481
  }
8877
9482
  }
8878
- return this.findTranscriptFile(path5.join(this.codexHome, "sessions"), sessionId);
9483
+ return this.findTranscriptFile(path7.join(this.codexHome, "sessions"), sessionId);
8879
9484
  }
8880
9485
  async findTranscriptFile(directory, sessionId) {
8881
9486
  let entries;
8882
9487
  try {
8883
- entries = await fs4.readdir(directory, { withFileTypes: true });
9488
+ entries = await fs6.readdir(directory, { withFileTypes: true });
8884
9489
  } catch {
8885
9490
  return null;
8886
9491
  }
8887
9492
  for (const entry of entries) {
8888
- const absPath = path5.join(directory, entry.name);
9493
+ const absPath = path7.join(directory, entry.name);
8889
9494
  if (entry.isDirectory()) {
8890
9495
  const nested = await this.findTranscriptFile(absPath, sessionId);
8891
9496
  if (nested) {
@@ -8903,23 +9508,33 @@ var LocalCodexSessionStore = class {
8903
9508
 
8904
9509
  // src/agent-runtime-bootstrap.ts
8905
9510
  function createAgentRuntimeBootstrap(config) {
8906
- const codexRuntime = new CodexRuntimeAdapter(
9511
+ const runtimes = [];
9512
+ const providerHostHomes = {};
9513
+ const codexConfig = config.agentProviders.codex;
9514
+ const codexRuntime = codexConfig.enabled ? new CodexRuntimeAdapter(
8907
9515
  new CodexAppServerManager({
8908
- command: config.codexCommand,
8909
- startupTimeoutMs: config.codexAppServerStartTimeoutMs,
9516
+ command: codexConfig.command,
9517
+ startupTimeoutMs: codexConfig.appServerStartTimeoutMs,
8910
9518
  clientInfo: {
8911
9519
  name: "remote-codex-supervisor",
8912
9520
  title: config.appName,
8913
9521
  version: config.appVersion
8914
9522
  }
8915
9523
  })
8916
- );
9524
+ ) : null;
9525
+ if (codexRuntime) {
9526
+ runtimes.push(codexRuntime);
9527
+ providerHostHomes.codex = codexConfig.home;
9528
+ }
9529
+ const claudeConfig = config.agentProviders.claude;
9530
+ if (claudeConfig.enabled) {
9531
+ providerHostHomes.claude = claudeConfig.home;
9532
+ }
8917
9533
  return {
8918
- agentRuntimes: new AgentRuntimeRegistry([codexRuntime]),
8919
- localCodexSessionStore: new LocalCodexSessionStore(config.codexHome),
8920
- providerHostHomes: {
8921
- codex: config.codexHome
8922
- }
9534
+ agentRuntimes: new AgentRuntimeRegistry(runtimes),
9535
+ localCodexSessionStore: new LocalCodexSessionStore(codexConfig.home),
9536
+ codexManagement: new CodexManagementService(codexConfig.home),
9537
+ providerHostHomes
8923
9538
  };
8924
9539
  }
8925
9540
 
@@ -8948,11 +9563,11 @@ var SupervisorEventBus = class extends EventEmitter4 {
8948
9563
 
8949
9564
  // src/thread-service.ts
8950
9565
  import { randomUUID as randomUUID2 } from "crypto";
8951
- import fs8 from "fs/promises";
8952
- import path8 from "path";
9566
+ import fs9 from "fs/promises";
9567
+ import path9 from "path";
8953
9568
 
8954
9569
  // src/exports/thread-pdf-export.ts
8955
- import fs5 from "fs";
9570
+ import fs7 from "fs";
8956
9571
  import puppeteer from "puppeteer-core";
8957
9572
  import { marked } from "marked";
8958
9573
  var MAX_TEXT_CHARS = 12e3;
@@ -8985,10 +9600,10 @@ function renderEmbeddedCjkFontCss() {
8985
9600
  embeddedCjkFontCss = "";
8986
9601
  for (const candidate of EMBEDDED_CJK_FONT_CANDIDATES) {
8987
9602
  try {
8988
- if (!fs5.existsSync(candidate.path)) {
9603
+ if (!fs7.existsSync(candidate.path)) {
8989
9604
  continue;
8990
9605
  }
8991
- const font = fs5.readFileSync(candidate.path);
9606
+ const font = fs7.readFileSync(candidate.path);
8992
9607
  embeddedCjkFontCss = `
8993
9608
  @font-face {
8994
9609
  font-family: "RemoteCodexCJK";
@@ -10030,208 +10645,6 @@ async function launchPdfBrowser() {
10030
10645
  }
10031
10646
  }
10032
10647
 
10033
- // src/codex/codexHostConfig.ts
10034
- import fs6 from "fs";
10035
- import fsp from "fs/promises";
10036
- import path6 from "path";
10037
- var SERVICE_TIER_LINE_GLOBAL_PATTERN = /^\s*service_tier\s*=\s*("fast"|"flex"|'fast'|'flex').*\n?/gm;
10038
- function resolveCodexConfigPath(codexHome) {
10039
- return path6.join(codexHome, "config.toml");
10040
- }
10041
- function parseCodexServiceTier(content) {
10042
- const match = content.match(
10043
- /^\s*service_tier\s*=\s*["'](?<tier>fast)["']\s*$/m
10044
- );
10045
- const tier = match?.groups?.tier;
10046
- return tier === "fast" ? tier : null;
10047
- }
10048
- function isFastModeEnabledFromConfig(content) {
10049
- return parseCodexServiceTier(content) === "fast";
10050
- }
10051
- function readCodexFastModeSync(codexHome) {
10052
- try {
10053
- const content = fs6.readFileSync(resolveCodexConfigPath(codexHome), "utf8");
10054
- return isFastModeEnabledFromConfig(content);
10055
- } catch (error) {
10056
- if (error.code === "ENOENT") {
10057
- return false;
10058
- }
10059
- throw error;
10060
- }
10061
- }
10062
- function upsertCodexServiceTier(content, enabled) {
10063
- const normalized = content.replace(/\r\n/g, "\n");
10064
- const withoutServiceTier = normalized.replace(
10065
- SERVICE_TIER_LINE_GLOBAL_PATTERN,
10066
- ""
10067
- );
10068
- if (!enabled) {
10069
- return withoutServiceTier.replace(/\n{3,}/g, "\n\n").trimEnd() + (withoutServiceTier.trim() ? "\n" : "");
10070
- }
10071
- const nextLine = 'service_tier = "fast"';
10072
- if (!withoutServiceTier.trim()) {
10073
- return `${nextLine}
10074
- `;
10075
- }
10076
- const firstSectionMatch = withoutServiceTier.match(/^\s*\[[^\]]+\]/m);
10077
- if (!firstSectionMatch || firstSectionMatch.index === void 0) {
10078
- return withoutServiceTier.endsWith("\n") ? `${withoutServiceTier}${nextLine}
10079
- ` : `${withoutServiceTier}
10080
- ${nextLine}
10081
- `;
10082
- }
10083
- const beforeFirstSection = withoutServiceTier.slice(0, firstSectionMatch.index).trimEnd();
10084
- const afterFirstSection = withoutServiceTier.slice(firstSectionMatch.index).replace(/^\n+/, "");
10085
- return beforeFirstSection ? `${beforeFirstSection}
10086
- ${nextLine}
10087
- ${afterFirstSection}` : `${nextLine}
10088
- ${afterFirstSection}`;
10089
- }
10090
- async function writeCodexFastMode(codexHome, enabled) {
10091
- const configPath = resolveCodexConfigPath(codexHome);
10092
- let current = "";
10093
- try {
10094
- current = await fsp.readFile(configPath, "utf8");
10095
- } catch (error) {
10096
- if (error.code !== "ENOENT") {
10097
- throw error;
10098
- }
10099
- }
10100
- const next = upsertCodexServiceTier(current, enabled);
10101
- await fsp.mkdir(path6.dirname(configPath), { recursive: true });
10102
- await fsp.writeFile(configPath, next, "utf8");
10103
- return next;
10104
- }
10105
- function isCodexFeatureEnabledFromConfig(content, featureName) {
10106
- const escaped = featureName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10107
- const featuresMatch = content.match(/^[^\S\r\n]*\[features\][^\S\r\n]*$/m);
10108
- if (!featuresMatch || featuresMatch.index === void 0) {
10109
- return false;
10110
- }
10111
- const sectionStart = featuresMatch.index + featuresMatch[0].length;
10112
- const nextSectionMatch = content.slice(sectionStart).match(/^[^\S\r\n]*\[[^\]\r\n]+\][^\S\r\n]*$/m);
10113
- const sectionEnd = nextSectionMatch && nextSectionMatch.index !== void 0 ? sectionStart + nextSectionMatch.index : content.length;
10114
- const featuresBody = content.slice(sectionStart, sectionEnd);
10115
- return new RegExp(`^\\s*${escaped}\\s*=\\s*true\\s*(?:#.*)?$`, "m").test(featuresBody);
10116
- }
10117
- async function readCodexFeatureFlag(codexHome, featureName) {
10118
- try {
10119
- const content = await fsp.readFile(resolveCodexConfigPath(codexHome), "utf8");
10120
- return isCodexFeatureEnabledFromConfig(content, featureName);
10121
- } catch (error) {
10122
- if (error.code === "ENOENT") {
10123
- return false;
10124
- }
10125
- throw error;
10126
- }
10127
- }
10128
- function upsertCodexFeatureFlag(content, featureName, enabled) {
10129
- const normalized = content.replace(/\r\n/g, "\n");
10130
- const escaped = featureName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10131
- const featuresMatch = normalized.match(/^[^\S\r\n]*\[features\][^\S\r\n]*$/m);
10132
- const nextLine = `${featureName} = ${enabled ? "true" : "false"}`;
10133
- if (!featuresMatch || featuresMatch.index === void 0) {
10134
- const prefix = normalized.trimEnd();
10135
- return prefix ? `${prefix}
10136
-
10137
- [features]
10138
- ${nextLine}
10139
- ` : `[features]
10140
- ${nextLine}
10141
- `;
10142
- }
10143
- const sectionStart = featuresMatch.index + featuresMatch[0].length;
10144
- const nextSectionMatch = normalized.slice(sectionStart).match(/^[^\S\r\n]*\[[^\]\r\n]+\][^\S\r\n]*$/m);
10145
- const sectionEnd = nextSectionMatch && nextSectionMatch.index !== void 0 ? sectionStart + nextSectionMatch.index : normalized.length;
10146
- const beforeSectionBody = normalized.slice(0, sectionStart);
10147
- const sectionBody = normalized.slice(sectionStart, sectionEnd);
10148
- const afterSection = normalized.slice(sectionEnd);
10149
- const flagPattern = new RegExp(
10150
- `(^[^\\S\\r\\n]*)${escaped}[^\\S\\r\\n]*=[^\\S\\r\\n]*(true|false)[^\\S\\r\\n]*(?:#.*)?$`,
10151
- "m"
10152
- );
10153
- if (flagPattern.test(sectionBody)) {
10154
- return beforeSectionBody + sectionBody.replace(flagPattern, `$1${nextLine}`) + afterSection;
10155
- }
10156
- const bodyWithFlag = `${sectionBody.replace(/\n*$/, "")}
10157
- ${nextLine}
10158
- `;
10159
- const normalizedAfterSection = afterSection.startsWith("\n") ? afterSection : `
10160
- ${afterSection}`;
10161
- return beforeSectionBody + bodyWithFlag + normalizedAfterSection;
10162
- }
10163
- async function writeCodexFeatureFlag(codexHome, featureName, enabled) {
10164
- const configPath = resolveCodexConfigPath(codexHome);
10165
- let current = "";
10166
- try {
10167
- current = await fsp.readFile(configPath, "utf8");
10168
- } catch (error) {
10169
- if (error.code !== "ENOENT") {
10170
- throw error;
10171
- }
10172
- }
10173
- const next = upsertCodexFeatureFlag(current, featureName, enabled);
10174
- await fsp.mkdir(path6.dirname(configPath), { recursive: true });
10175
- await fsp.writeFile(configPath, next, "utf8");
10176
- return next;
10177
- }
10178
-
10179
- // src/codex/runtime-errors.ts
10180
- function unwrapCodexJsonRpcError(error) {
10181
- if (error instanceof JsonRpcClientError) {
10182
- return error;
10183
- }
10184
- if (error instanceof AgentRuntimeError && error.provider === "codex") {
10185
- return error.cause instanceof JsonRpcClientError ? error.cause : null;
10186
- }
10187
- return null;
10188
- }
10189
- function isCodexRemoteError(error) {
10190
- const codexError = unwrapCodexJsonRpcError(error);
10191
- return codexError?.code === "remote_error" ? codexError : null;
10192
- }
10193
- function isCodexRuntimeRequestError(error) {
10194
- return Boolean(unwrapCodexJsonRpcError(error));
10195
- }
10196
- function isRemoteThreadBootstrapError(error) {
10197
- const codexError = isCodexRemoteError(error);
10198
- if (!codexError) {
10199
- return false;
10200
- }
10201
- return codexError.message.includes("includeTurns is unavailable before first user message") || codexError.message.includes("is not materialized yet") || codexError.message.includes("no rollout found for thread id") || codexError.message.includes("failed to load rollout") || codexError.message.includes("rollout at") && codexError.message.includes("is empty");
10202
- }
10203
- function isUnsupportedHooksListError(error) {
10204
- const codexError = isCodexRemoteError(error);
10205
- if (!codexError) {
10206
- return false;
10207
- }
10208
- const remoteCode = codexError.details?.code;
10209
- const message = codexError.message.toLowerCase();
10210
- return remoteCode === -32601 || message.includes("endpoint not found") || message.includes("method not found") || message.includes("hooks/list") && message.includes("not found");
10211
- }
10212
- function parseTurnSteerRace(error) {
10213
- const codexError = isCodexRemoteError(error);
10214
- if (!codexError) {
10215
- return null;
10216
- }
10217
- if (codexError.message === "no active turn to steer") {
10218
- return { type: "missing" };
10219
- }
10220
- const mismatchPrefix = "expected active turn id `";
10221
- const mismatchSeparator = "` but found `";
10222
- if (!codexError.message.startsWith(mismatchPrefix)) {
10223
- return null;
10224
- }
10225
- const actualTurnId = codexError.message.slice(mismatchPrefix.length).split(mismatchSeparator)[1]?.replace(/`$/, "");
10226
- if (!actualTurnId) {
10227
- return null;
10228
- }
10229
- return {
10230
- type: "turnIdMismatch",
10231
- actualTurnId
10232
- };
10233
- }
10234
-
10235
10648
  // src/thread-history-items.ts
10236
10649
  var DEFERRED_COMMAND_DETAIL_TITLE2 = "Command Output";
10237
10650
  var DEFERRED_TOOL_DETAIL_TITLE2 = "Tool Call Details";
@@ -10333,7 +10746,52 @@ function sortTurnItemsByRecordedSequence(items) {
10333
10746
  leadingItems.push(items[index]);
10334
10747
  index += 1;
10335
10748
  }
10336
- return [...leadingItems, ...sortHistoryItemsBySequence(items.slice(index))];
10749
+ const trailingItems = items.slice(index);
10750
+ if (!trailingItems.some(hasHistoryItemSequence)) {
10751
+ return items;
10752
+ }
10753
+ const sequenceValues = trailingItems.map((item) => historyItemSequence(item)).filter(Number.isFinite);
10754
+ const maxSequence = sequenceValues.length > 0 ? Math.max(...sequenceValues) : 0;
10755
+ const orderedItems = [];
10756
+ let cursor = 0;
10757
+ while (cursor < trailingItems.length) {
10758
+ const item = trailingItems[cursor];
10759
+ if (hasHistoryItemSequence(item)) {
10760
+ orderedItems.push({ item, index: cursor, order: historyItemSequence(item) });
10761
+ cursor += 1;
10762
+ continue;
10763
+ }
10764
+ const blockStart = cursor;
10765
+ while (cursor < trailingItems.length && !hasHistoryItemSequence(trailingItems[cursor])) {
10766
+ cursor += 1;
10767
+ }
10768
+ const block = trailingItems.slice(blockStart, cursor);
10769
+ const previousSequenced = [...trailingItems.slice(0, blockStart)].reverse().find(hasHistoryItemSequence);
10770
+ const nextSequenced = trailingItems.slice(cursor).find(hasHistoryItemSequence);
10771
+ const previousSequence = previousSequenced ? historyItemSequence(previousSequenced) : null;
10772
+ const nextSequence = nextSequenced ? historyItemSequence(nextSequenced) : null;
10773
+ block.forEach((blockItem, blockIndex) => {
10774
+ let order;
10775
+ if (previousSequence === null && nextSequence !== null) {
10776
+ order = nextSequence - (block.length - blockIndex) / (block.length + 1);
10777
+ } else if (previousSequence !== null && nextSequence !== null && nextSequence > previousSequence) {
10778
+ const span = nextSequence - previousSequence;
10779
+ order = previousSequence + (blockIndex + 1) / (block.length + 1) * span;
10780
+ } else {
10781
+ order = maxSequence + 1 + blockIndex / (block.length + 1);
10782
+ }
10783
+ orderedItems.push({
10784
+ item: blockItem,
10785
+ index: blockStart + blockIndex,
10786
+ order
10787
+ });
10788
+ });
10789
+ }
10790
+ const sortedTrailingItems = orderedItems.sort((left, right) => {
10791
+ const orderDelta = left.order - right.order;
10792
+ return orderDelta === 0 ? left.index - right.index : orderDelta;
10793
+ }).map((entry) => entry.item);
10794
+ return [...leadingItems, ...sortedTrailingItems];
10337
10795
  }
10338
10796
  function mergeHistoryItemsBySequence(items, missingItems) {
10339
10797
  if (missingItems.length === 0) {
@@ -10457,25 +10915,25 @@ function applyRecordedTurnItemOrders(turns, turnItemOrder) {
10457
10915
  }
10458
10916
 
10459
10917
  // src/codex/modelPricing.ts
10460
- import fs7 from "fs";
10461
- import path7 from "path";
10918
+ import fs8 from "fs";
10919
+ import path8 from "path";
10462
10920
  var TOKEN_PRICE_DENOMINATOR = 1e6;
10463
10921
  var cachedPricingConfig = null;
10464
10922
  function resolvePackageRoot2(start = process.cwd()) {
10465
10923
  if (process.env.REMOTE_CODEX_PACKAGE_ROOT) {
10466
- return path7.resolve(process.env.REMOTE_CODEX_PACKAGE_ROOT);
10924
+ return path8.resolve(process.env.REMOTE_CODEX_PACKAGE_ROOT);
10467
10925
  }
10468
- let current = path7.resolve(start);
10469
- while (current !== path7.dirname(current)) {
10470
- if (fs7.existsSync(path7.join(current, "pnpm-workspace.yaml"))) {
10926
+ let current = path8.resolve(start);
10927
+ while (current !== path8.dirname(current)) {
10928
+ if (fs8.existsSync(path8.join(current, "pnpm-workspace.yaml"))) {
10471
10929
  return current;
10472
10930
  }
10473
- current = path7.dirname(current);
10931
+ current = path8.dirname(current);
10474
10932
  }
10475
10933
  throw new Error("Unable to locate package root for Codex pricing config.");
10476
10934
  }
10477
10935
  function getPricingConfigPath() {
10478
- return path7.join(resolvePackageRoot2(), "config", "codex-model-pricing.json");
10936
+ return path8.join(resolvePackageRoot2(), "config", "codex-model-pricing.json");
10479
10937
  }
10480
10938
  function isPositiveNumber(value) {
10481
10939
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
@@ -10552,7 +11010,7 @@ function getPricingConfig() {
10552
11010
  return cachedPricingConfig;
10553
11011
  }
10554
11012
  const configPath = getPricingConfigPath();
10555
- const content = fs7.readFileSync(configPath, "utf8");
11013
+ const content = fs8.readFileSync(configPath, "utf8");
10556
11014
  cachedPricingConfig = parsePricingConfig(JSON.parse(content));
10557
11015
  return cachedPricingConfig;
10558
11016
  }
@@ -10624,29 +11082,15 @@ function estimateTurnPrice(usage, snapshot) {
10624
11082
  };
10625
11083
  }
10626
11084
 
10627
- // src/thread-service.ts
10628
- var DEFAULT_THREAD_TITLE = "Untitled thread";
10629
- var GENERIC_REMOTE_THREAD_TITLE = "Thread";
10630
- var LOCAL_PLAN_DECISION_PREFIX = "plan-decision:";
10631
- var IMPLEMENT_APPROVED_PLAN_PROMPT = "Implement the approved plan.";
10632
- var THREAD_DETAIL_CACHE_TTL_MS = 5e3;
10633
- var CONTEXT_BASELINE_TOKENS = 12e3;
10634
- var FAST_MODE_NOTE_ON = "Fast mode on";
10635
- var FAST_MODE_NOTE_OFF = "Fast mode off";
10636
- var HOOK_EVENT_JSON_KEYS = {
10637
- preToolUse: "PreToolUse",
10638
- permissionRequest: "PermissionRequest",
10639
- postToolUse: "PostToolUse",
10640
- preCompact: "PreCompact",
10641
- postCompact: "PostCompact",
10642
- sessionStart: "SessionStart",
10643
- userPromptSubmit: "UserPromptSubmit",
10644
- stop: "Stop"
10645
- };
10646
- var HOOK_EVENT_DTO_KEYS = Object.fromEntries(
10647
- Object.entries(HOOK_EVENT_JSON_KEYS).map(([dtoKey, jsonKey]) => [jsonKey, dtoKey])
10648
- );
10649
- var GOAL_FEATURE_DISABLED_MESSAGE = "Codex /goal is experimental. Enable it by adding `goals = true` under `[features]` in ~/.codex/config.toml, then restart the Codex app-server.";
11085
+ // src/thread-service.ts
11086
+ var DEFAULT_THREAD_TITLE = "Untitled thread";
11087
+ var GENERIC_REMOTE_THREAD_TITLE = "Thread";
11088
+ var LOCAL_PLAN_DECISION_PREFIX = "plan-decision:";
11089
+ var IMPLEMENT_APPROVED_PLAN_PROMPT = "Implement the approved plan.";
11090
+ var THREAD_DETAIL_CACHE_TTL_MS = 5e3;
11091
+ var CONTEXT_BASELINE_TOKENS = 12e3;
11092
+ var FAST_MODE_NOTE_ON = "Fast mode on";
11093
+ var FAST_MODE_NOTE_OFF = "Fast mode off";
10650
11094
  function toIsoFromEpoch2(value) {
10651
11095
  if (typeof value !== "number" || !Number.isFinite(value)) {
10652
11096
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -10718,22 +11162,36 @@ function mergeGoalHistoryEntry(existing, incoming) {
10718
11162
  localGoalId: latest.localGoalId ?? fallback.localGoalId ?? null
10719
11163
  };
10720
11164
  }
10721
- function mapCodexGoalError(error) {
10722
- const codexError = unwrapCodexJsonRpcError(error);
10723
- if (codexError) {
10724
- const remoteMessage = codexError.message || "";
10725
- if (remoteMessage.toLowerCase().includes("goals feature is disabled")) {
10726
- throw new HttpError(409, {
10727
- code: "goal_feature_disabled",
10728
- message: GOAL_FEATURE_DISABLED_MESSAGE
10729
- });
10730
- }
10731
- throw new HttpError(502, {
10732
- code: "codex_goal_error",
10733
- message: remoteMessage || "Codex goal operation failed."
10734
- });
11165
+ function goalHistoryStatusRank(status) {
11166
+ return ["active", "paused", "budgetLimited"].includes(status) ? 0 : 1;
11167
+ }
11168
+ function resetGoalProgress(goal) {
11169
+ return {
11170
+ ...goal,
11171
+ tokensUsed: 0,
11172
+ timeUsedSeconds: 0
11173
+ };
11174
+ }
11175
+ function goalObjectiveChanged(existing, nextObjective) {
11176
+ return existing !== null && typeof nextObjective === "string" && nextObjective.trim().length > 0 && nextObjective !== existing.objective;
11177
+ }
11178
+ function isLocalGoalStatus(status) {
11179
+ return ["active", "paused", "budgetLimited"].includes(status);
11180
+ }
11181
+ function localGoalSnapshotForFallback(goalHistory) {
11182
+ const activeGoal = goalHistory.find((entry) => isLocalGoalStatus(entry.status)) ?? null;
11183
+ if (activeGoal) {
11184
+ return activeGoal;
10735
11185
  }
10736
- throw error;
11186
+ return goalHistory.find((entry) => entry.status === "terminated") ?? null;
11187
+ }
11188
+ function localGoalSnapshotToPreserve(goalHistory, remoteGoal) {
11189
+ const activeGoal = goalHistory.find((entry) => isLocalGoalStatus(entry.status)) ?? null;
11190
+ const hasTerminatedHistory = goalHistory.some((entry) => entry.status === "terminated");
11191
+ if (activeGoal && hasTerminatedHistory && isLocalGoalStatus(remoteGoal.status) && activeGoal.objective === remoteGoal.objective && activeGoal.tokensUsed === 0 && activeGoal.timeUsedSeconds === 0 && (remoteGoal.tokensUsed > 0 || remoteGoal.timeUsedSeconds > 0)) {
11192
+ return activeGoal;
11193
+ }
11194
+ return activeGoal ? null : goalHistory.find((entry) => entry.status === "terminated") ?? null;
10737
11195
  }
10738
11196
  function defaultSandboxModeForApprovalMode(approvalMode) {
10739
11197
  return approvalMode === "guarded" ? "workspace-write" : "danger-full-access";
@@ -10748,34 +11206,6 @@ function normalizeSandboxMode(value) {
10748
11206
  return null;
10749
11207
  }
10750
11208
  }
10751
- function buildTurnSandboxPolicy(sandboxMode, writableRoot) {
10752
- switch (sandboxMode) {
10753
- case "danger-full-access":
10754
- return {
10755
- type: "dangerFullAccess"
10756
- };
10757
- case "read-only":
10758
- return {
10759
- type: "readOnly",
10760
- access: {
10761
- type: "fullAccess"
10762
- },
10763
- networkAccess: false
10764
- };
10765
- case "workspace-write":
10766
- default:
10767
- return {
10768
- type: "workspaceWrite",
10769
- writableRoots: [writableRoot],
10770
- readOnlyAccess: {
10771
- type: "fullAccess"
10772
- },
10773
- networkAccess: false,
10774
- excludeTmpdirEnvVar: false,
10775
- excludeSlashTmp: false
10776
- };
10777
- }
10778
- }
10779
11209
  function normalizeReasoningEffort(value) {
10780
11210
  switch (value) {
10781
11211
  case "none":
@@ -10795,8 +11225,8 @@ function normalizeCollaborationMode(value) {
10795
11225
  function normalizeFastMode(value) {
10796
11226
  return value === true || value === 1;
10797
11227
  }
10798
- function serviceTierForFastMode(fastMode) {
10799
- return fastMode ? "fast" : null;
11228
+ function performanceModeForFastMode(fastMode) {
11229
+ return fastMode ? "fast" : "standard";
10800
11230
  }
10801
11231
  function normalizePricingTier(value) {
10802
11232
  return value === "fast" || value === "standard" ? value : null;
@@ -10841,26 +11271,26 @@ function buildAnsweredRequestNote(request, input) {
10841
11271
  }
10842
11272
  async function pathExists(absPath) {
10843
11273
  try {
10844
- await fs8.access(absPath);
11274
+ await fs9.access(absPath);
10845
11275
  return true;
10846
11276
  } catch {
10847
11277
  return false;
10848
11278
  }
10849
11279
  }
10850
11280
  async function resolveComparablePath2(absPath) {
10851
- const resolved = path8.resolve(absPath);
11281
+ const resolved = path9.resolve(absPath);
10852
11282
  if (await pathExists(resolved)) {
10853
- return fs8.realpath(resolved);
11283
+ return fs9.realpath(resolved);
10854
11284
  }
10855
- const parentPath = path8.dirname(resolved);
11285
+ const parentPath = path9.dirname(resolved);
10856
11286
  if (parentPath === resolved) {
10857
11287
  return resolved;
10858
11288
  }
10859
11289
  const resolvedParent = await resolveComparablePath2(parentPath);
10860
- return path8.join(resolvedParent, path8.basename(resolved));
11290
+ return path9.join(resolvedParent, path9.basename(resolved));
10861
11291
  }
10862
11292
  async function resolveImportedWorkspacePath(workspaceRoot, candidatePath) {
10863
- if (!path8.isAbsolute(candidatePath)) {
11293
+ if (!path9.isAbsolute(candidatePath)) {
10864
11294
  throw new HttpError(400, {
10865
11295
  code: "bad_request",
10866
11296
  message: "Imported session path must be absolute."
@@ -10868,7 +11298,7 @@ async function resolveImportedWorkspacePath(workspaceRoot, candidatePath) {
10868
11298
  }
10869
11299
  const resolvedRoot = await resolveComparablePath2(workspaceRoot);
10870
11300
  const resolvedCandidate = await resolveComparablePath2(candidatePath);
10871
- const normalizedRoot = resolvedRoot.endsWith(path8.sep) ? resolvedRoot : `${resolvedRoot}${path8.sep}`;
11301
+ const normalizedRoot = resolvedRoot.endsWith(path9.sep) ? resolvedRoot : `${resolvedRoot}${path9.sep}`;
10872
11302
  if (resolvedCandidate !== resolvedRoot && !resolvedCandidate.startsWith(normalizedRoot)) {
10873
11303
  throw new HttpError(403, {
10874
11304
  code: "forbidden",
@@ -10888,7 +11318,7 @@ function toWorkspaceDto(record) {
10888
11318
  lastOpenedAt: record.lastOpenedAt
10889
11319
  };
10890
11320
  }
10891
- function isRecord4(value) {
11321
+ function isRecord5(value) {
10892
11322
  return typeof value === "object" && value !== null && !Array.isArray(value);
10893
11323
  }
10894
11324
  function numberOrNull2(value) {
@@ -10927,11 +11357,11 @@ function computeContextRemainingPercent(tokensInContextWindow, contextWindow) {
10927
11357
  return clampPercentage(Math.round(remaining / effectiveWindow * 100));
10928
11358
  }
10929
11359
  function buildThreadContextUsageFromPayload(payload, model = null, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
10930
- const tokenUsage = isRecord4(payload) ? payload : null;
11360
+ const tokenUsage = isRecord5(payload) ? payload : null;
10931
11361
  const modelContextWindow = contextWindowForModel(model) ?? numberOrNull2(
10932
11362
  tokenUsage?.modelContextWindow ?? tokenUsage?.model_context_window
10933
11363
  );
10934
- const lastUsage = isRecord4(tokenUsage?.last) ? tokenUsage.last : null;
11364
+ const lastUsage = isRecord5(tokenUsage?.last) ? tokenUsage.last : null;
10935
11365
  const tokensInContextWindow = numberOrNull2(
10936
11366
  lastUsage?.totalTokens ?? lastUsage?.total_tokens
10937
11367
  );
@@ -10950,7 +11380,7 @@ function buildThreadContextUsageFromPayload(payload, model = null, timestamp = (
10950
11380
  };
10951
11381
  }
10952
11382
  function buildTurnTokenBreakdown(payload) {
10953
- const usage = isRecord4(payload) ? payload : null;
11383
+ const usage = isRecord5(payload) ? payload : null;
10954
11384
  const totalTokens2 = numberOrNull2(usage?.totalTokens ?? usage?.total_tokens);
10955
11385
  const inputTokens = numberOrNull2(usage?.inputTokens ?? usage?.input_tokens);
10956
11386
  const cachedInputTokens = numberOrNull2(
@@ -10996,12 +11426,12 @@ function subtractTurnTokenBreakdowns(current, previous) {
10996
11426
  };
10997
11427
  }
10998
11428
  function parseThreadTurnTokenUsage(payload) {
10999
- const tokenUsage = isRecord4(payload) ? payload : null;
11429
+ const tokenUsage = isRecord5(payload) ? payload : null;
11000
11430
  const total = buildTurnTokenBreakdown(
11001
- isRecord4(tokenUsage?.total) ? tokenUsage.total : null
11431
+ isRecord5(tokenUsage?.total) ? tokenUsage.total : null
11002
11432
  );
11003
11433
  const last = buildTurnTokenBreakdown(
11004
- isRecord4(tokenUsage?.last) ? tokenUsage.last : null
11434
+ isRecord5(tokenUsage?.last) ? tokenUsage.last : null
11005
11435
  );
11006
11436
  const modelContextWindow = numberOrNull2(
11007
11437
  tokenUsage?.modelContextWindow ?? tokenUsage?.model_context_window
@@ -11025,7 +11455,7 @@ function parseStoredThreadTurnTokenUsageState(value) {
11025
11455
  try {
11026
11456
  const parsed = JSON.parse(value);
11027
11457
  const baselineTotal = buildTurnTokenBreakdown(
11028
- isRecord4(parsed?.baselineTotal) ? parsed.baselineTotal : null
11458
+ isRecord5(parsed?.baselineTotal) ? parsed.baselineTotal : null
11029
11459
  );
11030
11460
  return {
11031
11461
  baselineTotal,
@@ -11062,12 +11492,12 @@ function stringifyStoredThreadTurnTokenUsageState(state) {
11062
11492
  });
11063
11493
  }
11064
11494
  function buildThreadTurnTokenUsage(payload, baselineTotal, previous = null) {
11065
- const tokenUsage = isRecord4(payload) ? payload : null;
11495
+ const tokenUsage = isRecord5(payload) ? payload : null;
11066
11496
  const cumulativeTotal = buildTurnTokenBreakdown(
11067
- isRecord4(tokenUsage?.total) ? tokenUsage.total : null
11497
+ isRecord5(tokenUsage?.total) ? tokenUsage.total : null
11068
11498
  );
11069
11499
  const last = buildTurnTokenBreakdown(
11070
- isRecord4(tokenUsage?.last) ? tokenUsage.last : null
11500
+ isRecord5(tokenUsage?.last) ? tokenUsage.last : null
11071
11501
  );
11072
11502
  const modelContextWindow = numberOrNull2(
11073
11503
  tokenUsage?.modelContextWindow ?? tokenUsage?.model_context_window
@@ -11085,8 +11515,8 @@ function parseThreadTurnTokenUsageJson(value) {
11085
11515
  return parseStoredThreadTurnTokenUsageState(value).usage;
11086
11516
  }
11087
11517
  function sanitizeAttachmentFileName(originalName) {
11088
- const basename = path8.basename(originalName).trim() || "attachment";
11089
- const extension = path8.extname(basename).replace(/[^a-zA-Z0-9.]/g, "");
11518
+ const basename = path9.basename(originalName).trim() || "attachment";
11519
+ const extension = path9.extname(basename).replace(/[^a-zA-Z0-9.]/g, "");
11090
11520
  const rawStem = extension ? basename.slice(0, -extension.length) : basename;
11091
11521
  const sanitizedStem = rawStem.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 64);
11092
11522
  const stem = sanitizedStem || "attachment";
@@ -11094,269 +11524,7 @@ function sanitizeAttachmentFileName(originalName) {
11094
11524
  return `${stem}-${randomUUID2().slice(0, 8)}${normalizedExtension}`;
11095
11525
  }
11096
11526
  function threadTempDirectoryPath(workspacePath, localThreadId) {
11097
- return path8.join(workspacePath, ".temp", "threads", localThreadId);
11098
- }
11099
- function normalizeHooksJson(value) {
11100
- if (!isRecord4(value) || !isRecord4(value.hooks)) {
11101
- return { hooks: {} };
11102
- }
11103
- const hooks = {};
11104
- for (const [eventName, groups] of Object.entries(value.hooks)) {
11105
- hooks[eventName] = Array.isArray(groups) ? groups : [];
11106
- }
11107
- return { ...value, hooks };
11108
- }
11109
- function readJsonFileOrDefault(filePath) {
11110
- return fs8.readFile(filePath, "utf8").then((raw) => {
11111
- if (!raw.trim()) {
11112
- return { hooks: {} };
11113
- }
11114
- return normalizeHooksJson(JSON.parse(raw));
11115
- }).catch((error) => {
11116
- if (error.code === "ENOENT") {
11117
- return { hooks: {} };
11118
- }
11119
- throw error;
11120
- });
11121
- }
11122
- function validateHookInput(input) {
11123
- if (!HOOK_EVENT_JSON_KEYS[input.eventName]) {
11124
- throw new HttpError(400, {
11125
- code: "bad_request",
11126
- message: "Unsupported hook event."
11127
- });
11128
- }
11129
- if (input.scope !== "global" && input.scope !== "project") {
11130
- throw new HttpError(400, {
11131
- code: "bad_request",
11132
- message: "Hook scope must be global or project."
11133
- });
11134
- }
11135
- if (!input.command.trim()) {
11136
- throw new HttpError(400, {
11137
- code: "bad_request",
11138
- message: "Hook command cannot be empty."
11139
- });
11140
- }
11141
- if (input.timeoutSec !== void 0 && input.timeoutSec !== null && (!Number.isInteger(input.timeoutSec) || input.timeoutSec <= 0 || input.timeoutSec > 86400)) {
11142
- throw new HttpError(400, {
11143
- code: "bad_request",
11144
- message: "Hook timeout must be a positive number of seconds."
11145
- });
11146
- }
11147
- }
11148
- async function writeHookJsonEntry({
11149
- codexHome,
11150
- workspacePath,
11151
- input
11152
- }) {
11153
- validateHookInput(input);
11154
- const hooksPath = input.scope === "global" ? path8.join(codexHome, "hooks.json") : path8.join(workspacePath, ".codex", "hooks.json");
11155
- const config = await readJsonFileOrDefault(hooksPath);
11156
- const eventKey = HOOK_EVENT_JSON_KEYS[input.eventName];
11157
- const matcher = input.matcher?.trim() || null;
11158
- const handler = {
11159
- type: "command",
11160
- command: input.command.trim()
11161
- };
11162
- if (input.timeoutSec !== void 0 && input.timeoutSec !== null) {
11163
- handler.timeout = input.timeoutSec;
11164
- }
11165
- if (input.statusMessage?.trim()) {
11166
- handler.statusMessage = input.statusMessage.trim();
11167
- }
11168
- const group = {
11169
- hooks: [handler]
11170
- };
11171
- if (matcher) {
11172
- group.matcher = matcher;
11173
- }
11174
- const currentGroups = Array.isArray(config.hooks[eventKey]) ? config.hooks[eventKey] : [];
11175
- config.hooks[eventKey] = [...currentGroups, group];
11176
- await fs8.mkdir(path8.dirname(hooksPath), { recursive: true });
11177
- await fs8.writeFile(hooksPath, `${JSON.stringify(config, null, 2)}
11178
- `, "utf8");
11179
- }
11180
- function hookInputMatches(group, handler, input) {
11181
- if (!isRecord4(group) || !isRecord4(handler)) {
11182
- return false;
11183
- }
11184
- const matcher = typeof group.matcher === "string" ? group.matcher : null;
11185
- const handlerCommand = typeof handler.command === "string" ? handler.command : "";
11186
- const handlerTimeout = typeof handler.timeout === "number" && Number.isFinite(handler.timeout) ? handler.timeout : null;
11187
- const handlerStatusMessage = typeof handler.statusMessage === "string" ? handler.statusMessage : null;
11188
- return handler.type === "command" && (input.matcher?.trim() || null) === matcher && input.command.trim() === handlerCommand && (input.timeoutSec ?? null) === handlerTimeout && (input.statusMessage?.trim() || null) === handlerStatusMessage;
11189
- }
11190
- async function updateHookJsonEntry({
11191
- codexHome,
11192
- workspacePath,
11193
- input
11194
- }) {
11195
- validateHookInput(input);
11196
- validateHookInput(input.target);
11197
- if (input.scope !== input.target.scope) {
11198
- throw new HttpError(400, {
11199
- code: "bad_request",
11200
- message: "Hook scope cannot be changed while editing."
11201
- });
11202
- }
11203
- const hooksPath = input.scope === "global" ? path8.join(codexHome, "hooks.json") : path8.join(workspacePath, ".codex", "hooks.json");
11204
- const config = await readJsonFileOrDefault(hooksPath);
11205
- const targetEventKey = HOOK_EVENT_JSON_KEYS[input.target.eventName];
11206
- const nextEventKey = HOOK_EVENT_JSON_KEYS[input.eventName];
11207
- const currentGroups = Array.isArray(config.hooks[targetEventKey]) ? config.hooks[targetEventKey] : [];
11208
- let replacementGroup = null;
11209
- config.hooks[targetEventKey] = currentGroups.map((group) => {
11210
- if (replacementGroup || !isRecord4(group) || !Array.isArray(group.hooks)) {
11211
- return group;
11212
- }
11213
- const hookIndex = group.hooks.findIndex(
11214
- (handler2) => hookInputMatches(group, handler2, input.target)
11215
- );
11216
- if (hookIndex < 0) {
11217
- return group;
11218
- }
11219
- const handler = {
11220
- type: "command",
11221
- command: input.command.trim()
11222
- };
11223
- if (input.timeoutSec !== void 0 && input.timeoutSec !== null) {
11224
- handler.timeout = input.timeoutSec;
11225
- }
11226
- if (input.statusMessage?.trim()) {
11227
- handler.statusMessage = input.statusMessage.trim();
11228
- }
11229
- replacementGroup = {
11230
- hooks: [handler]
11231
- };
11232
- const matcher = input.matcher?.trim() || null;
11233
- if (matcher) {
11234
- replacementGroup.matcher = matcher;
11235
- }
11236
- if (targetEventKey !== nextEventKey) {
11237
- const remainingHooks = group.hooks.filter((_, index) => index !== hookIndex);
11238
- return {
11239
- ...group,
11240
- hooks: remainingHooks
11241
- };
11242
- }
11243
- return {
11244
- ...replacementGroup,
11245
- hooks: group.hooks.map(
11246
- (existing, index) => index === hookIndex ? replacementGroup.hooks[0] : existing
11247
- )
11248
- };
11249
- }).filter((group) => {
11250
- if (!isRecord4(group) || !Array.isArray(group.hooks)) {
11251
- return true;
11252
- }
11253
- return group.hooks.length > 0;
11254
- });
11255
- if (!replacementGroup) {
11256
- throw new HttpError(404, {
11257
- code: "not_found",
11258
- message: "Hook was not found in hooks.json."
11259
- });
11260
- }
11261
- if (targetEventKey !== nextEventKey) {
11262
- if (config.hooks[targetEventKey]?.length === 0) {
11263
- delete config.hooks[targetEventKey];
11264
- }
11265
- const nextGroups = Array.isArray(config.hooks[nextEventKey]) ? config.hooks[nextEventKey] : [];
11266
- config.hooks[nextEventKey] = [...nextGroups, replacementGroup];
11267
- }
11268
- await fs8.mkdir(path8.dirname(hooksPath), { recursive: true });
11269
- await fs8.writeFile(hooksPath, `${JSON.stringify(config, null, 2)}
11270
- `, "utf8");
11271
- }
11272
- async function readLocalHookDtos({
11273
- hooksPath,
11274
- source,
11275
- displayOffset
11276
- }) {
11277
- const config = await readJsonFileOrDefault(hooksPath);
11278
- const hooks = [];
11279
- for (const [eventKey, groups] of Object.entries(config.hooks)) {
11280
- const eventName = HOOK_EVENT_DTO_KEYS[eventKey];
11281
- if (!eventName || !Array.isArray(groups)) {
11282
- continue;
11283
- }
11284
- groups.forEach((group, groupIndex) => {
11285
- if (!isRecord4(group) || !Array.isArray(group.hooks)) {
11286
- return;
11287
- }
11288
- const matcher = typeof group.matcher === "string" ? group.matcher : null;
11289
- group.hooks.forEach((handler, handlerIndex) => {
11290
- if (!isRecord4(handler) || handler.type !== "command") {
11291
- return;
11292
- }
11293
- const command = typeof handler.command === "string" ? handler.command : null;
11294
- if (!command) {
11295
- return;
11296
- }
11297
- const timeoutSec = typeof handler.timeout === "number" && Number.isFinite(handler.timeout) ? handler.timeout : 600;
11298
- const statusMessage = typeof handler.statusMessage === "string" ? handler.statusMessage : null;
11299
- const key = `${source}:${hooksPath}:${eventKey}:${groupIndex}:${handlerIndex}`;
11300
- hooks.push({
11301
- key,
11302
- eventName,
11303
- handlerType: "command",
11304
- matcher,
11305
- command,
11306
- timeoutSec,
11307
- statusMessage,
11308
- sourcePath: hooksPath,
11309
- source,
11310
- pluginId: null,
11311
- displayOrder: displayOffset + hooks.length,
11312
- enabled: true,
11313
- isManaged: false,
11314
- currentHash: "",
11315
- trustStatus: "untrusted"
11316
- });
11317
- });
11318
- });
11319
- }
11320
- return hooks;
11321
- }
11322
- function hookMatchesInput(hook, input) {
11323
- return hook.source === input.scope && hook.eventName === input.eventName && (hook.matcher ?? null) === (input.matcher ?? null) && hook.command === input.command && (input.timeoutSec == null || hook.timeoutSec === input.timeoutSec) && (hook.statusMessage ?? null) === (input.statusMessage ?? null);
11324
- }
11325
- async function findOfficialHookForInput(runtime, workspacePath, input) {
11326
- if (!runtime.listHooks) {
11327
- return null;
11328
- }
11329
- const [entry] = await runtime.listHooks({
11330
- cwds: [workspacePath]
11331
- });
11332
- const officialHooks = (entry?.hooks ?? []).map((hook) => ({
11333
- key: hook.key,
11334
- eventName: hook.eventName,
11335
- handlerType: hook.handlerType,
11336
- matcher: hook.matcher,
11337
- command: hook.command,
11338
- timeoutSec: hook.timeoutSec,
11339
- statusMessage: hook.statusMessage,
11340
- sourcePath: hook.sourcePath,
11341
- source: hook.source,
11342
- pluginId: hook.pluginId,
11343
- displayOrder: hook.displayOrder,
11344
- enabled: hook.enabled,
11345
- isManaged: hook.isManaged,
11346
- currentHash: hook.currentHash,
11347
- trustStatus: hook.trustStatus
11348
- }));
11349
- return officialHooks.find((hook) => hookMatchesInput(hook, input)) ?? null;
11350
- }
11351
- async function trustHookForInput(runtime, workspacePath, input) {
11352
- const hook = await findOfficialHookForInput(runtime, workspacePath, input);
11353
- if (!runtime.setHookTrust || !hook || !hook.key || !hook.currentHash || hook.isManaged) {
11354
- return;
11355
- }
11356
- await runtime.setHookTrust({
11357
- key: hook.key,
11358
- trustedHash: hook.currentHash
11359
- });
11527
+ return path9.join(workspacePath, ".temp", "threads", localThreadId);
11360
11528
  }
11361
11529
  function buildTurnDto(turn, metadata) {
11362
11530
  const tokenUsage = parseThreadTurnTokenUsageJson(metadata?.tokenUsageJson);
@@ -11416,13 +11584,13 @@ function safeTranscriptExportFileName(title, extension) {
11416
11584
  return `remote-codex-${stem || "thread"}-${timestamp}.${extension}`;
11417
11585
  }
11418
11586
  var ThreadService = class {
11419
- constructor(db, agentRuntimes, eventBus, localSessionStore, workspaceRoot, codexHome) {
11587
+ constructor(db, agentRuntimes, eventBus, localSessionStore, workspaceRoot, codexManagement) {
11420
11588
  this.db = db;
11421
11589
  this.agentRuntimes = agentRuntimes;
11422
11590
  this.eventBus = eventBus;
11423
11591
  this.localSessionStore = localSessionStore;
11424
11592
  this.workspaceRoot = workspaceRoot;
11425
- this.codexHome = codexHome;
11593
+ this.codexManagement = codexManagement;
11426
11594
  for (const runtime of this.agentRuntimes.all()) {
11427
11595
  runtime.on("event", (event) => {
11428
11596
  void this.handleRuntimeEvent(event);
@@ -11437,7 +11605,7 @@ var ThreadService = class {
11437
11605
  eventBus;
11438
11606
  localSessionStore;
11439
11607
  workspaceRoot;
11440
- codexHome;
11608
+ codexManagement;
11441
11609
  pendingRequests = /* @__PURE__ */ new Map();
11442
11610
  dismissedPlanDecisionTurns = /* @__PURE__ */ new Map();
11443
11611
  threadDetailCache = /* @__PURE__ */ new Map();
@@ -11497,13 +11665,13 @@ var ThreadService = class {
11497
11665
  return this.providerForRecord({ provider }) === "codex";
11498
11666
  }
11499
11667
  runtimeSupportsFastMode(provider) {
11500
- return this.runtimeForProvider(provider).capabilities.controls.fastServiceTier;
11668
+ return this.runtimeForProvider(provider).capabilities.controls.performanceMode;
11501
11669
  }
11502
11670
  fastModeForProvider(provider, fastMode) {
11503
11671
  return this.runtimeSupportsFastMode(provider) ? normalizeFastMode(fastMode) : false;
11504
11672
  }
11505
- serviceTierForRecord(record) {
11506
- return serviceTierForFastMode(this.fastModeForProvider(record.provider, record.fastMode));
11673
+ performanceModeForRecord(record) {
11674
+ return performanceModeForFastMode(this.fastModeForProvider(record.provider, record.fastMode));
11507
11675
  }
11508
11676
  assertCodexHooksFileManagement(provider) {
11509
11677
  if (!this.isCodexProvider(provider)) {
@@ -11740,7 +11908,7 @@ var ThreadService = class {
11740
11908
  const matchedModel = modelRecords.find((entry) => entry.model === input.model);
11741
11909
  const reasoningEffort = normalizeReasoningEffort(matchedModel?.defaultReasoningEffort) ?? "medium";
11742
11910
  const sandboxMode = defaultSandboxModeForApprovalMode(input.approvalMode);
11743
- const fastMode = this.runtimeSupportsFastMode(provider) ? readCodexFastModeSync(this.codexHome) : false;
11911
+ const fastMode = this.runtimeSupportsFastMode(provider) ? this.codexManagement.readFastMode() : false;
11744
11912
  if (this.runtimeSupportsFastMode(provider)) {
11745
11913
  ensureFastModeSupported(input.model, fastMode);
11746
11914
  }
@@ -11749,7 +11917,7 @@ var ThreadService = class {
11749
11917
  model: input.model,
11750
11918
  approvalMode: input.approvalMode,
11751
11919
  sandboxMode,
11752
- serviceTier: serviceTierForFastMode(fastMode)
11920
+ performanceMode: performanceModeForFastMode(fastMode)
11753
11921
  });
11754
11922
  const created = createThreadRecord(this.db, {
11755
11923
  workspaceId: workspace.id,
@@ -11804,7 +11972,7 @@ var ThreadService = class {
11804
11972
  if (!workspace) {
11805
11973
  workspace = createWorkspaceRecord(this.db, {
11806
11974
  absPath: importedPath,
11807
- label: path8.basename(importedPath) || "workspace"
11975
+ label: path9.basename(importedPath) || "workspace"
11808
11976
  });
11809
11977
  }
11810
11978
  const created = createThreadRecord(this.db, {
@@ -11820,7 +11988,7 @@ var ThreadService = class {
11820
11988
  approvalMode: "yolo",
11821
11989
  sandboxMode: defaultSandboxModeForApprovalMode("yolo"),
11822
11990
  summaryText: localSession.turns.flatMap((turn) => turn.items).find((item) => item.kind === "userMessage")?.text ?? null,
11823
- fastMode: readCodexFastModeSync(this.codexHome),
11991
+ fastMode: this.codexManagement.readFastMode(),
11824
11992
  source: "local_codex_import",
11825
11993
  isConnected: false
11826
11994
  });
@@ -11869,8 +12037,8 @@ var ThreadService = class {
11869
12037
  cachedDetail.turns,
11870
12038
  pagedTurns.turns
11871
12039
  );
11872
- const goal = await this.getThreadGoalForRecord(updated).catch(() => null);
11873
12040
  const goalHistory = this.listThreadGoalHistory(updated.id);
12041
+ const goal = await this.getThreadGoalForRecord(updated).catch(() => null) ?? localGoalSnapshotForFallback(goalHistory);
11874
12042
  return {
11875
12043
  thread: this.toThreadDto(updated, loadedIds),
11876
12044
  workspace: toWorkspaceDto(workspace),
@@ -12010,7 +12178,7 @@ var ThreadService = class {
12010
12178
  });
12011
12179
  }
12012
12180
  this.requireProviderSessionId(record);
12013
- return this.getThreadGoalForRecord(record, { allowEnableFeature: true });
12181
+ return await this.getThreadGoalForRecord(record, { allowEnableFeature: true }) ?? localGoalSnapshotForFallback(this.listThreadGoalHistory(localThreadId));
12014
12182
  }
12015
12183
  async updateThreadGoal(localThreadId, input) {
12016
12184
  const record = getThreadRecordById(this.db, localThreadId);
@@ -12037,14 +12205,32 @@ var ThreadService = class {
12037
12205
  try {
12038
12206
  await this.ensureGoalsFeatureEnabled(record.provider);
12039
12207
  await this.ensureThreadLoadedForCodexOperation(record);
12040
- const upstreamStatus = input.status === "terminated" ? void 0 : input.status;
12208
+ const activeGoal = this.listThreadGoalHistory(localThreadId).find(
12209
+ (goal2) => ["active", "paused", "budgetLimited"].includes(goal2.status)
12210
+ ) ?? null;
12211
+ const creatingNewGoal = goalObjectiveChanged(activeGoal, input.objective);
12212
+ if (creatingNewGoal) {
12213
+ markActiveThreadGoalRecordTerminated(this.db, localThreadId);
12214
+ }
12215
+ if (input.status === "terminated") {
12216
+ const terminatedGoal = markActiveThreadGoalRecordTerminated(this.db, localThreadId);
12217
+ const goalHistory = this.listThreadGoalHistory(localThreadId);
12218
+ const goal2 = terminatedGoal ? toThreadGoalDtoFromRecord(terminatedGoal) : goalHistory[0] ?? null;
12219
+ this.emitThreadEvent("thread.goal.updated", localThreadId, {
12220
+ goal: goal2,
12221
+ goalHistory
12222
+ });
12223
+ return goal2;
12224
+ }
12225
+ const upstreamStatus = input.status;
12041
12226
  const goal = await runtime.setGoal({
12042
12227
  providerSessionId,
12043
12228
  ...input.objective !== void 0 ? { objective: input.objective } : {},
12044
12229
  ...upstreamStatus !== void 0 ? { status: upstreamStatus } : {},
12045
12230
  ...input.tokenBudget !== void 0 ? { tokenBudget: input.tokenBudget } : {}
12046
12231
  });
12047
- const dto = normalizeThreadGoalStatusForThread(toThreadGoalDtoFromAgentGoal(goal), record);
12232
+ const upstreamDto = normalizeThreadGoalStatusForThread(toThreadGoalDtoFromAgentGoal(goal), record);
12233
+ const dto = creatingNewGoal ? resetGoalProgress(upstreamDto) : upstreamDto;
12048
12234
  const persistedGoal = toThreadGoalDtoFromRecord(
12049
12235
  this.persistThreadGoalSnapshot(localThreadId, dto)
12050
12236
  );
@@ -12054,7 +12240,7 @@ var ThreadService = class {
12054
12240
  });
12055
12241
  return persistedGoal;
12056
12242
  } catch (error) {
12057
- mapCodexGoalError(error);
12243
+ this.codexManagement.mapGoalError(error);
12058
12244
  }
12059
12245
  }
12060
12246
  async clearThreadGoal(localThreadId) {
@@ -12088,7 +12274,7 @@ var ThreadService = class {
12088
12274
  this.emitThreadEvent("thread.goal.cleared", localThreadId, { goalHistory });
12089
12275
  return { cleared, goalHistory };
12090
12276
  } catch (error) {
12091
- mapCodexGoalError(error);
12277
+ this.codexManagement.mapGoalError(error);
12092
12278
  }
12093
12279
  }
12094
12280
  async getThreadGoalForRecord(record, options = {}) {
@@ -12109,9 +12295,16 @@ var ThreadService = class {
12109
12295
  return null;
12110
12296
  }
12111
12297
  const dto = normalizeThreadGoalStatusForThread(toThreadGoalDtoFromAgentGoal(goal), record);
12298
+ const localGoal = localGoalSnapshotToPreserve(
12299
+ this.listThreadGoalHistory(record.id),
12300
+ dto
12301
+ );
12302
+ if (localGoal) {
12303
+ return localGoal;
12304
+ }
12112
12305
  return toThreadGoalDtoFromRecord(this.persistThreadGoalSnapshot(record.id, dto));
12113
12306
  } catch (error) {
12114
- if (isCodexRuntimeRequestError(error)) {
12307
+ if (this.codexManagement.isRuntimeRequestError(error)) {
12115
12308
  return null;
12116
12309
  }
12117
12310
  throw error;
@@ -12144,29 +12337,20 @@ var ThreadService = class {
12144
12337
  deduped.set(key, existing ? mergeGoalHistoryEntry(existing, goal) : goal);
12145
12338
  }
12146
12339
  return [...deduped.values()].sort(
12147
- (left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt)
12340
+ (left, right) => {
12341
+ const updatedDelta = Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
12342
+ if (updatedDelta !== 0) {
12343
+ return updatedDelta;
12344
+ }
12345
+ return goalHistoryStatusRank(left.status) - goalHistoryStatusRank(right.status);
12346
+ }
12148
12347
  );
12149
12348
  }
12150
12349
  async ensureGoalsFeatureEnabled(provider) {
12151
12350
  if (!this.isCodexProvider(provider)) {
12152
12351
  return;
12153
12352
  }
12154
- try {
12155
- if (await readCodexFeatureFlag(this.codexHome, "goals")) {
12156
- return;
12157
- }
12158
- await writeCodexFeatureFlag(this.codexHome, "goals", true);
12159
- await this.codexRuntime().stop();
12160
- await this.codexRuntime().start();
12161
- } catch (error) {
12162
- if (isCodexRuntimeRequestError(error)) {
12163
- throw new HttpError(409, {
12164
- code: "goal_feature_disabled",
12165
- message: GOAL_FEATURE_DISABLED_MESSAGE
12166
- });
12167
- }
12168
- throw error;
12169
- }
12353
+ await this.codexManagement.ensureGoalsFeatureEnabled(this.codexRuntime());
12170
12354
  }
12171
12355
  async ensureThreadLoadedForCodexOperation(record) {
12172
12356
  if (!record.providerSessionId) {
@@ -12244,7 +12428,7 @@ var ThreadService = class {
12244
12428
  });
12245
12429
  }
12246
12430
  const tempDirectory = threadTempDirectoryPath(workspace.absPath, localThreadId);
12247
- await fs8.mkdir(tempDirectory, { recursive: true });
12431
+ await fs9.mkdir(tempDirectory, { recursive: true });
12248
12432
  let rewrittenPrompt = input.prompt;
12249
12433
  for (const attachment of attachments) {
12250
12434
  if (!rewrittenPrompt.includes(attachment.manifest.placeholder)) {
@@ -12254,7 +12438,7 @@ var ThreadService = class {
12254
12438
  });
12255
12439
  }
12256
12440
  const savedFileName = sanitizeAttachmentFileName(attachment.manifest.originalName);
12257
- await fs8.writeFile(path8.join(tempDirectory, savedFileName), attachment.buffer);
12441
+ await fs9.writeFile(path9.join(tempDirectory, savedFileName), attachment.buffer);
12258
12442
  const relativePath = `./.temp/threads/${localThreadId}/${savedFileName}`;
12259
12443
  const replacementToken = attachment.manifest.kind === "photo" ? `[PHOTO ${relativePath}]` : `[FILE ${relativePath}]`;
12260
12444
  rewrittenPrompt = rewrittenPrompt.split(attachment.manifest.placeholder).join(replacementToken);
@@ -12286,7 +12470,7 @@ var ThreadService = class {
12286
12470
  providerSessionId,
12287
12471
  model: input.model ?? record.model ?? null,
12288
12472
  sandboxMode,
12289
- serviceTier: serviceTierForFastMode(fastMode)
12473
+ performanceMode: performanceModeForFastMode(fastMode)
12290
12474
  });
12291
12475
  } catch (error) {
12292
12476
  if (!isRemoteThreadBootstrapError(error)) {
@@ -12391,7 +12575,7 @@ var ThreadService = class {
12391
12575
  const sandboxMode = (input.sandboxMode !== void 0 ? normalizeSandboxMode(input.sandboxMode) : normalizeSandboxMode(record.sandboxMode)) ?? defaultSandboxModeForApprovalMode(record.approvalMode ?? "yolo");
12392
12576
  const fastMode = this.fastModeForProvider(record.provider, record.fastMode);
12393
12577
  ensureFastModeSupported(effectiveModel, fastMode);
12394
- const serviceTier = serviceTierForFastMode(fastMode);
12578
+ const performanceMode = performanceModeForFastMode(fastMode);
12395
12579
  const connectedRecord = {
12396
12580
  ...record,
12397
12581
  providerSessionId
@@ -12408,7 +12592,7 @@ var ThreadService = class {
12408
12592
  normalizedReasoning,
12409
12593
  collaborationMode,
12410
12594
  sandboxMode,
12411
- serviceTier,
12595
+ performanceMode,
12412
12596
  workspacePath: workspace.absPath
12413
12597
  });
12414
12598
  }
@@ -12418,26 +12602,27 @@ var ThreadService = class {
12418
12602
  normalizedReasoning,
12419
12603
  collaborationMode,
12420
12604
  sandboxMode,
12421
- serviceTier,
12605
+ performanceMode,
12422
12606
  workspacePath: workspace.absPath
12423
12607
  });
12424
12608
  }
12425
12609
  async startPromptTurn(localThreadId, record, input) {
12426
12610
  const runtime = this.runtimeForProvider(record.provider);
12427
12611
  const modelRecords = await runtime.listModels().catch(() => []);
12428
- ensureFastModeSupported(input.effectiveModel, input.serviceTier === "fast");
12612
+ ensureFastModeSupported(input.effectiveModel, input.performanceMode === "fast");
12429
12613
  const pricingSnapshot = buildTurnPricingSnapshot(
12430
12614
  input.effectiveModel,
12431
- input.serviceTier === "fast"
12615
+ input.performanceMode === "fast"
12432
12616
  );
12433
12617
  const turn = await runtime.startTurn({
12434
12618
  providerSessionId: record.providerSessionId,
12435
12619
  prompt: input.prompt,
12436
12620
  model: input.effectiveModel,
12437
- serviceTier: input.serviceTier,
12621
+ performanceMode: input.performanceMode,
12438
12622
  reasoningEffort: input.normalizedReasoning,
12439
12623
  collaborationMode: input.collaborationMode,
12440
- sandboxPolicy: buildTurnSandboxPolicy(input.sandboxMode, input.workspacePath)
12624
+ sandboxMode: input.sandboxMode,
12625
+ workspacePath: input.workspacePath
12441
12626
  });
12442
12627
  upsertThreadTurnMetadata(this.db, {
12443
12628
  threadId: localThreadId,
@@ -12525,7 +12710,7 @@ var ThreadService = class {
12525
12710
  normalizedReasoning: input.normalizedReasoning,
12526
12711
  collaborationMode: input.collaborationMode,
12527
12712
  sandboxMode: input.sandboxMode,
12528
- serviceTier: input.serviceTier,
12713
+ performanceMode: input.performanceMode,
12529
12714
  workspacePath: input.workspacePath
12530
12715
  });
12531
12716
  }
@@ -12559,7 +12744,7 @@ var ThreadService = class {
12559
12744
  this.clearPendingPlanDecisionRequests(localThreadId, true);
12560
12745
  }
12561
12746
  if (supportsFastMode2 && currentFastMode !== nextFastMode) {
12562
- await writeCodexFastMode(this.codexHome, nextFastMode);
12747
+ await this.codexManagement.writeFastMode(nextFastMode);
12563
12748
  this.appendActivityNote(localThreadId, {
12564
12749
  kind: "fastMode",
12565
12750
  text: nextFastMode ? FAST_MODE_NOTE_ON : FAST_MODE_NOTE_OFF
@@ -12912,12 +13097,7 @@ var ThreadService = class {
12912
13097
  });
12913
13098
  }
12914
13099
  this.assertCodexHooksFileManagement(record.provider);
12915
- await writeHookJsonEntry({
12916
- codexHome: this.codexHome,
12917
- workspacePath: workspace.absPath,
12918
- input
12919
- });
12920
- await trustHookForInput(runtime, workspace.absPath, input);
13100
+ await this.codexManagement.writeHookEntry(runtime, workspace.absPath, input);
12921
13101
  return this.listThreadHooks(localThreadId);
12922
13102
  }
12923
13103
  async updateThreadHook(localThreadId, input) {
@@ -12943,12 +13123,7 @@ var ThreadService = class {
12943
13123
  });
12944
13124
  }
12945
13125
  this.assertCodexHooksFileManagement(record.provider);
12946
- await updateHookJsonEntry({
12947
- codexHome: this.codexHome,
12948
- workspacePath: workspace.absPath,
12949
- input
12950
- });
12951
- await trustHookForInput(runtime, workspace.absPath, input);
13126
+ await this.codexManagement.updateHookEntry(runtime, workspace.absPath, input);
12952
13127
  return this.listThreadHooks(localThreadId);
12953
13128
  }
12954
13129
  async trustThreadHook(localThreadId, input) {
@@ -13051,7 +13226,7 @@ var ThreadService = class {
13051
13226
  const workspace = getWorkspaceRecordById(this.db, record.workspaceId);
13052
13227
  if (workspace) {
13053
13228
  const tempDirectory = threadTempDirectoryPath(workspace.absPath, localThreadId);
13054
- await fs8.rm(tempDirectory, { recursive: true, force: true }).catch(() => {
13229
+ await fs9.rm(tempDirectory, { recursive: true, force: true }).catch(() => {
13055
13230
  });
13056
13231
  }
13057
13232
  this.pendingRequests.delete(localThreadId);
@@ -13226,7 +13401,7 @@ var ThreadService = class {
13226
13401
  if (!record) {
13227
13402
  return;
13228
13403
  }
13229
- const tokenUsage = isRecord4(event.usage) ? event.usage : null;
13404
+ const tokenUsage = isRecord5(event.usage) ? event.usage : null;
13230
13405
  const usage = buildThreadContextUsageFromPayload(
13231
13406
  tokenUsage,
13232
13407
  record.model
@@ -13242,7 +13417,7 @@ var ThreadService = class {
13242
13417
  );
13243
13418
  const previousCumulativeTotal = this.threadCumulativeTokenUsage.get(record.id);
13244
13419
  const currentCumulativeTotal = buildTurnTokenBreakdown(
13245
- isRecord4(tokenUsage?.total) ? tokenUsage.total : null
13420
+ isRecord5(tokenUsage?.total) ? tokenUsage.total : null
13246
13421
  );
13247
13422
  if (currentCumulativeTotal) {
13248
13423
  this.threadCumulativeTokenUsage.set(record.id, currentCumulativeTotal);
@@ -13489,7 +13664,7 @@ var ThreadService = class {
13489
13664
  const defaultMappedRequest = runtime.mapProviderRequest?.(request, {
13490
13665
  approvalMode: "guarded"
13491
13666
  });
13492
- const providerSessionIdFromParams = isRecord4(request.params) ? request.params.providerSessionId ?? request.params.threadId ?? request.params.conversationId ?? request.params.sessionId : null;
13667
+ const providerSessionIdFromParams = isRecord5(request.params) ? request.params.providerSessionId ?? request.params.threadId ?? request.params.conversationId ?? request.params.sessionId : null;
13493
13668
  const providerSessionId = defaultMappedRequest?.providerSessionId ?? (typeof providerSessionIdFromParams === "string" ? providerSessionIdFromParams : null);
13494
13669
  const record = providerSessionId ? this.findRecordByProviderSessionId(request.provider, providerSessionId) : null;
13495
13670
  if (!record) {
@@ -13659,8 +13834,7 @@ var ThreadService = class {
13659
13834
  });
13660
13835
  }
13661
13836
  async toThreadHooksDto(provider, workspacePath, entry, fallbackWarnings = []) {
13662
- const globalHooksPath = path8.join(this.codexHome, "hooks.json");
13663
- const projectHooksPath = path8.join(workspacePath, ".codex", "hooks.json");
13837
+ const { globalHooksPath, projectHooksPath } = this.codexManagement.hooksPaths(workspacePath);
13664
13838
  const officialHooks = (entry?.hooks ?? []).map((hook) => ({
13665
13839
  key: hook.key,
13666
13840
  eventName: hook.eventName,
@@ -13679,12 +13853,12 @@ var ThreadService = class {
13679
13853
  trustStatus: hook.trustStatus
13680
13854
  }));
13681
13855
  const [globalHooks, projectHooks] = this.isCodexProvider(provider) ? await Promise.all([
13682
- readLocalHookDtos({
13856
+ this.codexManagement.readLocalHookDtos({
13683
13857
  hooksPath: globalHooksPath,
13684
13858
  source: "user",
13685
13859
  displayOffset: officialHooks.length
13686
13860
  }),
13687
- readLocalHookDtos({
13861
+ this.codexManagement.readLocalHookDtos({
13688
13862
  hooksPath: projectHooksPath,
13689
13863
  source: "project",
13690
13864
  displayOffset: officialHooks.length + 1e4
@@ -14266,8 +14440,8 @@ async function registerSystemRoutes(app2) {
14266
14440
  }
14267
14441
 
14268
14442
  // src/routes/threads.ts
14269
- import fs9 from "fs/promises";
14270
- import path9 from "path";
14443
+ import fs10 from "fs/promises";
14444
+ import path10 from "path";
14271
14445
  import { z as z5 } from "zod";
14272
14446
  var createThreadSchema = z5.object({
14273
14447
  workspaceId: z5.string().uuid(),
@@ -14304,7 +14478,7 @@ var updateThreadSettingsSchema = z5.object({
14304
14478
  });
14305
14479
  var updateThreadGoalSchema = z5.object({
14306
14480
  objective: z5.string().min(1).nullable().optional(),
14307
- status: z5.enum(["active", "paused", "budgetLimited", "complete"]).nullable().optional(),
14481
+ status: z5.enum(["active", "paused", "budgetLimited", "complete", "terminated"]).nullable().optional(),
14308
14482
  tokenBudget: z5.number().int().positive().nullable().optional()
14309
14483
  }).refine((body) => Object.keys(body).length > 0, {
14310
14484
  message: "At least one goal field must be provided."
@@ -14626,23 +14800,23 @@ async function registerThreadRoutes(app2) {
14626
14800
  message: "Workspace was not found for this thread."
14627
14801
  });
14628
14802
  }
14629
- const candidatePath = path9.isAbsolute(query.path) ? query.path : path9.resolve(workspace.absPath, query.path);
14630
- const requestedPath = await fs9.realpath(candidatePath).catch(() => null);
14803
+ const candidatePath = path10.isAbsolute(query.path) ? query.path : path10.resolve(workspace.absPath, query.path);
14804
+ const requestedPath = await fs10.realpath(candidatePath).catch(() => null);
14631
14805
  if (!requestedPath) {
14632
14806
  throw new HttpError(404, {
14633
14807
  code: "not_found",
14634
14808
  message: "Image file was not found."
14635
14809
  });
14636
14810
  }
14637
- const resolvedWorkspaceRoot = await fs9.realpath(app2.services.config.workspaceRoot).catch(() => path9.resolve(app2.services.config.workspaceRoot));
14638
- const workspacePrefix = resolvedWorkspaceRoot.endsWith(path9.sep) ? resolvedWorkspaceRoot : `${resolvedWorkspaceRoot}${path9.sep}`;
14811
+ const resolvedWorkspaceRoot = await fs10.realpath(app2.services.config.workspaceRoot).catch(() => path10.resolve(app2.services.config.workspaceRoot));
14812
+ const workspacePrefix = resolvedWorkspaceRoot.endsWith(path10.sep) ? resolvedWorkspaceRoot : `${resolvedWorkspaceRoot}${path10.sep}`;
14639
14813
  if (requestedPath !== resolvedWorkspaceRoot && !requestedPath.startsWith(workspacePrefix)) {
14640
14814
  throw new HttpError(403, {
14641
14815
  code: "forbidden",
14642
14816
  message: "Image path must stay within the configured workspace root."
14643
14817
  });
14644
14818
  }
14645
- const stats = await fs9.stat(requestedPath).catch(() => null);
14819
+ const stats = await fs10.stat(requestedPath).catch(() => null);
14646
14820
  if (!stats?.isFile()) {
14647
14821
  throw new HttpError(404, {
14648
14822
  code: "not_found",
@@ -14653,7 +14827,7 @@ async function registerThreadRoutes(app2) {
14653
14827
  const contentType = lowerPath.endsWith(".png") ? "image/png" : lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg") ? "image/jpeg" : lowerPath.endsWith(".gif") ? "image/gif" : lowerPath.endsWith(".webp") ? "image/webp" : lowerPath.endsWith(".svg") ? "image/svg+xml" : lowerPath.endsWith(".heic") ? "image/heic" : lowerPath.endsWith(".heif") ? "image/heif" : "application/octet-stream";
14654
14828
  reply.header("Content-Type", contentType);
14655
14829
  reply.header("Cache-Control", "private, max-age=60");
14656
- return reply.send(await fs9.readFile(requestedPath));
14830
+ return reply.send(await fs10.readFile(requestedPath));
14657
14831
  });
14658
14832
  app2.patch("/api/threads/:id", async (request) => {
14659
14833
  const params = z5.object({ id: z5.string().uuid() }).parse(request.params);
@@ -14827,8 +15001,8 @@ async function registerThreadRoutes(app2) {
14827
15001
  }
14828
15002
 
14829
15003
  // src/routes/workspaces.ts
14830
- import fs10 from "fs/promises";
14831
- import path10 from "path";
15004
+ import fs11 from "fs/promises";
15005
+ import path11 from "path";
14832
15006
  import { spawn as spawn2 } from "child_process";
14833
15007
  import { z as z6 } from "zod";
14834
15008
  var createWorkspaceSchema = z6.union([
@@ -14868,7 +15042,7 @@ function inferGitRepoName(gitUrl) {
14868
15042
  const normalized = withoutQuery.replace(/[\\/]+$/, "");
14869
15043
  const rawName = normalized.split(/[/:]/).filter(Boolean).at(-1) ?? "";
14870
15044
  const repoName = rawName.endsWith(".git") ? rawName.slice(0, -4) : rawName;
14871
- if (!repoName || repoName === "." || repoName === ".." || repoName.includes(path10.sep)) {
15045
+ if (!repoName || repoName === "." || repoName === ".." || repoName.includes(path11.sep)) {
14872
15046
  throw new HttpError(400, {
14873
15047
  code: "bad_request",
14874
15048
  message: "Unable to infer a target directory from the Git URL."
@@ -14878,7 +15052,7 @@ function inferGitRepoName(gitUrl) {
14878
15052
  }
14879
15053
  async function pathExists2(absPath) {
14880
15054
  try {
14881
- await fs10.stat(absPath);
15055
+ await fs11.stat(absPath);
14882
15056
  return true;
14883
15057
  } catch (error) {
14884
15058
  if (error.code === "ENOENT") {
@@ -14933,7 +15107,7 @@ async function registerWorkspaceRoutes(app2) {
14933
15107
  });
14934
15108
  app2.get("/api/workspaces/tree", async (request) => {
14935
15109
  const query = treeQuerySchema.parse(request.query);
14936
- const requestedPath = query.path ? path10.resolve(query.path) : app2.services.config.workspaceRoot;
15110
+ const requestedPath = query.path ? path11.resolve(query.path) : app2.services.config.workspaceRoot;
14937
15111
  const tree = await readWorkspaceTree({
14938
15112
  rootPath: app2.services.config.workspaceRoot,
14939
15113
  targetPath: requestedPath,
@@ -14965,7 +15139,7 @@ async function registerWorkspaceRoutes(app2) {
14965
15139
  let validated;
14966
15140
  if ("gitUrl" in body) {
14967
15141
  const repoName = inferGitRepoName(body.gitUrl);
14968
- const targetPath = path10.join(settings.devHome, repoName);
15142
+ const targetPath = path11.join(settings.devHome, repoName);
14969
15143
  if (await pathExists2(targetPath)) {
14970
15144
  throw new HttpError(409, {
14971
15145
  code: "conflict",
@@ -15074,8 +15248,8 @@ async function registerWorkspaceRoutes(app2) {
15074
15248
  }
15075
15249
 
15076
15250
  // src/provider-host-config-service.ts
15077
- import fs11 from "fs/promises";
15078
- import path11 from "path";
15251
+ import fs12 from "fs/promises";
15252
+ import path12 from "path";
15079
15253
  import { randomUUID as randomUUID3 } from "crypto";
15080
15254
  function providerError(message, statusCode = 404) {
15081
15255
  const error = new Error(message);
@@ -15083,23 +15257,23 @@ function providerError(message, statusCode = 404) {
15083
15257
  return error;
15084
15258
  }
15085
15259
  function resolveProviderHostFilePath(providerHome, name) {
15086
- return path11.join(providerHome, name);
15260
+ return path12.join(providerHome, name);
15087
15261
  }
15088
15262
  function resolveArchiveRoot(providerHome) {
15089
- return path11.join(providerHome, "supervisor-config-archives");
15263
+ return path12.join(providerHome, "supervisor-config-archives");
15090
15264
  }
15091
15265
  function resolveArchiveIndexPath(providerHome) {
15092
- return path11.join(resolveArchiveRoot(providerHome), "index.json");
15266
+ return path12.join(resolveArchiveRoot(providerHome), "index.json");
15093
15267
  }
15094
15268
  function resolveArchivePath(providerHome, archiveId) {
15095
- return path11.join(resolveArchiveRoot(providerHome), archiveId);
15269
+ return path12.join(resolveArchiveRoot(providerHome), archiveId);
15096
15270
  }
15097
15271
  function defaultArchiveLabel(createdAt) {
15098
15272
  return `Backup ${createdAt.replace("T", " ").replace(/\.\d{3}Z$/, " UTC")}`;
15099
15273
  }
15100
15274
  async function readArchiveIndex(providerHome) {
15101
15275
  try {
15102
- const raw = await fs11.readFile(resolveArchiveIndexPath(providerHome), "utf8");
15276
+ const raw = await fs12.readFile(resolveArchiveIndexPath(providerHome), "utf8");
15103
15277
  const parsed = JSON.parse(raw);
15104
15278
  return {
15105
15279
  archives: Array.isArray(parsed.archives) ? parsed.archives : []
@@ -15113,8 +15287,8 @@ async function readArchiveIndex(providerHome) {
15113
15287
  }
15114
15288
  async function writeArchiveIndex(providerHome, index) {
15115
15289
  const root = resolveArchiveRoot(providerHome);
15116
- await fs11.mkdir(root, { recursive: true });
15117
- await fs11.writeFile(
15290
+ await fs12.mkdir(root, { recursive: true });
15291
+ await fs12.writeFile(
15118
15292
  resolveArchiveIndexPath(providerHome),
15119
15293
  `${JSON.stringify(index, null, 2)}
15120
15294
  `,
@@ -15185,7 +15359,7 @@ var ProviderHostConfigService = class {
15185
15359
  const fileName = this.assertHostFile(provider, name);
15186
15360
  const filePath = resolveProviderHostFilePath(providerHome, fileName);
15187
15361
  try {
15188
- const content = await fs11.readFile(filePath, "utf8");
15362
+ const content = await fs12.readFile(filePath, "utf8");
15189
15363
  return {
15190
15364
  name: fileName,
15191
15365
  path: filePath,
@@ -15208,8 +15382,8 @@ var ProviderHostConfigService = class {
15208
15382
  const providerHome = this.providerHome(provider);
15209
15383
  const fileName = this.assertHostFile(provider, name);
15210
15384
  const filePath = resolveProviderHostFilePath(providerHome, fileName);
15211
- await fs11.mkdir(path11.dirname(filePath), { recursive: true });
15212
- await fs11.writeFile(filePath, input.content, "utf8");
15385
+ await fs12.mkdir(path12.dirname(filePath), { recursive: true });
15386
+ await fs12.writeFile(filePath, input.content, "utf8");
15213
15387
  return this.readFile(provider, fileName);
15214
15388
  }
15215
15389
  async listArchives(provider) {
@@ -15235,7 +15409,7 @@ var ProviderHostConfigService = class {
15235
15409
  }
15236
15410
  ])
15237
15411
  );
15238
- await fs11.mkdir(archivePath, { recursive: true });
15412
+ await fs12.mkdir(archivePath, { recursive: true });
15239
15413
  for (const name of fileNames) {
15240
15414
  const hostFile = await this.readFile(provider, name);
15241
15415
  files[name] = {
@@ -15243,7 +15417,7 @@ var ProviderHostConfigService = class {
15243
15417
  exists: hostFile.exists
15244
15418
  };
15245
15419
  if (hostFile.exists) {
15246
- await fs11.writeFile(path11.join(archivePath, name), hostFile.content, "utf8");
15420
+ await fs12.writeFile(path12.join(archivePath, name), hostFile.content, "utf8");
15247
15421
  }
15248
15422
  }
15249
15423
  const archive = {
@@ -15279,14 +15453,14 @@ var ProviderHostConfigService = class {
15279
15453
  const fileNames = this.archiveFileNames(provider);
15280
15454
  const { archive } = await findArchiveOrThrow(providerHome, id);
15281
15455
  const archivePath = resolveArchivePath(providerHome, archive.id);
15282
- await fs11.mkdir(providerHome, { recursive: true });
15456
+ await fs12.mkdir(providerHome, { recursive: true });
15283
15457
  for (const name of fileNames) {
15284
15458
  const hostPath = resolveProviderHostFilePath(providerHome, name);
15285
15459
  if (archive.files[name]?.exists) {
15286
- const content = await fs11.readFile(path11.join(archivePath, name), "utf8");
15287
- await fs11.writeFile(hostPath, content, "utf8");
15460
+ const content = await fs12.readFile(path12.join(archivePath, name), "utf8");
15461
+ await fs12.writeFile(hostPath, content, "utf8");
15288
15462
  } else {
15289
- await fs11.rm(hostPath, { force: true });
15463
+ await fs12.rm(hostPath, { force: true });
15290
15464
  }
15291
15465
  }
15292
15466
  await runtime.stop();
@@ -15299,12 +15473,12 @@ var ProviderHostConfigService = class {
15299
15473
  };
15300
15474
 
15301
15475
  // src/shell/shell-session-service.ts
15302
- import fs12 from "fs/promises";
15476
+ import fs13 from "fs/promises";
15303
15477
  import os3 from "os";
15304
- import path12 from "path";
15478
+ import path13 from "path";
15305
15479
  async function pathExists3(filePath) {
15306
15480
  try {
15307
- await fs12.access(filePath);
15481
+ await fs13.access(filePath);
15308
15482
  return true;
15309
15483
  } catch {
15310
15484
  return false;
@@ -15329,7 +15503,7 @@ function basenameFromPath2(filePath) {
15329
15503
  if (!normalized) {
15330
15504
  return "";
15331
15505
  }
15332
- return path12.basename(normalized) || normalized;
15506
+ return path13.basename(normalized) || normalized;
15333
15507
  }
15334
15508
  function isInteractiveShellCommand(command) {
15335
15509
  const normalized = (command ?? "").trim().toLowerCase();
@@ -15490,11 +15664,11 @@ function buildShellPromptInitScriptContents(command) {
15490
15664
  async function ensureShellPromptInitScript(command) {
15491
15665
  const normalized = command.trim().toLowerCase();
15492
15666
  const extension = normalized === "zsh" ? "zsh" : "sh";
15493
- const filePath = path12.join(
15667
+ const filePath = path13.join(
15494
15668
  os3.tmpdir(),
15495
15669
  `remote-codex-shell-prompt.${extension}`
15496
15670
  );
15497
- await fs12.writeFile(filePath, buildShellPromptInitScriptContents(command), "utf8");
15671
+ await fs13.writeFile(filePath, buildShellPromptInitScriptContents(command), "utf8");
15498
15672
  return filePath;
15499
15673
  }
15500
15674
  async function buildShellPromptInitCommand(command, options = {}) {
@@ -16080,8 +16254,8 @@ var ShellSessionService = class {
16080
16254
  };
16081
16255
 
16082
16256
  // src/shell/tmux-manager.ts
16083
- import fs13 from "fs";
16084
- import path13 from "path";
16257
+ import fs14 from "fs";
16258
+ import path14 from "path";
16085
16259
  import { spawn as spawnChild } from "child_process";
16086
16260
  async function defaultExecCommand(command, args) {
16087
16261
  return await new Promise((resolve, reject) => {
@@ -16108,17 +16282,17 @@ async function defaultExecCommand(command, args) {
16108
16282
  });
16109
16283
  }
16110
16284
  function resolveExecutablePath(command) {
16111
- if (command.includes(path13.sep)) {
16285
+ if (command.includes(path14.sep)) {
16112
16286
  return command;
16113
16287
  }
16114
16288
  const searchPath = process.env.PATH ?? "";
16115
- for (const entry of searchPath.split(path13.delimiter)) {
16289
+ for (const entry of searchPath.split(path14.delimiter)) {
16116
16290
  const trimmed = entry.trim();
16117
16291
  if (!trimmed) {
16118
16292
  continue;
16119
16293
  }
16120
- const candidate = path13.join(trimmed, command);
16121
- if (fs13.existsSync(candidate)) {
16294
+ const candidate = path14.join(trimmed, command);
16295
+ if (fs14.existsSync(candidate)) {
16122
16296
  return candidate;
16123
16297
  }
16124
16298
  }
@@ -16447,16 +16621,16 @@ var HttpError = class extends Error {
16447
16621
  };
16448
16622
  function findRepoRoot(start = process.cwd()) {
16449
16623
  if (process.env.REMOTE_CODEX_REPO_ROOT) {
16450
- return path14.resolve(process.env.REMOTE_CODEX_REPO_ROOT);
16624
+ return path15.resolve(process.env.REMOTE_CODEX_REPO_ROOT);
16451
16625
  }
16452
- let current = path14.resolve(start);
16453
- while (current !== path14.dirname(current)) {
16454
- if (fs14.existsSync(path14.join(current, "pnpm-workspace.yaml")) && fs14.existsSync(path14.join(current, "scripts", "service-restart.mjs"))) {
16626
+ let current = path15.resolve(start);
16627
+ while (current !== path15.dirname(current)) {
16628
+ if (fs15.existsSync(path15.join(current, "pnpm-workspace.yaml")) && fs15.existsSync(path15.join(current, "scripts", "service-restart.mjs"))) {
16455
16629
  return current;
16456
16630
  }
16457
- current = path14.dirname(current);
16631
+ current = path15.dirname(current);
16458
16632
  }
16459
- return path14.resolve(process.cwd());
16633
+ return path15.resolve(process.cwd());
16460
16634
  }
16461
16635
  function createServiceLifecycle() {
16462
16636
  return {
@@ -16468,8 +16642,8 @@ function createServiceLifecycle() {
16468
16642
  });
16469
16643
  }
16470
16644
  const repoRoot = findRepoRoot();
16471
- const restartScript = path14.join(repoRoot, "scripts", "service-restart.mjs");
16472
- if (!fs14.existsSync(restartScript) || !fs14.existsSync(path14.join(repoRoot, "pnpm-workspace.yaml"))) {
16645
+ const restartScript = path15.join(repoRoot, "scripts", "service-restart.mjs");
16646
+ if (!fs15.existsSync(restartScript) || !fs15.existsSync(path15.join(repoRoot, "pnpm-workspace.yaml"))) {
16473
16647
  throw new HttpError(503, {
16474
16648
  code: "service_unavailable",
16475
16649
  message: "Build and restart requires a Remote Codex source checkout. Set REMOTE_CODEX_REPO_ROOT to the checkout path, or update the npm package with npm install -g remote-codex@latest."
@@ -16500,7 +16674,7 @@ function buildApp(options = {}) {
16500
16674
  eventBus,
16501
16675
  runtimeBootstrap.localCodexSessionStore,
16502
16676
  config.workspaceRoot,
16503
- config.codexHome
16677
+ runtimeBootstrap.codexManagement
16504
16678
  );
16505
16679
  const shellService = options.shellService ?? new ShellSessionService(database.db, eventBus, new TmuxManager());
16506
16680
  const providerHostConfigService = new ProviderHostConfigService(
@@ -16810,7 +16984,7 @@ function makeShellErrorEnvelope(shellId, error) {
16810
16984
  }
16811
16985
 
16812
16986
  // src/index.ts
16813
- if (fs15.existsSync(".env")) {
16987
+ if (fs16.existsSync(".env")) {
16814
16988
  process.loadEnvFile?.(".env");
16815
16989
  }
16816
16990
  var app = buildApp();