replicas-engine 0.1.224 → 0.1.226

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/src/index.js +1904 -1486
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -20,6 +20,11 @@ import { readFileSync } from "fs";
20
20
  import { homedir } from "os";
21
21
  import { join } from "path";
22
22
 
23
+ // ../shared/src/type-guards.ts
24
+ function isRecord(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
27
+
23
28
  // ../shared/src/agent.ts
24
29
  var CODEX_REASONING_EFFORT_BY_THINKING_LEVEL = {
25
30
  low: "low",
@@ -35,6 +40,7 @@ function codexReasoningEffortForThinkingLevel(thinkingLevel) {
35
40
  var ACCEPTED_USER_MESSAGE_SOURCE = "replicas-chat-turn-accepted";
36
41
  var USER_MESSAGE_ID_PAYLOAD_KEY = "replicasMessageId";
37
42
  var CODEX_ASP_ITEM_ID_PAYLOAD_KEY = "codexAspItemId";
43
+ var CODEX_ASP_TRANSCRIPT_UPDATED_EVENT_TYPE = "codex-asp-transcript-updated";
38
44
  var CODEX_QUOTA_STATUS_EVENT_TYPE = "codex-quota-status";
39
45
  var COMPACTION_STATUS_EVENT_TYPE = "compaction-status";
40
46
  var CHAT_GOAL_EVENT_TYPE = "chat-goal";
@@ -1648,7 +1654,7 @@ var DEFAULT_WARM_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
1648
1654
  var MAX_WARM_HOOK_TIMEOUT_MS = 15 * 60 * 1e3;
1649
1655
  var DEFAULT_HOOK_OUTPUT_PREVIEW_CHARS = 1e5;
1650
1656
  var HOOK_EXEC_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
1651
- function isRecord(value) {
1657
+ function isRecord2(value) {
1652
1658
  return typeof value === "object" && value !== null;
1653
1659
  }
1654
1660
  function clampWarmHookTimeoutMs(timeoutMs) {
@@ -1672,7 +1678,7 @@ function parseWarmHookConfig(value, filename = "replicas.json") {
1672
1678
  if (typeof value === "string") {
1673
1679
  return value;
1674
1680
  }
1675
- if (!isRecord(value)) {
1681
+ if (!isRecord2(value)) {
1676
1682
  throw new Error(`Invalid ${filename}: "warmHook" must be a string or object`);
1677
1683
  }
1678
1684
  if (!Array.isArray(value.commands) || !value.commands.every((entry) => typeof entry === "string")) {
@@ -1708,11 +1714,11 @@ function resolveWarmHookConfig(value) {
1708
1714
 
1709
1715
  // ../shared/src/replicas-config.ts
1710
1716
  var REPLICAS_CONFIG_FILENAMES = ["replicas.json", "replicas.yaml", "replicas.yml"];
1711
- function isRecord2(value) {
1717
+ function isRecord3(value) {
1712
1718
  return typeof value === "object" && value !== null;
1713
1719
  }
1714
1720
  function parseReplicasConfig(value, filename = "replicas.json") {
1715
- if (!isRecord2(value)) {
1721
+ if (!isRecord3(value)) {
1716
1722
  throw new Error(`Invalid ${filename}: expected an object`);
1717
1723
  }
1718
1724
  const config = {};
@@ -1729,7 +1735,7 @@ function parseReplicasConfig(value, filename = "replicas.json") {
1729
1735
  config.systemPrompt = value.systemPrompt;
1730
1736
  }
1731
1737
  if ("startHook" in value) {
1732
- if (!isRecord2(value.startHook)) {
1738
+ if (!isRecord3(value.startHook)) {
1733
1739
  throw new Error(`Invalid ${filename}: "startHook" must be an object with "commands" array`);
1734
1740
  }
1735
1741
  const { commands, timeout, separate } = value.startHook;
@@ -1767,7 +1773,7 @@ function isClaudeAuthErrorText(text) {
1767
1773
  }
1768
1774
 
1769
1775
  // ../shared/src/engine/environment.ts
1770
- var DAYTONA_SNAPSHOT_ID = "28-05-2026-royal-york-v1";
1776
+ var DAYTONA_SNAPSHOT_ID = "28-05-2026-royal-york-v3";
1771
1777
 
1772
1778
  // ../shared/src/engine/types.ts
1773
1779
  var DEFAULT_CHAT_TITLES = {
@@ -1806,6 +1812,15 @@ var IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
1806
1812
 
1807
1813
  // ../shared/src/engine/v1.ts
1808
1814
  var MERGED_MESSAGE_SEPARATOR = "\n\n<!-- replicas:merged -->\n\n";
1815
+ function normalizeCodexAspTranscriptStatus(status, failed = false) {
1816
+ if (failed || status === "failed") return "failed";
1817
+ if (status === "completed") return "completed";
1818
+ return "in_progress";
1819
+ }
1820
+ function isCodexAspTranscript(value) {
1821
+ if (!isRecord(value)) return false;
1822
+ return typeof value.threadId === "string" && typeof value.updatedAt === "string" && Array.isArray(value.turns);
1823
+ }
1809
1824
 
1810
1825
  // ../shared/src/routes/codex.ts
1811
1826
  var CODEX_AUTH_ENV_KEYS = [
@@ -1879,14 +1894,6 @@ var AUDIT_LOG_ACTION = {
1879
1894
  };
1880
1895
  var AUDIT_LOG_ACTIONS = Object.values(AUDIT_LOG_ACTION);
1881
1896
 
1882
- // ../shared/src/object-store/types.ts
1883
- var MEDIA_KIND = {
1884
- IMAGE: "image",
1885
- VIDEO: "video",
1886
- AUDIO: "audio"
1887
- };
1888
- var MEDIA_KINDS = [MEDIA_KIND.IMAGE, MEDIA_KIND.VIDEO, MEDIA_KIND.AUDIO];
1889
-
1890
1897
  // ../shared/src/agent-event-utils.ts
1891
1898
  function getUserMessage(event) {
1892
1899
  return event.type === "event_msg" && event.payload.type === "user_message" && typeof event.payload.message === "string" ? event.payload.message : null;
@@ -1899,9 +1906,12 @@ function getUserMessageItemId(event) {
1899
1906
  const itemId = event.payload[CODEX_ASP_ITEM_ID_PAYLOAD_KEY];
1900
1907
  return typeof itemId === "string" ? itemId : null;
1901
1908
  }
1909
+ function parseTimestampMs(timestamp) {
1910
+ const value = Date.parse(timestamp);
1911
+ return Number.isFinite(value) ? value : 0;
1912
+ }
1902
1913
  function getEventTimestampMs(event) {
1903
- const value = Date.parse(event.timestamp);
1904
- return Number.isNaN(value) ? 0 : value;
1914
+ return parseTimestampMs(event.timestamp);
1905
1915
  }
1906
1916
  function areSameUserMessageEvents(a, b) {
1907
1917
  const aMessage = getUserMessage(a);
@@ -1915,21 +1925,17 @@ function areSameUserMessageEvents(a, b) {
1915
1925
  if (aItemId || bItemId) return aItemId === bItemId;
1916
1926
  return Math.abs(getEventTimestampMs(a) - getEventTimestampMs(b)) <= 3e4;
1917
1927
  }
1918
- function mergeAgentEvents(primary, supplemental, options) {
1919
- const merged = [...primary];
1920
- const { areDuplicates, mergeEvent } = options;
1921
- for (const event of supplemental) {
1922
- const existingIndex = merged.findIndex((existing) => areDuplicates(existing, event));
1923
- if (existingIndex === -1) {
1924
- merged.push(event);
1925
- continue;
1926
- }
1927
- if (mergeEvent) {
1928
- merged[existingIndex] = mergeEvent(merged[existingIndex], event);
1929
- }
1930
- }
1931
- return merged;
1932
- }
1928
+
1929
+ // ../shared/src/display-message/parsers/codex-asp-parser.ts
1930
+ var DUPLICATE_WINDOW_MS = 5 * 60 * 1e3;
1931
+
1932
+ // ../shared/src/object-store/types.ts
1933
+ var MEDIA_KIND = {
1934
+ IMAGE: "image",
1935
+ VIDEO: "video",
1936
+ AUDIO: "audio"
1937
+ };
1938
+ var MEDIA_KINDS = [MEDIA_KIND.IMAGE, MEDIA_KIND.VIDEO, MEDIA_KIND.AUDIO];
1933
1939
 
1934
1940
  // src/runtime-env-loader.ts
1935
1941
  function loadRuntimeEnvFile() {
@@ -2013,8 +2019,7 @@ function loadEngineEnv() {
2013
2019
  AWS_REGION: readEnv("AWS_REGION"),
2014
2020
  REPLICAS_CLAUDE_AUTH_METHOD: parseClaudeAuthMethod(readEnv("REPLICAS_CLAUDE_AUTH_METHOD")),
2015
2021
  REPLICAS_CODEX_AUTH_METHOD: parseCodexAuthMethod(readEnv("REPLICAS_CODEX_AUTH_METHOD")),
2016
- REPLICAS_ENV_SYSTEM_PROMPT: readEnv("REPLICAS_ENV_SYSTEM_PROMPT"),
2017
- CODEX_ASP_ENABLED: readEnv("CODEX_ASP_ENABLED")?.toLowerCase() === "true"
2022
+ REPLICAS_ENV_SYSTEM_PROMPT: readEnv("REPLICAS_ENV_SYSTEM_PROMPT")
2018
2023
  };
2019
2024
  if (!IS_WARMING_MODE && !env.WORKSPACE_ID) {
2020
2025
  console.error("WORKSPACE_ID is not set \u2014 this is required in normal (non-warming) mode");
@@ -2423,7 +2428,7 @@ import { join as join3 } from "path";
2423
2428
  import { homedir as homedir3 } from "os";
2424
2429
 
2425
2430
  // src/utils/type-guards.ts
2426
- function isRecord3(value) {
2431
+ function isRecord4(value) {
2427
2432
  return typeof value === "object" && value !== null;
2428
2433
  }
2429
2434
 
@@ -2455,10 +2460,10 @@ async function updateEngineState(updater) {
2455
2460
  });
2456
2461
  }
2457
2462
  function isEngineRepoDiff(value) {
2458
- return isRecord3(value) && typeof value.added === "number" && typeof value.removed === "number";
2463
+ return isRecord4(value) && typeof value.added === "number" && typeof value.removed === "number";
2459
2464
  }
2460
2465
  function coerceRepoState(value) {
2461
- if (!isRecord3(value)) {
2466
+ if (!isRecord4(value)) {
2462
2467
  return null;
2463
2468
  }
2464
2469
  if (typeof value.name !== "string") return null;
@@ -2484,11 +2489,11 @@ function coerceRepoState(value) {
2484
2489
  };
2485
2490
  }
2486
2491
  function coerceEngineState(value) {
2487
- if (!isRecord3(value)) {
2492
+ if (!isRecord4(value)) {
2488
2493
  return {};
2489
2494
  }
2490
2495
  const partial = {};
2491
- if (isRecord3(value.repos)) {
2496
+ if (isRecord4(value.repos)) {
2492
2497
  const repos = {};
2493
2498
  for (const [repoName, repoState] of Object.entries(value.repos)) {
2494
2499
  const coerced = coerceRepoState(repoState);
@@ -3700,10 +3705,10 @@ import { homedir as homedir11 } from "os";
3700
3705
  // src/utils/jsonl-reader.ts
3701
3706
  import { readFile as readFile6 } from "fs/promises";
3702
3707
  function isJsonlEvent(value) {
3703
- if (!isRecord3(value)) {
3708
+ if (!isRecord4(value)) {
3704
3709
  return false;
3705
3710
  }
3706
- return typeof value.timestamp === "string" && typeof value.type === "string" && isRecord3(value.payload);
3711
+ return typeof value.timestamp === "string" && typeof value.type === "string" && isRecord4(value.payload);
3707
3712
  }
3708
3713
  function parseJsonlEvents(lines) {
3709
3714
  const events = [];
@@ -5349,765 +5354,279 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
5349
5354
  }
5350
5355
  };
5351
5356
 
5352
- // src/managers/codex-manager.ts
5353
- import { Codex } from "@openai/codex-sdk";
5354
- import { readdir as readdir3, stat as stat2, writeFile as writeFile8, mkdir as mkdir10, readFile as readFile7 } from "fs/promises";
5355
- import { existsSync as existsSync6 } from "fs";
5356
- import { join as join14 } from "path";
5357
- import { homedir as homedir12 } from "os";
5358
- import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
5359
-
5360
- // src/utils/codex-quota.ts
5361
- function buildCodexRateLimitsSnapshot(fields) {
5362
- if (fields.unlimited === true) return null;
5363
- let state = "ok";
5364
- if (fields.hasCredits === false) {
5365
- state = "out_of_credits";
5366
- } else if (fields.rateLimitResetType !== null) {
5367
- state = "rate_limited";
5368
- }
5369
- return {
5370
- state,
5371
- balance: fields.balance,
5372
- rateLimitResetType: fields.rateLimitResetType,
5373
- planType: fields.planType
5374
- };
5375
- }
5376
- function extractCodexRateLimitsSnapshotFromJsonl(parsed) {
5377
- if (!isRecord3(parsed)) return null;
5378
- const payload = isRecord3(parsed.payload) ? parsed.payload : parsed;
5379
- const rateLimits = isRecord3(payload.rate_limits) ? payload.rate_limits : null;
5380
- if (!rateLimits) return null;
5381
- const credits = isRecord3(rateLimits.credits) ? rateLimits.credits : null;
5382
- const hasCredits = credits && typeof credits.has_credits === "boolean" ? credits.has_credits : null;
5383
- const unlimited = credits && typeof credits.unlimited === "boolean" ? credits.unlimited : null;
5384
- const balance = credits && typeof credits.balance === "string" ? credits.balance : null;
5385
- const rateLimitResetType = typeof rateLimits.rate_limit_reached_type === "string" && rateLimits.rate_limit_reached_type.length > 0 ? rateLimits.rate_limit_reached_type : null;
5386
- const planType = typeof rateLimits.plan_type === "string" ? rateLimits.plan_type : null;
5387
- return buildCodexRateLimitsSnapshot({
5388
- unlimited,
5389
- hasCredits,
5390
- balance,
5391
- rateLimitResetType,
5392
- planType
5393
- });
5394
- }
5395
- var CodexQuotaStatusTracker = class {
5396
- lastEmittedQuotaState = "ok";
5397
- latestQuotaSnapshot = null;
5398
- quotaBlocked = false;
5399
- get blocked() {
5400
- return this.quotaBlocked;
5401
- }
5402
- get latestSnapshot() {
5403
- return this.latestQuotaSnapshot;
5404
- }
5405
- prime(snapshot) {
5406
- this.latestQuotaSnapshot = snapshot;
5407
- this.quotaBlocked = snapshot.state === "out_of_credits";
5408
- }
5409
- apply(snapshot, force = false) {
5410
- const stateChanged = snapshot.state !== this.lastEmittedQuotaState;
5411
- if (!stateChanged && !force) {
5412
- return null;
5413
- }
5414
- this.lastEmittedQuotaState = snapshot.state;
5415
- this.quotaBlocked = snapshot.state === "out_of_credits";
5416
- this.latestQuotaSnapshot = snapshot;
5417
- const payload = {
5418
- state: snapshot.state,
5419
- balance: snapshot.balance,
5420
- rateLimitResetType: snapshot.rateLimitResetType,
5421
- planType: snapshot.planType
5422
- };
5423
- const event = {
5424
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5425
- type: CODEX_QUOTA_STATUS_EVENT_TYPE,
5426
- payload
5427
- };
5428
- return event;
5429
- }
5430
- };
5357
+ // src/managers/codex-asp/app-server-process.ts
5358
+ import { spawn } from "child_process";
5359
+ import { EventEmitter as EventEmitter2 } from "events";
5431
5360
 
5432
- // src/utils/codex-auth.ts
5433
- function isCodexAuthError(error) {
5434
- const msg = error instanceof Error ? error.message : String(error);
5435
- const lower = msg.toLowerCase();
5436
- return lower.includes("unauthorized") || lower.includes("authentication") || lower.includes("invalid api key") || lower.includes("incorrect api key") || lower.includes("api key") && lower.includes("invalid") || lower.includes("401") || lower.includes('codexerrorinfo="unauthorized"') || lower.includes("codexerrorinfo=unauthorized");
5361
+ // src/managers/codex-asp/asp-client.ts
5362
+ import { EventEmitter } from "events";
5363
+ var DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
5364
+ function hasOwn(record, key) {
5365
+ return Object.prototype.hasOwnProperty.call(record, key);
5437
5366
  }
5438
-
5439
- // src/managers/codex-manager.ts
5440
- var DEFAULT_MODEL = "gpt-5.5";
5441
- var CODEX_CONFIG_PATH = join14(homedir12(), ".codex", "config.toml");
5442
- function isJsonlEvent2(value) {
5443
- if (!isRecord3(value)) {
5444
- return false;
5367
+ var AspClient = class {
5368
+ stdin;
5369
+ stdout;
5370
+ emitter = new EventEmitter();
5371
+ pending = /* @__PURE__ */ new Map();
5372
+ nextId = 1;
5373
+ lineBuffer = "";
5374
+ disposed = false;
5375
+ get isDisposed() {
5376
+ return this.disposed;
5445
5377
  }
5446
- return typeof value.timestamp === "string" && typeof value.type === "string" && isRecord3(value.payload);
5447
- }
5448
- function sleep(ms) {
5449
- return new Promise((resolve3) => setTimeout(resolve3, ms));
5450
- }
5451
- var CodexManager = class extends CodingAgentManager {
5452
- codex;
5453
- currentThreadId = null;
5454
- currentThread = null;
5455
- activeAbortController = null;
5456
- quotaStatus = new CodexQuotaStatusTracker();
5457
5378
  constructor(options) {
5458
- super(options);
5459
- this.codex = this.createCodexClient();
5460
- this.initializeManager(this.processMessageInternal.bind(this));
5379
+ this.stdin = options.stdin;
5380
+ this.stdout = options.stdout;
5381
+ this.stdout.setEncoding("utf8");
5382
+ this.stdout.on("data", this.handleStdoutData);
5383
+ this.stdin.on("error", this.handleStdinError);
5461
5384
  }
5462
- createCodexClient() {
5463
- const codexApiKey = resolveCodexApiKey();
5464
- return new Codex({
5465
- env: buildCodexAgentEnv(),
5466
- ...codexApiKey ? { apiKey: codexApiKey } : {}
5467
- });
5385
+ on(event, listener) {
5386
+ this.emitter.on(event, listener);
5468
5387
  }
5469
- resetCodexClient() {
5470
- this.codex = this.createCodexClient();
5471
- this.currentThread = null;
5388
+ off(event, listener) {
5389
+ this.emitter.off(event, listener);
5472
5390
  }
5473
- async initialize() {
5474
- if (this.initialSessionId) {
5475
- this.currentThreadId = this.initialSessionId;
5476
- console.log(`[CodexManager] Restored thread ID from persisted state: ${this.currentThreadId}`);
5391
+ async request(method, params, opts) {
5392
+ if (this.disposed) {
5393
+ throw new Error(`Cannot send ${method}: ASP client disposed`);
5477
5394
  }
5395
+ const id = this.nextId;
5396
+ this.nextId += 1;
5397
+ const promise = new Promise((resolve3, reject) => {
5398
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
5399
+ const timer = timeoutMs > 0 ? setTimeout(() => {
5400
+ this.pending.delete(id);
5401
+ reject(new Error(`ASP request timed out for ${method}`));
5402
+ }, timeoutMs) : null;
5403
+ this.pending.set(id, { resolve: resolve3, reject, method, timer });
5404
+ });
5405
+ this.write({ method, id, params });
5406
+ return promise;
5478
5407
  }
5479
- async flushQuotaSnapshotFromCurrentSession() {
5480
- if (!this.currentThreadId) return;
5408
+ notify(method, params) {
5409
+ if (this.disposed) {
5410
+ return;
5411
+ }
5481
5412
  try {
5482
- const sessionFile = await this.findSessionFile(this.currentThreadId);
5483
- if (!sessionFile) return;
5484
- const content = await readFile7(sessionFile, "utf-8");
5485
- const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
5486
- let latest = null;
5487
- for (const line of lines) {
5488
- try {
5489
- const parsed = JSON.parse(line);
5490
- const snapshot = extractCodexRateLimitsSnapshotFromJsonl(parsed);
5491
- if (snapshot) {
5492
- latest = snapshot;
5493
- }
5494
- } catch {
5495
- }
5496
- }
5497
- if (latest) {
5498
- this.emitQuotaStatus(latest);
5499
- }
5413
+ this.write(params === void 0 ? { method } : { method, params });
5500
5414
  } catch (error) {
5501
- console.warn("[CodexManager] Failed to flush quota snapshot from session:", error);
5415
+ console.warn(`[AspClient] Failed to send notification ${method}:`, error);
5502
5416
  }
5503
5417
  }
5504
- emitQuotaStatus(snapshot, force = false) {
5505
- const event = this.quotaStatus.apply(snapshot, force);
5506
- if (event) this.onEvent(event);
5418
+ respond(id, result) {
5419
+ if (this.disposed) {
5420
+ return;
5421
+ }
5422
+ try {
5423
+ this.write({ id, result });
5424
+ } catch (error) {
5425
+ console.warn(`[AspClient] Failed to send response ${String(id)}:`, error);
5426
+ }
5507
5427
  }
5508
- async interruptActiveTurn() {
5509
- if (this.activeAbortController) {
5510
- this.activeAbortController.abort();
5428
+ dispose(reason = new Error("ASP client disposed")) {
5429
+ if (this.disposed) {
5430
+ return;
5511
5431
  }
5432
+ this.disposed = true;
5433
+ this.stdout.off("data", this.handleStdoutData);
5434
+ this.stdin.removeListener("error", this.handleStdinError);
5435
+ for (const [id, pending] of this.pending) {
5436
+ if (pending.timer) {
5437
+ clearTimeout(pending.timer);
5438
+ }
5439
+ pending.reject(new Error(`${reason.message} while waiting for ${pending.method}`));
5440
+ this.pending.delete(id);
5441
+ }
5442
+ this.lineBuffer = "";
5443
+ this.emitter.emit("dispose", reason);
5444
+ this.emitter.removeAllListeners();
5512
5445
  }
5513
- /**
5514
- * Update the developer_instructions in ~/.codex/config.toml
5515
- * This sets the system prompt that Codex will use for this turn
5516
- */
5517
- async updateCodexConfig(developerInstructions) {
5518
- try {
5519
- const codexDir = join14(homedir12(), ".codex");
5520
- await mkdir10(codexDir, { recursive: true });
5521
- let config = {};
5522
- if (existsSync6(CODEX_CONFIG_PATH)) {
5523
- try {
5524
- const existingContent = await readFile7(CODEX_CONFIG_PATH, "utf-8");
5525
- const parsed = parseToml(existingContent);
5526
- if (isRecord3(parsed)) {
5527
- config = parsed;
5528
- }
5529
- } catch (parseError) {
5530
- console.warn("[CodexManager] Failed to parse existing config.toml, starting fresh:", parseError);
5531
- }
5532
- }
5533
- if (developerInstructions) {
5534
- config.developer_instructions = developerInstructions;
5535
- } else {
5536
- delete config.developer_instructions;
5446
+ handleStdoutData = (chunk) => {
5447
+ this.lineBuffer += chunk.toString();
5448
+ let newlineIndex = this.lineBuffer.indexOf("\n");
5449
+ while (newlineIndex >= 0) {
5450
+ const line = this.lineBuffer.slice(0, newlineIndex).trim();
5451
+ this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
5452
+ if (line.length > 0) {
5453
+ this.handleLine(line);
5537
5454
  }
5538
- const tomlContent = stringifyToml(config);
5539
- await writeFile8(CODEX_CONFIG_PATH, tomlContent, "utf-8");
5540
- console.log("[CodexManager] Updated config.toml with developer_instructions");
5541
- } catch (error) {
5542
- console.error("[CodexManager] Failed to update config.toml:", error);
5455
+ newlineIndex = this.lineBuffer.indexOf("\n");
5543
5456
  }
5544
- }
5545
- async processMessageInternal(request) {
5457
+ };
5458
+ handleStdinError = (error) => {
5459
+ this.dispose(new Error(`ASP stdin error: ${error.message}`));
5460
+ };
5461
+ handleLine(line) {
5462
+ let parsed;
5546
5463
  try {
5547
- await this.executeCodexTurn(request);
5464
+ parsed = JSON.parse(line);
5548
5465
  } catch (error) {
5549
- if (isCodexAuthError(error)) {
5550
- const refreshed = await codexTokenManager.fetchFreshCredentials(error instanceof Error ? error.message : String(error));
5551
- if (refreshed) {
5552
- this.resetCodexClient();
5553
- await this.executeCodexTurn(request);
5554
- return;
5555
- }
5556
- }
5557
- throw error;
5466
+ console.warn("[AspClient] Failed to parse ASP JSON line:", error);
5467
+ return;
5558
5468
  }
5559
- }
5560
- async executeCodexTurn(request) {
5561
- if (this.quotaStatus.blocked && this.quotaStatus.latestSnapshot) {
5562
- await this.flushQuotaSnapshotFromCurrentSession();
5563
- if (this.quotaStatus.blocked && this.quotaStatus.latestSnapshot) {
5564
- this.emitQuotaStatus(this.quotaStatus.latestSnapshot, true);
5565
- try {
5566
- await this.onTurnComplete();
5567
- } catch (error) {
5568
- console.error("[CodexManager] onTurnComplete failed during quota-blocked turn:", error);
5569
- }
5570
- return;
5571
- }
5469
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5470
+ console.warn("[AspClient] Ignoring non-object ASP message");
5471
+ return;
5572
5472
  }
5573
- const {
5574
- message,
5575
- model,
5576
- customInstructions,
5577
- images,
5578
- permissionMode,
5579
- thinkingLevel
5580
- } = request;
5581
- const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
5582
- let tempImagePaths = [];
5583
- let stopTail = null;
5584
- let abortController = null;
5585
- try {
5586
- if (images && images.length > 0) {
5587
- const normalizedImages = await normalizeImages(images);
5588
- tempImagePaths = await saveNormalizedImagesToTempFiles(normalizedImages);
5589
- }
5590
- const developerInstructions = this.buildCombinedInstructions(customInstructions);
5591
- await this.updateCodexConfig(developerInstructions);
5592
- const sandboxMode = "danger-full-access";
5593
- const webSearchMode = "live";
5594
- const codexReasoningEffort = codexReasoningEffortForThinkingLevel(thinkingLevel);
5595
- const additionalDirectories = await getAgentAdditionalDirectories();
5596
- const threadOptions = {
5597
- workingDirectory: this.workingDirectory,
5598
- skipGitRepoCheck: true,
5599
- sandboxMode,
5600
- model: model || DEFAULT_MODEL,
5601
- webSearchMode,
5602
- additionalDirectories,
5603
- ...codexReasoningEffort ? { modelReasoningEffort: codexReasoningEffort } : {}
5604
- };
5605
- abortController = new AbortController();
5606
- this.activeAbortController = abortController;
5607
- if (this.currentThreadId) {
5608
- this.currentThread = this.codex.resumeThread(this.currentThreadId, threadOptions);
5609
- } else {
5610
- this.currentThread = this.codex.startThread(threadOptions);
5611
- const { events } = await this.currentThread.runStreamed("Hello", { signal: abortController.signal });
5612
- for await (const event of events) {
5613
- if (event.type === "thread.started") {
5614
- this.currentThreadId = event.thread_id;
5615
- await this.onSaveSessionId(this.currentThreadId);
5616
- console.log(`[CodexManager] Captured and persisted thread ID: ${this.currentThreadId}`);
5617
- }
5618
- }
5619
- if (!this.currentThreadId && this.currentThread.id) {
5620
- this.currentThreadId = this.currentThread.id;
5621
- await this.onSaveSessionId(this.currentThreadId);
5622
- console.log(`[CodexManager] Captured and persisted thread ID from thread.id: ${this.currentThreadId}`);
5623
- }
5624
- }
5625
- stopTail = this.currentThreadId ? await this.startSessionTail(this.currentThreadId) : null;
5626
- let input;
5627
- if (tempImagePaths.length > 0) {
5628
- const inputItems = [
5629
- { type: "text", text: message },
5630
- ...tempImagePaths.map((path4) => ({ type: "local_image", path: path4 }))
5631
- ];
5632
- input = inputItems;
5633
- } else {
5634
- input = message;
5635
- }
5636
- try {
5637
- const { events } = await this.currentThread.runStreamed(input, { signal: abortController.signal });
5638
- const linearForwarder = new LinearEventForwarder(linearSessionId);
5639
- for await (const event of events) {
5640
- if (linearSessionId) {
5641
- linearForwarder.sendPlan(extractPlanFromCodexEvent(event));
5642
- linearForwarder.sendEvent(convertCodexEvent(event, linearSessionId));
5643
- }
5644
- }
5645
- linearForwarder.flushThoughtAsResponse();
5646
- } catch (error) {
5647
- await this.flushQuotaSnapshotFromCurrentSession();
5648
- if (this.quotaStatus.blocked) {
5649
- return;
5650
- }
5651
- throw error;
5652
- }
5653
- } finally {
5654
- if (stopTail) {
5655
- await stopTail();
5656
- }
5657
- await removeTempImageFiles(tempImagePaths);
5658
- try {
5659
- await this.onTurnComplete();
5660
- } catch (error) {
5661
- console.error("[CodexManager] onTurnComplete failed:", error);
5662
- }
5663
- this.activeAbortController = null;
5473
+ const message = parsed;
5474
+ const hasRequestId = typeof message.id === "number" || typeof message.id === "string";
5475
+ if (hasRequestId && (hasOwn(message, "result") || hasOwn(message, "error"))) {
5476
+ this.handleResponse(message);
5477
+ return;
5478
+ }
5479
+ if (hasRequestId && typeof message.method === "string") {
5480
+ this.emitter.emit("serverRequest", message);
5481
+ return;
5482
+ }
5483
+ if (!hasOwn(message, "id") && typeof message.method === "string") {
5484
+ this.emitter.emit("notification", message);
5664
5485
  }
5665
5486
  }
5666
- async getHistory() {
5667
- if (!this.currentThreadId) {
5668
- return {
5669
- thread_id: null,
5670
- events: []
5671
- };
5487
+ handleResponse(message) {
5488
+ if (typeof message.id !== "number") {
5489
+ console.warn("[AspClient] Ignoring response with non-numeric request id");
5490
+ return;
5672
5491
  }
5673
- const sessionFile = await this.findSessionFile(this.currentThreadId);
5674
- if (!sessionFile) {
5675
- return {
5676
- thread_id: this.currentThreadId,
5677
- events: []
5678
- };
5492
+ const pending = this.pending.get(message.id);
5493
+ if (!pending) {
5494
+ console.warn(`[AspClient] Ignoring response for unknown request id ${message.id}`);
5495
+ return;
5679
5496
  }
5680
- const events = await readJSONL(sessionFile);
5681
- return {
5682
- thread_id: this.currentThreadId,
5683
- events
5684
- };
5497
+ this.pending.delete(message.id);
5498
+ if (pending.timer) {
5499
+ clearTimeout(pending.timer);
5500
+ }
5501
+ if (hasOwn(message, "error")) {
5502
+ pending.reject(this.createRpcError(pending.method, message.error));
5503
+ return;
5504
+ }
5505
+ pending.resolve(message.result);
5685
5506
  }
5686
- // Helper methods for finding session files
5687
- async findSessionFile(threadId) {
5688
- const sessionsDir = join14(homedir12(), ".codex", "sessions");
5689
- try {
5690
- const now = /* @__PURE__ */ new Date();
5691
- const year = now.getFullYear();
5692
- const month = String(now.getMonth() + 1).padStart(2, "0");
5693
- const day = String(now.getDate()).padStart(2, "0");
5694
- const todayDir = join14(sessionsDir, String(year), month, day);
5695
- const file = await this.findFileInDirectory(todayDir, threadId);
5696
- if (file) return file;
5697
- for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
5698
- const date = new Date(now);
5699
- date.setDate(date.getDate() - daysAgo);
5700
- const searchYear = date.getFullYear();
5701
- const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
5702
- const searchDay = String(date.getDate()).padStart(2, "0");
5703
- const searchDir = join14(sessionsDir, String(searchYear), searchMonth, searchDay);
5704
- const file2 = await this.findFileInDirectory(searchDir, threadId);
5705
- if (file2) return file2;
5706
- }
5707
- return null;
5708
- } catch (error) {
5709
- return null;
5507
+ createRpcError(method, error) {
5508
+ if (typeof error !== "object" || error === null || Array.isArray(error)) {
5509
+ return new Error(`ASP request failed for ${method}`);
5710
5510
  }
5511
+ const rpcError = error;
5512
+ const code = typeof rpcError.code === "number" ? ` ${rpcError.code}` : "";
5513
+ const message = typeof rpcError.message === "string" ? rpcError.message : "Unknown ASP error";
5514
+ const data = hasOwn(rpcError, "data") ? ` data=${JSON.stringify(rpcError.data)}` : "";
5515
+ return new Error(`ASP request failed for ${method}:${code} ${message}${data}`);
5711
5516
  }
5712
- async findFileInDirectory(directory, threadId) {
5517
+ write(message) {
5713
5518
  try {
5714
- const files = await readdir3(directory);
5715
- for (const file of files) {
5716
- if (file.endsWith(".jsonl") && file.includes(threadId)) {
5717
- const fullPath = join14(directory, file);
5718
- const stats = await stat2(fullPath);
5719
- if (stats.isFile()) {
5720
- return fullPath;
5721
- }
5519
+ this.stdin.write(`${JSON.stringify(message)}
5520
+ `, (error) => {
5521
+ if (error) {
5522
+ this.dispose(new Error(`ASP write failed: ${error.message}`));
5722
5523
  }
5723
- }
5724
- return null;
5524
+ });
5725
5525
  } catch (error) {
5726
- return null;
5727
- }
5728
- }
5729
- async waitForSessionFile(threadId, timeoutMs = 5e3) {
5730
- const start = Date.now();
5731
- while (Date.now() - start < timeoutMs) {
5732
- const sessionFile = await this.findSessionFile(threadId);
5733
- if (sessionFile) {
5734
- return sessionFile;
5735
- }
5736
- await sleep(100);
5737
- }
5738
- return null;
5739
- }
5740
- // @openai/codex-sdk doesn't expose manual /compact (TUI-only); we only mirror the auto-compaction rollout entries to the UI.
5741
- trackNativeCompaction(event) {
5742
- if (event.type === "compacted") {
5743
- this.setCompacting(false);
5744
- return;
5745
- }
5746
- if (event.type !== "event_msg") return;
5747
- const msg = event.payload.msg;
5748
- if (!msg) return;
5749
- const itemType = msg.payload?.item?.type;
5750
- if (itemType !== "context_compaction") return;
5751
- if (msg.type === "item_started") {
5752
- this.setCompacting(true);
5753
- } else if (msg.type === "item_completed") {
5754
- this.setCompacting(false);
5755
- }
5756
- }
5757
- async startSessionTail(threadId) {
5758
- const sessionFile = await this.waitForSessionFile(threadId);
5759
- if (!sessionFile) {
5760
- return async () => {
5761
- };
5526
+ const writeError = error instanceof Error ? error : new Error("ASP write failed");
5527
+ this.dispose(writeError);
5528
+ throw writeError;
5762
5529
  }
5763
- let active = true;
5764
- const seenLines = /* @__PURE__ */ new Set();
5765
- const seedSeenLines = async () => {
5766
- try {
5767
- const content = await readFile7(sessionFile, "utf-8");
5768
- const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
5769
- let latest = null;
5770
- for (const line of lines) {
5771
- seenLines.add(line);
5772
- try {
5773
- const parsed = JSON.parse(line);
5774
- const snapshot = extractCodexRateLimitsSnapshotFromJsonl(parsed);
5775
- if (snapshot) latest = snapshot;
5776
- } catch {
5777
- }
5778
- }
5779
- if (latest) {
5780
- this.quotaStatus.prime(latest);
5781
- }
5782
- } catch {
5783
- }
5784
- };
5785
- await seedSeenLines();
5786
- const pump = async () => {
5787
- let emitted = 0;
5788
- try {
5789
- const content = await readFile7(sessionFile, "utf-8");
5790
- const lines = content.split("\n");
5791
- const completeLines = content.endsWith("\n") ? lines : lines.slice(0, -1);
5792
- for (const line of completeLines) {
5793
- const trimmed = line.trim();
5794
- if (!trimmed || seenLines.has(trimmed)) {
5795
- continue;
5796
- }
5797
- seenLines.add(trimmed);
5798
- try {
5799
- const parsed = JSON.parse(trimmed);
5800
- const snapshot = extractCodexRateLimitsSnapshotFromJsonl(parsed);
5801
- if (snapshot) {
5802
- this.emitQuotaStatus(snapshot);
5803
- }
5804
- if (isJsonlEvent2(parsed)) {
5805
- this.trackNativeCompaction(parsed);
5806
- this.onEvent(parsed);
5807
- emitted += 1;
5808
- }
5809
- } catch {
5810
- }
5811
- }
5812
- } catch {
5813
- }
5814
- return emitted;
5815
- };
5816
- const loop = (async () => {
5817
- while (active) {
5818
- await pump();
5819
- await sleep(100);
5820
- }
5821
- await pump();
5822
- })();
5823
- return async () => {
5824
- active = false;
5825
- await loop;
5826
- const deadline = Date.now() + 1500;
5827
- while (Date.now() < deadline) {
5828
- const emitted = await pump();
5829
- if (emitted > 0) {
5830
- continue;
5831
- }
5832
- await sleep(100);
5833
- }
5834
- };
5835
5530
  }
5836
5531
  };
5837
5532
 
5838
5533
  // src/managers/codex-asp/app-server-process.ts
5839
- import { spawn } from "child_process";
5840
- import { EventEmitter as EventEmitter2 } from "events";
5841
-
5842
- // src/managers/codex-asp/asp-client.ts
5843
- import { EventEmitter } from "events";
5844
- var DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
5845
- function hasOwn(record, key) {
5846
- return Object.prototype.hasOwnProperty.call(record, key);
5847
- }
5848
- var AspClient = class {
5849
- stdin;
5850
- stdout;
5851
- emitter = new EventEmitter();
5852
- pending = /* @__PURE__ */ new Map();
5853
- nextId = 1;
5854
- lineBuffer = "";
5855
- disposed = false;
5856
- get isDisposed() {
5857
- return this.disposed;
5858
- }
5859
- constructor(options) {
5860
- this.stdin = options.stdin;
5861
- this.stdout = options.stdout;
5862
- this.stdout.setEncoding("utf8");
5863
- this.stdout.on("data", this.handleStdoutData);
5864
- this.stdin.on("error", this.handleStdinError);
5534
+ var DEFAULT_CODEX_BINARY = "codex";
5535
+ var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
5536
+ var ENGINE_PACKAGE_VERSION = "0.1.213";
5537
+ var INITIALIZE_METHOD = "initialize";
5538
+ var INITIALIZED_NOTIFICATION = "initialized";
5539
+ var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
5540
+ var AppServerProcess = class {
5541
+ binary;
5542
+ args;
5543
+ env;
5544
+ cwd;
5545
+ emitter = new EventEmitter2();
5546
+ child = null;
5547
+ client = null;
5548
+ shuttingDown = false;
5549
+ constructor(options = {}) {
5550
+ this.binary = options.binary ?? DEFAULT_CODEX_BINARY;
5551
+ this.args = options.args ?? DEFAULT_CODEX_ARGS;
5552
+ this.env = options.env ?? buildCodexAgentEnv();
5553
+ this.cwd = options.cwd ?? ENGINE_ENV.WORKSPACE_ROOT;
5865
5554
  }
5866
5555
  on(event, listener) {
5867
5556
  this.emitter.on(event, listener);
5868
5557
  }
5869
- off(event, listener) {
5870
- this.emitter.off(event, listener);
5871
- }
5872
- async request(method, params, opts) {
5873
- if (this.disposed) {
5874
- throw new Error(`Cannot send ${method}: ASP client disposed`);
5558
+ async start() {
5559
+ if (this.child && this.client) {
5560
+ return { client: this.client };
5875
5561
  }
5876
- const id = this.nextId;
5877
- this.nextId += 1;
5878
- const promise = new Promise((resolve3, reject) => {
5879
- const timeoutMs = opts?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
5880
- const timer = timeoutMs > 0 ? setTimeout(() => {
5881
- this.pending.delete(id);
5882
- reject(new Error(`ASP request timed out for ${method}`));
5883
- }, timeoutMs) : null;
5884
- this.pending.set(id, { resolve: resolve3, reject, method, timer });
5562
+ this.shuttingDown = false;
5563
+ const child = spawn(this.binary, this.args, {
5564
+ cwd: this.cwd,
5565
+ env: this.env,
5566
+ stdio: ["pipe", "pipe", "pipe"]
5567
+ });
5568
+ this.child = child;
5569
+ child.stderr.setEncoding("utf8");
5570
+ child.stderr.on("data", (chunk) => {
5571
+ for (const line of chunk.toString().split("\n")) {
5572
+ if (line.trim().length > 0) {
5573
+ console.error(`[codex-app-server] ${line}`);
5574
+ }
5575
+ }
5576
+ });
5577
+ child.on("exit", (code, signal) => {
5578
+ this.client?.dispose();
5579
+ this.client = null;
5580
+ this.child = null;
5581
+ if (!this.shuttingDown) {
5582
+ console.warn(`[AppServerProcess] codex app-server exited unexpectedly code=${code ?? "null"} signal=${signal ?? "null"}`);
5583
+ this.emitter.emit("exit", code, signal);
5584
+ }
5585
+ });
5586
+ const client = new AspClient({ stdin: child.stdin, stdout: child.stdout });
5587
+ this.client = client;
5588
+ let cleanupEarlyFailureHandlers = () => {
5589
+ };
5590
+ const earlyFailure = new Promise((_resolve, reject) => {
5591
+ const onError = (error) => {
5592
+ reject(error);
5593
+ };
5594
+ const onExit = (code, signal) => {
5595
+ reject(new Error(`codex app-server exited before initialize completed code=${code ?? "null"} signal=${signal ?? "null"}`));
5596
+ };
5597
+ child.once("error", onError);
5598
+ child.once("exit", onExit);
5599
+ cleanupEarlyFailureHandlers = () => {
5600
+ child.off("error", onError);
5601
+ child.off("exit", onExit);
5602
+ };
5885
5603
  });
5886
- this.write({ method, id, params });
5887
- return promise;
5888
- }
5889
- notify(method, params) {
5890
- if (this.disposed) {
5891
- return;
5892
- }
5893
5604
  try {
5894
- this.write(params === void 0 ? { method } : { method, params });
5605
+ const initializeParams = {
5606
+ clientInfo: {
5607
+ name: "replicas_engine",
5608
+ title: "Replicas Engine",
5609
+ version: ENGINE_PACKAGE_VERSION
5610
+ },
5611
+ capabilities: {
5612
+ experimentalApi: true,
5613
+ requestAttestation: false,
5614
+ optOutNotificationMethods: null
5615
+ }
5616
+ };
5617
+ await Promise.race([
5618
+ client.request(INITIALIZE_METHOD, initializeParams),
5619
+ earlyFailure
5620
+ ]);
5621
+ cleanupEarlyFailureHandlers();
5622
+ client.notify(INITIALIZED_NOTIFICATION);
5623
+ await this.loginWithConfiguredApiKey(client);
5624
+ return { client };
5895
5625
  } catch (error) {
5896
- console.warn(`[AspClient] Failed to send notification ${method}:`, error);
5897
- }
5898
- }
5899
- respond(id, result) {
5900
- if (this.disposed) {
5901
- return;
5902
- }
5903
- try {
5904
- this.write({ id, result });
5905
- } catch (error) {
5906
- console.warn(`[AspClient] Failed to send response ${String(id)}:`, error);
5907
- }
5908
- }
5909
- dispose(reason = new Error("ASP client disposed")) {
5910
- if (this.disposed) {
5911
- return;
5912
- }
5913
- this.disposed = true;
5914
- this.stdout.off("data", this.handleStdoutData);
5915
- this.stdin.removeListener("error", this.handleStdinError);
5916
- for (const [id, pending] of this.pending) {
5917
- if (pending.timer) {
5918
- clearTimeout(pending.timer);
5919
- }
5920
- pending.reject(new Error(`${reason.message} while waiting for ${pending.method}`));
5921
- this.pending.delete(id);
5922
- }
5923
- this.lineBuffer = "";
5924
- this.emitter.emit("dispose", reason);
5925
- this.emitter.removeAllListeners();
5926
- }
5927
- handleStdoutData = (chunk) => {
5928
- this.lineBuffer += chunk.toString();
5929
- let newlineIndex = this.lineBuffer.indexOf("\n");
5930
- while (newlineIndex >= 0) {
5931
- const line = this.lineBuffer.slice(0, newlineIndex).trim();
5932
- this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
5933
- if (line.length > 0) {
5934
- this.handleLine(line);
5935
- }
5936
- newlineIndex = this.lineBuffer.indexOf("\n");
5937
- }
5938
- };
5939
- handleStdinError = (error) => {
5940
- this.dispose(new Error(`ASP stdin error: ${error.message}`));
5941
- };
5942
- handleLine(line) {
5943
- let parsed;
5944
- try {
5945
- parsed = JSON.parse(line);
5946
- } catch (error) {
5947
- console.warn("[AspClient] Failed to parse ASP JSON line:", error);
5948
- return;
5949
- }
5950
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5951
- console.warn("[AspClient] Ignoring non-object ASP message");
5952
- return;
5953
- }
5954
- const message = parsed;
5955
- const hasRequestId = typeof message.id === "number" || typeof message.id === "string";
5956
- if (hasRequestId && (hasOwn(message, "result") || hasOwn(message, "error"))) {
5957
- this.handleResponse(message);
5958
- return;
5959
- }
5960
- if (hasRequestId && typeof message.method === "string") {
5961
- this.emitter.emit("serverRequest", message);
5962
- return;
5963
- }
5964
- if (!hasOwn(message, "id") && typeof message.method === "string") {
5965
- this.emitter.emit("notification", message);
5966
- }
5967
- }
5968
- handleResponse(message) {
5969
- if (typeof message.id !== "number") {
5970
- console.warn("[AspClient] Ignoring response with non-numeric request id");
5971
- return;
5972
- }
5973
- const pending = this.pending.get(message.id);
5974
- if (!pending) {
5975
- console.warn(`[AspClient] Ignoring response for unknown request id ${message.id}`);
5976
- return;
5977
- }
5978
- this.pending.delete(message.id);
5979
- if (pending.timer) {
5980
- clearTimeout(pending.timer);
5981
- }
5982
- if (hasOwn(message, "error")) {
5983
- pending.reject(this.createRpcError(pending.method, message.error));
5984
- return;
5985
- }
5986
- pending.resolve(message.result);
5987
- }
5988
- createRpcError(method, error) {
5989
- if (typeof error !== "object" || error === null || Array.isArray(error)) {
5990
- return new Error(`ASP request failed for ${method}`);
5991
- }
5992
- const rpcError = error;
5993
- const code = typeof rpcError.code === "number" ? ` ${rpcError.code}` : "";
5994
- const message = typeof rpcError.message === "string" ? rpcError.message : "Unknown ASP error";
5995
- const data = hasOwn(rpcError, "data") ? ` data=${JSON.stringify(rpcError.data)}` : "";
5996
- return new Error(`ASP request failed for ${method}:${code} ${message}${data}`);
5997
- }
5998
- write(message) {
5999
- try {
6000
- this.stdin.write(`${JSON.stringify(message)}
6001
- `, (error) => {
6002
- if (error) {
6003
- this.dispose(new Error(`ASP write failed: ${error.message}`));
6004
- }
6005
- });
6006
- } catch (error) {
6007
- const writeError = error instanceof Error ? error : new Error("ASP write failed");
6008
- this.dispose(writeError);
6009
- throw writeError;
6010
- }
6011
- }
6012
- };
6013
-
6014
- // src/managers/codex-asp/app-server-process.ts
6015
- var DEFAULT_CODEX_BINARY = "codex";
6016
- var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
6017
- var ENGINE_PACKAGE_VERSION = "0.1.213";
6018
- var INITIALIZE_METHOD = "initialize";
6019
- var INITIALIZED_NOTIFICATION = "initialized";
6020
- var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
6021
- var AppServerProcess = class {
6022
- binary;
6023
- args;
6024
- env;
6025
- cwd;
6026
- emitter = new EventEmitter2();
6027
- child = null;
6028
- client = null;
6029
- shuttingDown = false;
6030
- constructor(options = {}) {
6031
- this.binary = options.binary ?? DEFAULT_CODEX_BINARY;
6032
- this.args = options.args ?? DEFAULT_CODEX_ARGS;
6033
- this.env = options.env ?? buildCodexAgentEnv();
6034
- this.cwd = options.cwd ?? ENGINE_ENV.WORKSPACE_ROOT;
6035
- }
6036
- on(event, listener) {
6037
- this.emitter.on(event, listener);
6038
- }
6039
- async start() {
6040
- if (this.child && this.client) {
6041
- return { client: this.client };
6042
- }
6043
- this.shuttingDown = false;
6044
- const child = spawn(this.binary, this.args, {
6045
- cwd: this.cwd,
6046
- env: this.env,
6047
- stdio: ["pipe", "pipe", "pipe"]
6048
- });
6049
- this.child = child;
6050
- child.stderr.setEncoding("utf8");
6051
- child.stderr.on("data", (chunk) => {
6052
- for (const line of chunk.toString().split("\n")) {
6053
- if (line.trim().length > 0) {
6054
- console.error(`[codex-app-server] ${line}`);
6055
- }
6056
- }
6057
- });
6058
- child.on("exit", (code, signal) => {
6059
- this.client?.dispose();
6060
- this.client = null;
6061
- this.child = null;
6062
- if (!this.shuttingDown) {
6063
- console.warn(`[AppServerProcess] codex app-server exited unexpectedly code=${code ?? "null"} signal=${signal ?? "null"}`);
6064
- this.emitter.emit("exit", code, signal);
6065
- }
6066
- });
6067
- const client = new AspClient({ stdin: child.stdin, stdout: child.stdout });
6068
- this.client = client;
6069
- let cleanupEarlyFailureHandlers = () => {
6070
- };
6071
- const earlyFailure = new Promise((_resolve, reject) => {
6072
- const onError = (error) => {
6073
- reject(error);
6074
- };
6075
- const onExit = (code, signal) => {
6076
- reject(new Error(`codex app-server exited before initialize completed code=${code ?? "null"} signal=${signal ?? "null"}`));
6077
- };
6078
- child.once("error", onError);
6079
- child.once("exit", onExit);
6080
- cleanupEarlyFailureHandlers = () => {
6081
- child.off("error", onError);
6082
- child.off("exit", onExit);
6083
- };
6084
- });
6085
- try {
6086
- const initializeParams = {
6087
- clientInfo: {
6088
- name: "replicas_engine",
6089
- title: "Replicas Engine",
6090
- version: ENGINE_PACKAGE_VERSION
6091
- },
6092
- capabilities: {
6093
- experimentalApi: true,
6094
- requestAttestation: false,
6095
- optOutNotificationMethods: null
6096
- }
6097
- };
6098
- await Promise.race([
6099
- client.request(INITIALIZE_METHOD, initializeParams),
6100
- earlyFailure
6101
- ]);
6102
- cleanupEarlyFailureHandlers();
6103
- client.notify(INITIALIZED_NOTIFICATION);
6104
- await this.loginWithConfiguredApiKey(client);
6105
- return { client };
6106
- } catch (error) {
6107
- cleanupEarlyFailureHandlers();
6108
- client.dispose();
6109
- await this.killAfterFailedStart();
6110
- throw error;
5626
+ cleanupEarlyFailureHandlers();
5627
+ client.dispose();
5628
+ await this.killAfterFailedStart();
5629
+ throw error;
6111
5630
  }
6112
5631
  }
6113
5632
  async stop() {
@@ -6193,807 +5712,1609 @@ async function restartCodexAspHost() {
6193
5712
  if (process2) {
6194
5713
  await process2.stop();
6195
5714
  }
6196
- })();
6197
- try {
6198
- await restartPromise;
6199
- } finally {
6200
- restartPromise = null;
5715
+ })();
5716
+ try {
5717
+ await restartPromise;
5718
+ } finally {
5719
+ restartPromise = null;
5720
+ }
5721
+ }
5722
+
5723
+ // src/utils/codex-quota.ts
5724
+ function buildCodexRateLimitsSnapshot(fields) {
5725
+ if (fields.unlimited === true) return null;
5726
+ let state = "ok";
5727
+ if (fields.hasCredits === false) {
5728
+ state = "out_of_credits";
5729
+ } else if (fields.rateLimitResetType !== null) {
5730
+ state = "rate_limited";
5731
+ }
5732
+ return {
5733
+ state,
5734
+ balance: fields.balance,
5735
+ rateLimitResetType: fields.rateLimitResetType,
5736
+ planType: fields.planType
5737
+ };
5738
+ }
5739
+ function extractCodexRateLimitsSnapshotFromJsonl(parsed) {
5740
+ if (!isRecord4(parsed)) return null;
5741
+ const payload = isRecord4(parsed.payload) ? parsed.payload : parsed;
5742
+ const rateLimits = isRecord4(payload.rate_limits) ? payload.rate_limits : null;
5743
+ if (!rateLimits) return null;
5744
+ const credits = isRecord4(rateLimits.credits) ? rateLimits.credits : null;
5745
+ const hasCredits = credits && typeof credits.has_credits === "boolean" ? credits.has_credits : null;
5746
+ const unlimited = credits && typeof credits.unlimited === "boolean" ? credits.unlimited : null;
5747
+ const balance = credits && typeof credits.balance === "string" ? credits.balance : null;
5748
+ const rateLimitResetType = typeof rateLimits.rate_limit_reached_type === "string" && rateLimits.rate_limit_reached_type.length > 0 ? rateLimits.rate_limit_reached_type : null;
5749
+ const planType = typeof rateLimits.plan_type === "string" ? rateLimits.plan_type : null;
5750
+ return buildCodexRateLimitsSnapshot({
5751
+ unlimited,
5752
+ hasCredits,
5753
+ balance,
5754
+ rateLimitResetType,
5755
+ planType
5756
+ });
5757
+ }
5758
+ var CodexQuotaStatusTracker = class {
5759
+ lastEmittedQuotaState = "ok";
5760
+ latestQuotaSnapshot = null;
5761
+ quotaBlocked = false;
5762
+ get blocked() {
5763
+ return this.quotaBlocked;
5764
+ }
5765
+ get latestSnapshot() {
5766
+ return this.latestQuotaSnapshot;
5767
+ }
5768
+ prime(snapshot) {
5769
+ this.latestQuotaSnapshot = snapshot;
5770
+ this.quotaBlocked = snapshot.state === "out_of_credits";
5771
+ }
5772
+ apply(snapshot, force = false) {
5773
+ const stateChanged = snapshot.state !== this.lastEmittedQuotaState;
5774
+ if (!stateChanged && !force) {
5775
+ return null;
5776
+ }
5777
+ this.lastEmittedQuotaState = snapshot.state;
5778
+ this.quotaBlocked = snapshot.state === "out_of_credits";
5779
+ this.latestQuotaSnapshot = snapshot;
5780
+ const payload = {
5781
+ state: snapshot.state,
5782
+ balance: snapshot.balance,
5783
+ rateLimitResetType: snapshot.rateLimitResetType,
5784
+ planType: snapshot.planType
5785
+ };
5786
+ const event = {
5787
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5788
+ type: CODEX_QUOTA_STATUS_EVENT_TYPE,
5789
+ payload
5790
+ };
5791
+ return event;
5792
+ }
5793
+ };
5794
+
5795
+ // src/utils/codex-auth.ts
5796
+ function isCodexAuthError(error) {
5797
+ const msg = error instanceof Error ? error.message : String(error);
5798
+ const lower = msg.toLowerCase();
5799
+ return lower.includes("unauthorized") || lower.includes("authentication") || lower.includes("invalid api key") || lower.includes("incorrect api key") || lower.includes("api key") && lower.includes("invalid") || lower.includes("401") || lower.includes('codexerrorinfo="unauthorized"') || lower.includes("codexerrorinfo=unauthorized");
5800
+ }
5801
+
5802
+ // src/managers/codex-asp/mappers.ts
5803
+ var DEFAULT_MODEL = "gpt-5.5";
5804
+ var THREAD_START_METHOD = "thread/start";
5805
+ var THREAD_RESUME_METHOD = "thread/resume";
5806
+ var THREAD_READ_METHOD = "thread/read";
5807
+ var THREAD_GOAL_SET_METHOD = "thread/goal/set";
5808
+ var THREAD_GOAL_GET_METHOD = "thread/goal/get";
5809
+ var THREAD_GOAL_CLEAR_METHOD = "thread/goal/clear";
5810
+ var TURN_START_METHOD = "turn/start";
5811
+ var TURN_INTERRUPT_METHOD = "turn/interrupt";
5812
+ var ACCOUNT_RATE_LIMITS_READ_METHOD = "account/rateLimits/read";
5813
+ function toReasoningEffort(thinkingLevel) {
5814
+ return codexReasoningEffortForThinkingLevel(thinkingLevel);
5815
+ }
5816
+ function extractRateLimitsSnapshot(rateLimits) {
5817
+ const credits = rateLimits.credits;
5818
+ return buildCodexRateLimitsSnapshot({
5819
+ unlimited: credits?.unlimited ?? null,
5820
+ hasCredits: credits?.hasCredits ?? null,
5821
+ balance: credits?.balance ?? null,
5822
+ rateLimitResetType: rateLimits.rateLimitReachedType || null,
5823
+ planType: rateLimits.planType
5824
+ });
5825
+ }
5826
+ function timestampFromSeconds(value) {
5827
+ return new Date((value ?? Date.now() / 1e3) * 1e3).toISOString();
5828
+ }
5829
+ function timestampFromMilliseconds(value) {
5830
+ return new Date(value ?? Date.now()).toISOString();
5831
+ }
5832
+ function stringifyToolOutput(value) {
5833
+ if (value === null || value === void 0) return void 0;
5834
+ if (typeof value === "string") return value;
5835
+ if (typeof value === "object" && "content" in value && Array.isArray(value.content)) {
5836
+ const text = value.content.map((item) => {
5837
+ if (typeof item === "string") return item;
5838
+ if (item && typeof item === "object" && "text" in item && typeof item.text === "string") {
5839
+ return item.text;
5840
+ }
5841
+ return "";
5842
+ }).filter(Boolean).join("\n");
5843
+ if (text) return text;
5844
+ }
5845
+ try {
5846
+ return JSON.stringify(value);
5847
+ } catch {
5848
+ return String(value);
5849
+ }
5850
+ }
5851
+ function transcriptPatchOperation(change) {
5852
+ return {
5853
+ action: change.kind.type,
5854
+ path: change.path,
5855
+ ...change.kind.type === "update" && change.kind.move_path ? { moveTo: change.kind.move_path } : {},
5856
+ ...change.diff.trim().length > 0 ? { diff: change.diff } : {}
5857
+ };
5858
+ }
5859
+ function transcriptItemsForTurn(turn) {
5860
+ const userItems = turn.items.filter((item) => item.type === "userMessage");
5861
+ const otherItems = turn.items.filter((item) => item.type !== "userMessage");
5862
+ return [...userItems, ...otherItems];
5863
+ }
5864
+ function itemToTranscriptItem(item, timestamp, status) {
5865
+ if (item.type === "userMessage") {
5866
+ const content = item.content.filter((input) => input.type === "text").map((input) => input.text).join("\n");
5867
+ return {
5868
+ type: "userMessage",
5869
+ id: item.id,
5870
+ content,
5871
+ timestamp
5872
+ };
5873
+ }
5874
+ if (item.type === "agentMessage") {
5875
+ return {
5876
+ type: "agentMessage",
5877
+ id: item.id,
5878
+ text: item.text,
5879
+ timestamp
5880
+ };
5881
+ }
5882
+ if (item.type === "reasoning") {
5883
+ return {
5884
+ type: "reasoning",
5885
+ id: item.id,
5886
+ text: [...item.summary, ...item.content].filter(Boolean).join("\n").trim(),
5887
+ timestamp,
5888
+ status
5889
+ };
5890
+ }
5891
+ if (item.type === "commandExecution") {
5892
+ const exitCode = item.exitCode ?? null;
5893
+ return {
5894
+ type: "commandExecution",
5895
+ id: item.id,
5896
+ command: item.command,
5897
+ ...item.aggregatedOutput ? { output: item.aggregatedOutput } : {},
5898
+ exitCode,
5899
+ timestamp,
5900
+ status: normalizeCodexAspTranscriptStatus(item.status, typeof exitCode === "number" && exitCode !== 0)
5901
+ };
5902
+ }
5903
+ if (item.type === "fileChange") {
5904
+ return {
5905
+ type: "fileChange",
5906
+ id: item.id,
5907
+ operations: item.changes.map(transcriptPatchOperation),
5908
+ output: item.status,
5909
+ exitCode: item.status === "completed" ? 0 : 1,
5910
+ timestamp,
5911
+ status: normalizeCodexAspTranscriptStatus(item.status)
5912
+ };
5913
+ }
5914
+ if (item.type === "mcpToolCall") {
5915
+ return {
5916
+ type: "toolCall",
5917
+ id: item.id,
5918
+ server: item.server,
5919
+ tool: item.tool,
5920
+ input: item.arguments,
5921
+ output: item.error?.message ?? stringifyToolOutput(item.result),
5922
+ timestamp,
5923
+ status: normalizeCodexAspTranscriptStatus(item.status, item.status === "failed")
5924
+ };
5925
+ }
5926
+ if (item.type === "dynamicToolCall") {
5927
+ return {
5928
+ type: "toolCall",
5929
+ id: item.id,
5930
+ server: item.namespace ?? "dynamic",
5931
+ tool: item.tool,
5932
+ input: item.arguments,
5933
+ output: stringifyToolOutput(item.contentItems),
5934
+ timestamp,
5935
+ status: normalizeCodexAspTranscriptStatus(item.status, item.success === false)
5936
+ };
5937
+ }
5938
+ if (item.type === "collabAgentToolCall") {
5939
+ return {
5940
+ type: "toolCall",
5941
+ id: item.id,
5942
+ server: "collab",
5943
+ tool: item.tool,
5944
+ input: item.prompt ?? void 0,
5945
+ output: stringifyToolOutput(item.agentsStates),
5946
+ timestamp,
5947
+ status: normalizeCodexAspTranscriptStatus(item.status, item.status === "failed")
5948
+ };
5949
+ }
5950
+ if (item.type === "webSearch") {
5951
+ return {
5952
+ type: "webSearch",
5953
+ id: item.id,
5954
+ query: item.query,
5955
+ timestamp,
5956
+ status
5957
+ };
5958
+ }
5959
+ if (item.type === "plan") {
5960
+ return {
5961
+ type: "plan",
5962
+ id: item.id,
5963
+ text: item.text,
5964
+ timestamp,
5965
+ status
5966
+ };
5967
+ }
5968
+ if (item.type === "contextCompaction") {
5969
+ return {
5970
+ type: "contextCompaction",
5971
+ id: item.id,
5972
+ timestamp,
5973
+ status
5974
+ };
5975
+ }
5976
+ if (item.type === "imageView" || item.type === "imageGeneration" || item.type === "hookPrompt") {
5977
+ return null;
5978
+ }
5979
+ if (item.type === "enteredReviewMode" || item.type === "exitedReviewMode") {
5980
+ return {
5981
+ type: "reasoning",
5982
+ id: item.id,
5983
+ text: item.review,
5984
+ timestamp,
5985
+ status
5986
+ };
5987
+ }
5988
+ return null;
5989
+ }
5990
+ function aspGoalToChatGoal(goal) {
5991
+ return {
5992
+ threadId: goal.threadId,
5993
+ objective: goal.objective,
5994
+ status: goal.status,
5995
+ tokenBudget: goal.tokenBudget,
5996
+ tokensUsed: goal.tokensUsed,
5997
+ timeUsedSeconds: goal.timeUsedSeconds,
5998
+ createdAt: timestampFromSeconds(goal.createdAt),
5999
+ updatedAt: timestampFromSeconds(goal.updatedAt)
6000
+ };
6001
+ }
6002
+ function formatTurnFailure(turn) {
6003
+ const turnError = turn.error;
6004
+ if (!turnError) {
6005
+ return "Codex ASP turn failed without error details";
6006
+ }
6007
+ const parts = [`Codex ASP turn failed: ${turnError.message}`];
6008
+ if (turnError.codexErrorInfo !== null) {
6009
+ parts.push(`codexErrorInfo=${JSON.stringify(turnError.codexErrorInfo)}`);
6010
+ }
6011
+ if (turnError.additionalDetails) {
6012
+ parts.push(`details=${turnError.additionalDetails}`);
6013
+ }
6014
+ return parts.join(" ");
6015
+ }
6016
+ function threadToAspTranscript(thread) {
6017
+ let sequence = 0;
6018
+ const turns = thread.turns.map((turn, index) => ({ turn, index })).sort((a, b) => (a.turn.startedAt ?? a.turn.completedAt ?? Number.MAX_SAFE_INTEGER) - (b.turn.startedAt ?? b.turn.completedAt ?? Number.MAX_SAFE_INTEGER) || a.index - b.index).map(({ turn }) => {
6019
+ const startedAt = timestampFromSeconds(turn.startedAt);
6020
+ const completedAt = turn.completedAt === null ? null : timestampFromSeconds(turn.completedAt);
6021
+ const itemTimestamp = completedAt ?? startedAt;
6022
+ const status = normalizeCodexAspTranscriptStatus(turn.status);
6023
+ const items = [];
6024
+ for (const item of transcriptItemsForTurn(turn)) {
6025
+ const transcriptItem = itemToTranscriptItem(item, item.type === "userMessage" ? startedAt : itemTimestamp, status);
6026
+ if (transcriptItem) {
6027
+ items.push({ ...transcriptItem, sequence: sequence++ });
6028
+ }
6029
+ }
6030
+ if (turn.error) {
6031
+ items.push({
6032
+ type: "error",
6033
+ id: `${turn.id}-error`,
6034
+ message: formatTurnFailure(turn),
6035
+ timestamp: itemTimestamp,
6036
+ sequence: sequence++
6037
+ });
6038
+ }
6039
+ return {
6040
+ id: turn.id,
6041
+ status: turn.status,
6042
+ startedAt,
6043
+ completedAt,
6044
+ items
6045
+ };
6046
+ });
6047
+ return {
6048
+ threadId: thread.id,
6049
+ updatedAt: timestampFromSeconds(thread.updatedAt),
6050
+ turns
6051
+ };
6052
+ }
6053
+ function latestIsoTimestamp(a, b) {
6054
+ return Date.parse(a) >= Date.parse(b) ? a : b;
6055
+ }
6056
+ function nextTranscriptSequence(transcript) {
6057
+ let max = -1;
6058
+ for (const turn of transcript.turns) {
6059
+ for (const item of turn.items) {
6060
+ if (typeof item.sequence === "number" && item.sequence > max) {
6061
+ max = item.sequence;
6062
+ }
6063
+ }
6064
+ }
6065
+ return max + 1;
6066
+ }
6067
+ function transcriptItemRank(item) {
6068
+ return item.type === "userMessage" ? 0 : 1;
6069
+ }
6070
+ function transcriptItemSignature(item) {
6071
+ if (item.type === "userMessage") return `user:${item.content.trim()}`;
6072
+ if (item.type === "agentMessage" && item.text.trim()) return `agent:${item.text.trim()}`;
6073
+ if (item.type === "plan" && item.text.trim()) return `plan:${item.text.trim()}`;
6074
+ if (item.type === "error" && item.message.trim()) return `error:${item.message.trim()}`;
6075
+ return null;
6076
+ }
6077
+ function areEquivalentTranscriptItems(current, candidate) {
6078
+ if (current.id === candidate.id) return true;
6079
+ if (current.type !== candidate.type) return false;
6080
+ const signature = transcriptItemSignature(current);
6081
+ return signature !== null && signature === transcriptItemSignature(candidate);
6082
+ }
6083
+ function findEquivalentCodexAspTranscriptItemIndex(items, candidate) {
6084
+ return items.findIndex((current) => areEquivalentTranscriptItems(current, candidate));
6085
+ }
6086
+ function sortTranscriptItems(items) {
6087
+ return [...items].map((item, index) => ({ item, index })).sort((a, b) => {
6088
+ const aSequence = a.item.sequence ?? Number.MAX_SAFE_INTEGER;
6089
+ const bSequence = b.item.sequence ?? Number.MAX_SAFE_INTEGER;
6090
+ return transcriptItemRank(a.item) - transcriptItemRank(b.item) || aSequence - bSequence || Date.parse(a.item.timestamp) - Date.parse(b.item.timestamp) || a.index - b.index;
6091
+ }).map(({ item }) => item);
6092
+ }
6093
+ function dedupeTranscriptItems(items) {
6094
+ const deduped = [];
6095
+ for (const item of items) {
6096
+ const existingIndex = findEquivalentCodexAspTranscriptItemIndex(deduped, item);
6097
+ if (existingIndex === -1) {
6098
+ deduped.push(item);
6099
+ continue;
6100
+ }
6101
+ deduped[existingIndex] = mergeTranscriptItem(deduped[existingIndex], item);
6102
+ }
6103
+ return deduped;
6104
+ }
6105
+ function normalizeCodexAspTranscriptSequences(transcript) {
6106
+ let sequence = 0;
6107
+ const turns = [...transcript.turns].map((turn, index) => ({ turn, index })).sort((a, b) => Date.parse(a.turn.startedAt) - Date.parse(b.turn.startedAt) || a.index - b.index).map(({ turn }) => ({
6108
+ ...turn,
6109
+ items: sortTranscriptItems(dedupeTranscriptItems(turn.items)).map((item) => ({
6110
+ ...item,
6111
+ sequence: sequence++
6112
+ }))
6113
+ }));
6114
+ return {
6115
+ ...transcript,
6116
+ turns
6117
+ };
6118
+ }
6119
+ function mergeCodexAspTranscriptItem(current, candidate) {
6120
+ if (current.type === "agentMessage" && candidate.type === "agentMessage" && candidate.text.length === 0) {
6121
+ return {
6122
+ ...current,
6123
+ sequence: current.sequence ?? candidate.sequence
6124
+ };
6125
+ }
6126
+ return {
6127
+ ...current,
6128
+ ...candidate,
6129
+ id: current.id,
6130
+ timestamp: current.timestamp,
6131
+ sequence: current.sequence ?? candidate.sequence
6132
+ };
6133
+ }
6134
+ function mergeCodexAspTranscripts(primary, supplemental) {
6135
+ if (!primary) return supplemental ? normalizeCodexAspTranscriptSequences(supplemental) : null;
6136
+ if (!supplemental) return normalizeCodexAspTranscriptSequences(primary);
6137
+ if (primary.threadId !== supplemental.threadId) return normalizeCodexAspTranscriptSequences(supplemental);
6138
+ let sequence = nextTranscriptSequence(primary);
6139
+ const turns = primary.turns.map((turn) => ({
6140
+ ...turn,
6141
+ items: [...turn.items]
6142
+ }));
6143
+ for (const supplementalTurn of supplemental.turns) {
6144
+ const existingTurn = turns.find((turn) => turn.id === supplementalTurn.id);
6145
+ if (!existingTurn) {
6146
+ turns.push({
6147
+ ...supplementalTurn,
6148
+ items: supplementalTurn.items.map((item) => typeof item.sequence === "number" ? item : { ...item, sequence: sequence++ })
6149
+ });
6150
+ continue;
6151
+ }
6152
+ existingTurn.status = supplementalTurn.status;
6153
+ existingTurn.completedAt = supplementalTurn.completedAt ?? existingTurn.completedAt;
6154
+ existingTurn.startedAt = Date.parse(existingTurn.startedAt) <= Date.parse(supplementalTurn.startedAt) ? existingTurn.startedAt : supplementalTurn.startedAt;
6155
+ for (const item of supplementalTurn.items) {
6156
+ const existingIndex = findEquivalentCodexAspTranscriptItemIndex(existingTurn.items, item);
6157
+ if (existingIndex === -1) {
6158
+ existingTurn.items.push(
6159
+ typeof item.sequence === "number" ? item : { ...item, sequence: sequence++ }
6160
+ );
6161
+ continue;
6162
+ }
6163
+ existingTurn.items[existingIndex] = mergeCodexAspTranscriptItem(existingTurn.items[existingIndex], item);
6164
+ }
6165
+ existingTurn.items = sortTranscriptItems(existingTurn.items);
6166
+ }
6167
+ return normalizeCodexAspTranscriptSequences({
6168
+ threadId: primary.threadId,
6169
+ updatedAt: latestIsoTimestamp(primary.updatedAt, supplemental.updatedAt),
6170
+ turns: turns.sort((a, b) => Date.parse(a.startedAt) - Date.parse(b.startedAt))
6171
+ });
6172
+ }
6173
+ async function buildThreadStartParams(workingDirectory, request, developerInstructions) {
6174
+ const additionalDirectories = await getAgentAdditionalDirectories();
6175
+ return {
6176
+ model: request.model ?? DEFAULT_MODEL,
6177
+ cwd: workingDirectory,
6178
+ runtimeWorkspaceRoots: additionalDirectories,
6179
+ sandbox: "danger-full-access",
6180
+ developerInstructions: developerInstructions ?? null,
6181
+ config: { web_search: "live" },
6182
+ experimentalRawEvents: false,
6183
+ persistExtendedHistory: false
6184
+ };
6185
+ }
6186
+ async function buildThreadResumeParams(workingDirectory, threadId, request, developerInstructions) {
6187
+ const additionalDirectories = await getAgentAdditionalDirectories();
6188
+ return {
6189
+ threadId,
6190
+ model: request.model ?? DEFAULT_MODEL,
6191
+ cwd: workingDirectory,
6192
+ runtimeWorkspaceRoots: additionalDirectories,
6193
+ sandbox: "danger-full-access",
6194
+ developerInstructions: developerInstructions ?? null,
6195
+ config: { web_search: "live" },
6196
+ excludeTurns: false,
6197
+ persistExtendedHistory: false
6198
+ };
6199
+ }
6200
+ async function buildTurnInput(request) {
6201
+ const input = [{
6202
+ type: "text",
6203
+ text: request.message,
6204
+ text_elements: []
6205
+ }];
6206
+ if (!request.images || request.images.length === 0) {
6207
+ return { input, tempImagePaths: [] };
6208
+ }
6209
+ const normalizedImages = await normalizeImages(request.images);
6210
+ const tempImagePaths = await saveNormalizedImagesToTempFiles(normalizedImages);
6211
+ input.push(...tempImagePaths.map((path4) => ({
6212
+ type: "localImage",
6213
+ path: path4
6214
+ })));
6215
+ return { input, tempImagePaths };
6216
+ }
6217
+ async function buildTurnStartParams(threadId, request, developerInstructions) {
6218
+ const effort = toReasoningEffort(request.thinkingLevel);
6219
+ const model = request.model ?? DEFAULT_MODEL;
6220
+ const { input, tempImagePaths } = await buildTurnInput(request);
6221
+ return {
6222
+ params: {
6223
+ threadId,
6224
+ input,
6225
+ model,
6226
+ ...effort ? { effort } : {},
6227
+ ...developerInstructions ? {
6228
+ collaborationMode: {
6229
+ mode: "default",
6230
+ settings: {
6231
+ model,
6232
+ reasoning_effort: effort ?? null,
6233
+ developer_instructions: developerInstructions
6234
+ }
6235
+ }
6236
+ } : {}
6237
+ },
6238
+ tempImagePaths
6239
+ };
6240
+ }
6241
+
6242
+ // src/managers/codex-asp/notification-dispatch.ts
6243
+ var TURN_STARTED_METHOD = "turn/started";
6244
+ var TURN_COMPLETED_METHOD = "turn/completed";
6245
+ var TURN_PLAN_UPDATED_METHOD = "turn/plan/updated";
6246
+ var THREAD_GOAL_UPDATED_METHOD = "thread/goal/updated";
6247
+ var THREAD_GOAL_CLEARED_METHOD = "thread/goal/cleared";
6248
+ var ITEM_STARTED_METHOD = "item/started";
6249
+ var ITEM_COMPLETED_METHOD = "item/completed";
6250
+ var AGENT_MESSAGE_DELTA_METHOD = "item/agentMessage/delta";
6251
+ var COMMAND_EXECUTION_OUTPUT_DELTA_METHOD = "item/commandExecution/outputDelta";
6252
+ var FILE_CHANGE_OUTPUT_DELTA_METHOD = "item/fileChange/outputDelta";
6253
+ var ACCOUNT_RATE_LIMITS_UPDATED_METHOD = "account/rateLimits/updated";
6254
+ var THREAD_TOKEN_USAGE_UPDATED_METHOD = "thread/tokenUsage/updated";
6255
+ var THREAD_COMPACTED_METHOD = "thread/compacted";
6256
+ var COMMAND_APPROVAL_METHOD = "item/commandExecution/requestApproval";
6257
+ var FILE_CHANGE_APPROVAL_METHOD = "item/fileChange/requestApproval";
6258
+ function dispatchAspNotification(notification, handlers) {
6259
+ const handler = handlers[notification.method];
6260
+ if (!handler) return;
6261
+ handler(notification);
6262
+ }
6263
+
6264
+ // src/managers/codex-asp/codex-asp-manager.ts
6265
+ var CodexAspManager = class extends CodingAgentManager {
6266
+ currentThreadId = null;
6267
+ activeTurnId = null;
6268
+ threadAttached = false;
6269
+ historyEvents = [];
6270
+ codexAspTranscript = null;
6271
+ codexAspSequence = 0;
6272
+ quotaStatus = new CodexQuotaStatusTracker();
6273
+ currentGoal = null;
6274
+ constructor(options) {
6275
+ super(options);
6276
+ this.initializeManager(this.processMessageInternal.bind(this));
6277
+ }
6278
+ async initialize() {
6279
+ if (this.initialSessionId) {
6280
+ this.currentThreadId = this.initialSessionId;
6281
+ }
6282
+ }
6283
+ async interruptActiveTurn() {
6284
+ if (!this.currentThreadId || !this.activeTurnId) {
6285
+ return;
6286
+ }
6287
+ try {
6288
+ const host = await getCodexAspHost();
6289
+ const params = {
6290
+ threadId: this.currentThreadId,
6291
+ turnId: this.activeTurnId
6292
+ };
6293
+ await host.client.request(TURN_INTERRUPT_METHOD, params);
6294
+ } catch (error) {
6295
+ console.warn("[CodexAspManager] Failed to interrupt active turn:", error);
6296
+ }
6297
+ }
6298
+ async getHistory() {
6299
+ if (!this.currentThreadId) {
6300
+ return { thread_id: null, events: [], goal: null };
6301
+ }
6302
+ if (this.activeTurnId && this.codexAspTranscript?.threadId === this.currentThreadId) {
6303
+ try {
6304
+ const host = await getCodexAspHost();
6305
+ await this.refreshThreadGoal(host, this.currentThreadId);
6306
+ } catch {
6307
+ }
6308
+ return {
6309
+ thread_id: this.currentThreadId,
6310
+ events: [...this.historyEvents],
6311
+ codexAspTranscript: this.codexAspTranscript,
6312
+ goal: this.currentGoal
6313
+ };
6314
+ }
6315
+ try {
6316
+ const host = await getCodexAspHost();
6317
+ const [response] = await Promise.all([
6318
+ host.client.request(
6319
+ THREAD_READ_METHOD,
6320
+ { threadId: this.currentThreadId, includeTurns: true }
6321
+ ),
6322
+ this.refreshThreadGoal(host, this.currentThreadId)
6323
+ ]);
6324
+ const transcript = this.mergeTranscriptSnapshot(threadToAspTranscript(response.thread));
6325
+ if (transcript) {
6326
+ transcript.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
6327
+ }
6328
+ return {
6329
+ thread_id: this.currentThreadId,
6330
+ events: [...this.historyEvents],
6331
+ codexAspTranscript: transcript,
6332
+ goal: this.currentGoal
6333
+ };
6334
+ } catch {
6335
+ }
6336
+ return {
6337
+ thread_id: this.currentThreadId,
6338
+ events: [...this.historyEvents],
6339
+ codexAspTranscript: this.codexAspTranscript,
6340
+ goal: this.currentGoal
6341
+ };
6342
+ }
6343
+ getGoal() {
6344
+ return this.currentGoal;
6345
+ }
6346
+ async clearGoal() {
6347
+ await this.initialized;
6348
+ if (!this.currentThreadId) {
6349
+ this.recordGoalChange(null, true);
6350
+ return null;
6351
+ }
6352
+ const host = await getCodexAspHost();
6353
+ await host.client.request(
6354
+ THREAD_GOAL_CLEAR_METHOD,
6355
+ { threadId: this.currentThreadId }
6356
+ );
6357
+ this.recordGoalChange(null, true);
6358
+ return null;
6359
+ }
6360
+ async processMessageInternal(request) {
6361
+ let userMessageRecorded = false;
6362
+ const recordUserMessage = (extraPayload = {}) => {
6363
+ if (userMessageRecorded) return;
6364
+ userMessageRecorded = true;
6365
+ this.recordHistoryEvent("event_msg", {
6366
+ type: "user_message",
6367
+ message: request.message,
6368
+ ...extraPayload
6369
+ });
6370
+ };
6371
+ const goalCommand = parseGoalCommand(request.message);
6372
+ const dispatch = async () => {
6373
+ if (goalCommand?.type === "clear") {
6374
+ await this.executeGoalClearCommand(request, recordUserMessage);
6375
+ return;
6376
+ }
6377
+ if (goalCommand?.type === "set") {
6378
+ await this.executeAspTurn(request, recordUserMessage, {
6379
+ runTurn: (host, threadId) => this.runGoalTurn(host, threadId, request, goalCommand.objective),
6380
+ userMessagePayload: { command: "goal" }
6381
+ });
6382
+ return;
6383
+ }
6384
+ await this.executeAspTurn(request, recordUserMessage);
6385
+ };
6386
+ try {
6387
+ await dispatch();
6388
+ } catch (error) {
6389
+ if (isCodexAuthError(error)) {
6390
+ const refreshed = await codexTokenManager.fetchFreshCredentials(error instanceof Error ? error.message : String(error));
6391
+ if (refreshed) {
6392
+ await restartCodexAspHost();
6393
+ this.threadAttached = false;
6394
+ await dispatch();
6395
+ return;
6396
+ }
6397
+ }
6398
+ throw error;
6399
+ } finally {
6400
+ this.activeTurnId = null;
6401
+ await this.onTurnComplete();
6402
+ }
6403
+ }
6404
+ async executeGoalClearCommand(request, recordUserMessage) {
6405
+ const host = await getCodexAspHost();
6406
+ const developerInstructions = this.buildCombinedInstructions(request.customInstructions);
6407
+ recordUserMessage({ command: "goal" });
6408
+ const threadId = await this.ensureThread(host, request, developerInstructions);
6409
+ await host.client.request(
6410
+ THREAD_GOAL_CLEAR_METHOD,
6411
+ { threadId }
6412
+ );
6413
+ this.recordGoalChange(null, true);
6414
+ }
6415
+ async executeAspTurn(request, recordUserMessage, options = {}) {
6416
+ const host = await getCodexAspHost();
6417
+ if (this.quotaStatus.blocked && this.quotaStatus.latestSnapshot) {
6418
+ await this.refreshQuotaSnapshot(host);
6419
+ if (this.quotaStatus.blocked && this.quotaStatus.latestSnapshot) {
6420
+ recordUserMessage(options.userMessagePayload);
6421
+ this.emitQuotaStatus(this.quotaStatus.latestSnapshot, true);
6422
+ return;
6423
+ }
6424
+ }
6425
+ const developerInstructions = this.buildCombinedInstructions(request.customInstructions);
6426
+ recordUserMessage(options.userMessagePayload);
6427
+ const threadId = await this.ensureThread(host, request, developerInstructions);
6428
+ const runTurn = options.runTurn ?? ((aspHost, aspThreadId, aspInstructions) => this.runTurn(aspHost, aspThreadId, request, aspInstructions));
6429
+ let completedTurn;
6430
+ try {
6431
+ completedTurn = await runTurn(host, threadId, developerInstructions);
6432
+ } catch (error) {
6433
+ await this.refreshQuotaSnapshot(host);
6434
+ if (this.quotaStatus.blocked) {
6435
+ return;
6436
+ }
6437
+ throw error;
6438
+ }
6439
+ if (completedTurn.status === "failed") {
6440
+ await this.refreshQuotaSnapshot(host);
6441
+ if (this.quotaStatus.blocked) {
6442
+ return;
6443
+ }
6444
+ throw new Error(formatTurnFailure(completedTurn));
6445
+ }
6446
+ }
6447
+ async ensureThread(host, request, developerInstructions) {
6448
+ const existingThreadId = this.currentThreadId;
6449
+ if (existingThreadId) {
6450
+ if (!this.threadAttached) {
6451
+ const response = await host.client.request(
6452
+ THREAD_RESUME_METHOD,
6453
+ await buildThreadResumeParams(this.workingDirectory, existingThreadId, request, developerInstructions)
6454
+ );
6455
+ this.currentThreadId = response.thread.id;
6456
+ this.threadAttached = true;
6457
+ this.seedHistoryFromThread(response.thread);
6458
+ await this.onSaveSessionId(this.currentThreadId);
6459
+ }
6460
+ return this.currentThreadId ?? existingThreadId;
6461
+ }
6462
+ const threadStartResponse = await host.client.request(
6463
+ THREAD_START_METHOD,
6464
+ await buildThreadStartParams(this.workingDirectory, request, developerInstructions)
6465
+ );
6466
+ const threadId = threadStartResponse.thread.id;
6467
+ this.currentThreadId = threadId;
6468
+ this.threadAttached = true;
6469
+ await this.onSaveSessionId(this.currentThreadId);
6470
+ return threadId;
6471
+ }
6472
+ async runTurn(host, threadId, request, developerInstructions) {
6473
+ const { params, tempImagePaths } = await buildTurnStartParams(threadId, request, developerInstructions);
6474
+ return this.observeTurn(host, threadId, request, async () => {
6475
+ const turnStartResponse = await host.client.request(
6476
+ TURN_START_METHOD,
6477
+ params
6478
+ );
6479
+ return { turn: turnStartResponse.turn, tempImagePaths };
6480
+ });
6481
+ }
6482
+ async runGoalTurn(host, threadId, request, objective) {
6483
+ return this.observeTurn(host, threadId, request, async () => {
6484
+ const response = await host.client.request(
6485
+ THREAD_GOAL_SET_METHOD,
6486
+ { threadId, objective, status: "active" }
6487
+ );
6488
+ this.recordGoalChange(response.goal, true);
6489
+ return { turn: null, tempImagePaths: [] };
6490
+ });
6491
+ }
6492
+ async observeTurn(host, threadId, request, startTurn) {
6493
+ let resolveCompleted;
6494
+ const completed = new Promise((resolve3) => {
6495
+ resolveCompleted = resolve3;
6496
+ });
6497
+ let rejectDisposed = () => {
6498
+ };
6499
+ const disposed = new Promise((_resolve, reject) => {
6500
+ rejectDisposed = reject;
6501
+ });
6502
+ void disposed.catch(() => {
6503
+ });
6504
+ let observedTurnId = null;
6505
+ const completedItems = [];
6506
+ const agentMessageDeltas = /* @__PURE__ */ new Map();
6507
+ const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
6508
+ const model = request.model ?? DEFAULT_MODEL;
6509
+ let tempImagePaths = [];
6510
+ const linearForwarder = new LinearEventForwarder(linearSessionId);
6511
+ const matchesTurn = (notificationThreadId, notificationTurnId) => notificationThreadId === threadId && (!observedTurnId || notificationTurnId === null || notificationTurnId === observedTurnId);
6512
+ const handlers = {
6513
+ [ACCOUNT_RATE_LIMITS_UPDATED_METHOD]: (notification) => {
6514
+ this.handleRateLimits(notification.params.rateLimits);
6515
+ },
6516
+ [THREAD_TOKEN_USAGE_UPDATED_METHOD]: (notification) => {
6517
+ if (notification.params.threadId !== threadId) return;
6518
+ this.emitCodexTokenUsage(notification.params.tokenUsage, model);
6519
+ },
6520
+ [THREAD_GOAL_UPDATED_METHOD]: (notification) => {
6521
+ if (notification.params.threadId !== threadId) return;
6522
+ this.recordGoalChange(notification.params.goal);
6523
+ },
6524
+ [THREAD_GOAL_CLEARED_METHOD]: (notification) => {
6525
+ if (notification.params.threadId !== threadId) return;
6526
+ this.recordGoalChange(null);
6527
+ },
6528
+ [THREAD_COMPACTED_METHOD]: (notification) => {
6529
+ if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
6530
+ this.setCompacting(false);
6531
+ },
6532
+ [TURN_STARTED_METHOD]: (notification) => {
6533
+ if (notification.params.threadId !== threadId) return;
6534
+ observedTurnId = notification.params.turn.id;
6535
+ this.activeTurnId = notification.params.turn.id;
6536
+ this.mergeTranscriptTurn(notification.params.threadId, notification.params.turn);
6537
+ this.emitTranscriptUpdated(notification.params.threadId);
6538
+ linearForwarder.sendEvent(convertCodexAspNotification(notification, linearSessionId ?? ""));
6539
+ },
6540
+ [TURN_PLAN_UPDATED_METHOD]: (notification) => {
6541
+ if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
6542
+ this.emitTranscriptUpdated(notification.params.threadId);
6543
+ linearForwarder.sendPlan(extractPlanFromCodexAspNotification(notification));
6544
+ },
6545
+ [ITEM_STARTED_METHOD]: (notification) => {
6546
+ if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
6547
+ if (notification.params.item.type === "contextCompaction") {
6548
+ this.setCompacting(true);
6549
+ }
6550
+ this.upsertTranscriptItem(
6551
+ notification.params.threadId,
6552
+ notification.params.turnId,
6553
+ notification.params.item,
6554
+ timestampFromMilliseconds(notification.params.startedAtMs),
6555
+ "in_progress",
6556
+ "started"
6557
+ );
6558
+ this.emitTranscriptUpdated(notification.params.threadId);
6559
+ linearForwarder.sendEvent(convertCodexAspNotification(notification, linearSessionId ?? ""));
6560
+ },
6561
+ [ITEM_COMPLETED_METHOD]: (notification) => {
6562
+ if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
6563
+ completedItems.push(notification.params.item);
6564
+ if (notification.params.item.type === "contextCompaction") {
6565
+ this.setCompacting(false);
6566
+ }
6567
+ const itemFailed = notification.params.item.type === "commandExecution" && typeof notification.params.item.exitCode === "number" && notification.params.item.exitCode !== 0;
6568
+ this.upsertTranscriptItem(
6569
+ notification.params.threadId,
6570
+ notification.params.turnId,
6571
+ notification.params.item,
6572
+ timestampFromMilliseconds(notification.params.completedAtMs),
6573
+ itemFailed ? "failed" : "completed",
6574
+ "completed"
6575
+ );
6576
+ this.emitTranscriptUpdated(notification.params.threadId);
6577
+ linearForwarder.sendEvent(convertCodexAspNotification(notification, linearSessionId ?? ""));
6578
+ },
6579
+ [AGENT_MESSAGE_DELTA_METHOD]: (notification) => {
6580
+ if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
6581
+ const currentText = agentMessageDeltas.get(notification.params.itemId) ?? "";
6582
+ agentMessageDeltas.set(notification.params.itemId, currentText + notification.params.delta);
6583
+ this.appendAgentMessageDelta(
6584
+ notification.params.threadId,
6585
+ notification.params.turnId,
6586
+ notification.params.itemId,
6587
+ notification.params.delta
6588
+ );
6589
+ this.emitTranscriptUpdated(notification.params.threadId);
6590
+ },
6591
+ [COMMAND_EXECUTION_OUTPUT_DELTA_METHOD]: (notification) => {
6592
+ if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
6593
+ this.appendTranscriptOutput(notification.params.turnId, notification.params.itemId, notification.params.delta);
6594
+ this.emitTranscriptUpdated(notification.params.threadId);
6595
+ },
6596
+ [FILE_CHANGE_OUTPUT_DELTA_METHOD]: (notification) => {
6597
+ if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
6598
+ this.appendTranscriptOutput(notification.params.turnId, notification.params.itemId, notification.params.delta);
6599
+ this.emitTranscriptUpdated(notification.params.threadId);
6600
+ },
6601
+ [TURN_COMPLETED_METHOD]: (notification) => {
6602
+ if (notification.params.threadId !== threadId) return;
6603
+ observedTurnId = notification.params.turn.id;
6604
+ const turn = notification.params.turn;
6605
+ const items = turn.items.length > 0 ? [...turn.items] : [];
6606
+ const itemIds = new Set(items.map((item) => item.id));
6607
+ for (const item of completedItems) {
6608
+ if (!itemIds.has(item.id)) {
6609
+ items.push(item);
6610
+ itemIds.add(item.id);
6611
+ }
6612
+ }
6613
+ for (const [itemId, text] of agentMessageDeltas) {
6614
+ if (!itemIds.has(itemId)) {
6615
+ items.push({
6616
+ type: "agentMessage",
6617
+ id: itemId,
6618
+ text,
6619
+ phase: null,
6620
+ memoryCitation: null
6621
+ });
6622
+ }
6623
+ }
6624
+ const completedTurn = items.length > 0 ? { ...turn, items, itemsView: "full" } : turn;
6625
+ this.mergeTranscriptTurn(notification.params.threadId, completedTurn);
6626
+ this.emitTranscriptUpdated(notification.params.threadId);
6627
+ resolveCompleted(completedTurn);
6628
+ }
6629
+ };
6630
+ const onNotification = (notification) => {
6631
+ dispatchAspNotification(notification, handlers);
6632
+ };
6633
+ const onServerRequest = (serverRequest) => {
6634
+ if (serverRequest.method !== COMMAND_APPROVAL_METHOD && serverRequest.method !== FILE_CHANGE_APPROVAL_METHOD) {
6635
+ return;
6636
+ }
6637
+ console.warn("[CodexAspManager] approval requested while sandbox is danger-full-access");
6638
+ host.client.respond(serverRequest.id, { decision: "accept" });
6639
+ };
6640
+ const onDispose = (reason) => {
6641
+ this.threadAttached = false;
6642
+ this.activeTurnId = null;
6643
+ const turnLabel = observedTurnId ? ` ${observedTurnId}` : "";
6644
+ rejectDisposed(new Error(`Codex ASP client disposed before turn${turnLabel} completed: ${reason.message}`));
6645
+ };
6646
+ host.client.on("notification", onNotification);
6647
+ host.client.on("serverRequest", onServerRequest);
6648
+ host.client.on("dispose", onDispose);
6649
+ try {
6650
+ const started = await startTurn();
6651
+ tempImagePaths = started.tempImagePaths;
6652
+ if (started.turn) {
6653
+ observedTurnId = started.turn.id;
6654
+ this.activeTurnId = started.turn.id;
6655
+ }
6656
+ const turn = await Promise.race([completed, disposed]);
6657
+ linearForwarder.flushThoughtAsResponse();
6658
+ return turn;
6659
+ } finally {
6660
+ host.client.off("notification", onNotification);
6661
+ host.client.off("serverRequest", onServerRequest);
6662
+ host.client.off("dispose", onDispose);
6663
+ await removeTempImageFiles(tempImagePaths);
6664
+ if (host.client.isDisposed) {
6665
+ this.threadAttached = false;
6666
+ }
6667
+ }
6668
+ }
6669
+ nextTranscriptSequence() {
6670
+ return this.codexAspSequence++;
6671
+ }
6672
+ syncTranscriptSequence(transcript) {
6673
+ if (!transcript) return;
6674
+ let next = 0;
6675
+ for (const turn of transcript.turns) {
6676
+ for (const item of turn.items) {
6677
+ if (typeof item.sequence === "number" && item.sequence >= next) {
6678
+ next = item.sequence + 1;
6679
+ }
6680
+ }
6681
+ }
6682
+ this.codexAspSequence = next;
6201
6683
  }
6202
- }
6203
-
6204
- // src/managers/codex-asp/mappers.ts
6205
- var DEFAULT_MODEL2 = "gpt-5.5";
6206
- var THREAD_START_METHOD = "thread/start";
6207
- var THREAD_RESUME_METHOD = "thread/resume";
6208
- var THREAD_READ_METHOD = "thread/read";
6209
- var THREAD_GOAL_SET_METHOD = "thread/goal/set";
6210
- var THREAD_GOAL_GET_METHOD = "thread/goal/get";
6211
- var THREAD_GOAL_CLEAR_METHOD = "thread/goal/clear";
6212
- var TURN_START_METHOD = "turn/start";
6213
- var TURN_INTERRUPT_METHOD = "turn/interrupt";
6214
- var ACCOUNT_RATE_LIMITS_READ_METHOD = "account/rateLimits/read";
6215
- function toReasoningEffort(thinkingLevel) {
6216
- return codexReasoningEffortForThinkingLevel(thinkingLevel);
6217
- }
6218
- function extractRateLimitsSnapshot(rateLimits) {
6219
- const credits = rateLimits.credits;
6220
- return buildCodexRateLimitsSnapshot({
6221
- unlimited: credits?.unlimited ?? null,
6222
- hasCredits: credits?.hasCredits ?? null,
6223
- balance: credits?.balance ?? null,
6224
- rateLimitResetType: rateLimits.rateLimitReachedType || null,
6225
- planType: rateLimits.planType
6226
- });
6227
- }
6228
- function timestampFromSeconds(value) {
6229
- return new Date((value ?? Date.now() / 1e3) * 1e3).toISOString();
6230
- }
6231
- function aspGoalToChatGoal(goal) {
6232
- return {
6233
- threadId: goal.threadId,
6234
- objective: goal.objective,
6235
- status: goal.status,
6236
- tokenBudget: goal.tokenBudget,
6237
- tokensUsed: goal.tokensUsed,
6238
- timeUsedSeconds: goal.timeUsedSeconds,
6239
- createdAt: timestampFromSeconds(goal.createdAt),
6240
- updatedAt: timestampFromSeconds(goal.updatedAt)
6241
- };
6242
- }
6243
- function formatTurnFailure(turn) {
6244
- const turnError = turn.error;
6245
- if (!turnError) {
6246
- return "Codex ASP turn failed without error details";
6684
+ mergeTranscriptSnapshot(snapshot) {
6685
+ this.codexAspTranscript = mergeCodexAspTranscripts(this.codexAspTranscript, snapshot);
6686
+ this.syncTranscriptSequence(this.codexAspTranscript);
6687
+ return this.codexAspTranscript;
6247
6688
  }
6248
- const parts = [`Codex ASP turn failed: ${turnError.message}`];
6249
- if (turnError.codexErrorInfo !== null) {
6250
- parts.push(`codexErrorInfo=${JSON.stringify(turnError.codexErrorInfo)}`);
6689
+ ensureTranscript(threadId) {
6690
+ if (this.codexAspTranscript?.threadId === threadId) {
6691
+ return this.codexAspTranscript;
6692
+ }
6693
+ this.codexAspTranscript = {
6694
+ threadId,
6695
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6696
+ turns: []
6697
+ };
6698
+ this.codexAspSequence = 0;
6699
+ return this.codexAspTranscript;
6700
+ }
6701
+ ensureTranscriptTurn(threadId, turnId, startedAt) {
6702
+ const transcript = this.ensureTranscript(threadId);
6703
+ let turn = transcript.turns.find((candidate) => candidate.id === turnId);
6704
+ if (!turn) {
6705
+ turn = {
6706
+ id: turnId,
6707
+ status: "inProgress",
6708
+ startedAt,
6709
+ completedAt: null,
6710
+ items: []
6711
+ };
6712
+ transcript.turns.push(turn);
6713
+ transcript.turns.sort((a, b) => Date.parse(a.startedAt) - Date.parse(b.startedAt));
6714
+ return turn;
6715
+ }
6716
+ if (Date.parse(startedAt) < Date.parse(turn.startedAt)) {
6717
+ turn.startedAt = startedAt;
6718
+ }
6719
+ return turn;
6251
6720
  }
6252
- if (turnError.additionalDetails) {
6253
- parts.push(`details=${turnError.additionalDetails}`);
6721
+ mergeTranscriptTurn(threadId, turn) {
6722
+ const startedAt = timestampFromSeconds(turn.startedAt);
6723
+ const completedAt = turn.completedAt === null ? null : timestampFromSeconds(turn.completedAt);
6724
+ const itemTimestamp = completedAt ?? startedAt;
6725
+ const transcriptTurn = this.ensureTranscriptTurn(threadId, turn.id, startedAt);
6726
+ transcriptTurn.status = turn.status;
6727
+ transcriptTurn.completedAt = completedAt;
6728
+ const itemStatus = turn.status === "failed" ? "failed" : turn.status === "completed" ? "completed" : "in_progress";
6729
+ for (const item of turn.items) {
6730
+ this.upsertTranscriptItem(
6731
+ threadId,
6732
+ turn.id,
6733
+ item,
6734
+ item.type === "userMessage" ? startedAt : itemTimestamp,
6735
+ itemStatus,
6736
+ "completed"
6737
+ );
6738
+ }
6739
+ if (turn.error) {
6740
+ const existingIndex = transcriptTurn.items.findIndex((item) => item.id === `${turn.id}-error`);
6741
+ const errorItem = {
6742
+ type: "error",
6743
+ id: `${turn.id}-error`,
6744
+ message: formatTurnFailure(turn),
6745
+ timestamp: itemTimestamp,
6746
+ sequence: existingIndex === -1 ? this.nextTranscriptSequence() : transcriptTurn.items[existingIndex].sequence
6747
+ };
6748
+ if (existingIndex === -1) {
6749
+ transcriptTurn.items.push(errorItem);
6750
+ } else {
6751
+ transcriptTurn.items[existingIndex] = errorItem;
6752
+ }
6753
+ }
6754
+ if (this.codexAspTranscript) {
6755
+ this.codexAspTranscript.updatedAt = itemTimestamp;
6756
+ }
6254
6757
  }
6255
- return parts.join(" ");
6256
- }
6257
- function formatCommandOutput(item) {
6258
- const exitCode = item.exitCode ?? (item.status === "completed" ? 0 : 1);
6259
- const output = item.aggregatedOutput ?? "";
6260
- return output ? `Exit code: ${exitCode}
6261
- ${output}` : `Exit code: ${exitCode}`;
6262
- }
6263
- function fileChangeToPatchInput(item) {
6264
- const lines = ["*** Begin Patch"];
6265
- for (const change of item.changes) {
6266
- if (change.kind.type === "add") {
6267
- lines.push(`*** Add File: ${change.path}`);
6268
- } else if (change.kind.type === "delete") {
6269
- lines.push(`*** Delete File: ${change.path}`);
6758
+ upsertTranscriptItem(threadId, turnId, item, timestamp, status, lifecycle) {
6759
+ const turn = this.ensureTranscriptTurn(threadId, turnId, timestamp);
6760
+ const candidate = itemToTranscriptItem(item, timestamp, status);
6761
+ if (!candidate) return;
6762
+ const existingIndex = findEquivalentCodexAspTranscriptItemIndex(turn.items, candidate);
6763
+ const existing = existingIndex === -1 ? null : turn.items[existingIndex];
6764
+ const sequence = existing?.sequence ?? this.nextTranscriptSequence();
6765
+ const itemTimestamp = existing && lifecycle === "completed" ? existing.timestamp : timestamp;
6766
+ const transcriptItem = itemToTranscriptItem(item, itemTimestamp, status);
6767
+ if (!transcriptItem) return;
6768
+ const sequencedItem = { ...transcriptItem, sequence };
6769
+ if (existingIndex === -1) {
6770
+ turn.items.push(sequencedItem);
6270
6771
  } else {
6271
- lines.push(`*** Update File: ${change.path}`);
6272
- if (change.kind.move_path) {
6273
- lines.push(`*** Move to: ${change.kind.move_path}`);
6274
- }
6772
+ const existingItem = turn.items[existingIndex];
6773
+ const mergedItem = mergeCodexAspTranscriptItem(existingItem, {
6774
+ ...sequencedItem,
6775
+ timestamp: itemTimestamp,
6776
+ sequence
6777
+ });
6778
+ turn.items[existingIndex] = existingItem.type === "agentMessage" && mergedItem.type === "agentMessage" && lifecycle === "started" && mergedItem.text.length === 0 ? { ...mergedItem, text: existingItem.text } : mergedItem;
6275
6779
  }
6276
- if (change.diff.trim().length > 0) {
6277
- lines.push(change.diff);
6780
+ if (this.codexAspTranscript) {
6781
+ this.codexAspTranscript.updatedAt = timestamp;
6278
6782
  }
6279
6783
  }
6280
- lines.push("*** End Patch");
6281
- return lines.join("\n");
6282
- }
6283
- function itemToAgentEventDrafts(item, lifecycle) {
6284
- if (item.type === "agentMessage") {
6285
- if (lifecycle !== "completed" || !item.text) return [];
6286
- return [{
6287
- type: "response_item",
6288
- payload: {
6289
- type: "message",
6290
- role: "assistant",
6291
- content: [{
6292
- type: "output_text",
6293
- text: item.text
6294
- }]
6784
+ appendAgentMessageDelta(threadId, turnId, itemId, delta) {
6785
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
6786
+ const turn = this.ensureTranscriptTurn(threadId, turnId, timestamp);
6787
+ const existingIndex = turn.items.findIndex((item) => item.id === itemId);
6788
+ if (existingIndex === -1) {
6789
+ turn.items.push({
6790
+ type: "agentMessage",
6791
+ id: itemId,
6792
+ text: delta,
6793
+ timestamp,
6794
+ sequence: this.nextTranscriptSequence()
6795
+ });
6796
+ } else {
6797
+ const existing = turn.items[existingIndex];
6798
+ if (existing.type === "agentMessage") {
6799
+ turn.items[existingIndex] = {
6800
+ ...existing,
6801
+ text: `${existing.text}${delta}`
6802
+ };
6295
6803
  }
6296
- }];
6804
+ }
6805
+ if (this.codexAspTranscript) {
6806
+ this.codexAspTranscript.updatedAt = timestamp;
6807
+ }
6297
6808
  }
6298
- if (item.type === "reasoning") {
6299
- if (lifecycle !== "completed") return [];
6300
- const text = [...item.summary, ...item.content].filter(Boolean).join("\n").trim();
6301
- if (!text) return [];
6302
- return [{
6303
- type: "event_msg",
6304
- payload: {
6305
- type: "agent_reasoning",
6306
- text
6307
- }
6308
- }];
6809
+ appendTranscriptOutput(turnId, itemId, delta) {
6810
+ if (!this.codexAspTranscript) return;
6811
+ const turn = this.codexAspTranscript.turns.find((candidate) => candidate.id === turnId);
6812
+ if (!turn) return;
6813
+ const itemIndex = turn.items.findIndex((item2) => item2.id === itemId);
6814
+ if (itemIndex === -1) return;
6815
+ const item = turn.items[itemIndex];
6816
+ if (item.type !== "commandExecution" && item.type !== "fileChange") return;
6817
+ turn.items[itemIndex] = {
6818
+ ...item,
6819
+ output: `${item.output ?? ""}${delta}`,
6820
+ status: "in_progress"
6821
+ };
6822
+ this.codexAspTranscript.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
6309
6823
  }
6310
- if (item.type === "commandExecution") {
6311
- if (lifecycle === "started") {
6312
- return [{
6313
- type: "response_item",
6314
- payload: {
6315
- type: "function_call",
6316
- name: "exec_command",
6317
- call_id: item.id,
6318
- arguments: JSON.stringify({ cmd: item.command })
6319
- }
6320
- }];
6321
- }
6322
- return [{
6323
- type: "response_item",
6324
- payload: {
6325
- type: "function_call_output",
6326
- call_id: item.id,
6327
- output: formatCommandOutput(item)
6328
- }
6329
- }];
6824
+ seedHistoryFromThread(thread) {
6825
+ this.mergeTranscriptSnapshot(threadToAspTranscript(thread));
6330
6826
  }
6331
- if (item.type === "fileChange") {
6332
- if (lifecycle === "started") {
6333
- return [{
6334
- type: "response_item",
6335
- payload: {
6336
- type: "custom_tool_call",
6337
- name: "apply_patch",
6338
- call_id: item.id,
6339
- input: fileChangeToPatchInput(item),
6340
- status: "in_progress"
6341
- }
6342
- }];
6827
+ async refreshThreadGoal(host, threadId) {
6828
+ try {
6829
+ const response = await host.client.request(
6830
+ THREAD_GOAL_GET_METHOD,
6831
+ { threadId }
6832
+ );
6833
+ this.currentGoal = response.goal ? aspGoalToChatGoal(response.goal) : null;
6834
+ } catch (error) {
6835
+ console.warn("[CodexAspManager] Failed to read ASP thread goal:", error);
6343
6836
  }
6344
- return [{
6345
- type: "response_item",
6346
- payload: {
6347
- type: "custom_tool_call_output",
6348
- call_id: item.id,
6349
- output: JSON.stringify({
6350
- output: item.status,
6351
- metadata: { exit_code: item.status === "completed" ? 0 : 1 }
6352
- })
6353
- }
6354
- }];
6355
6837
  }
6356
- if (item.type === "mcpToolCall" || item.type === "dynamicToolCall" || item.type === "webSearch") {
6357
- const tool2 = item.type === "webSearch" ? "web_search" : item.tool;
6358
- const input = item.type === "webSearch" ? item.query : JSON.stringify(item.arguments);
6359
- if (lifecycle === "started") {
6360
- return [{
6361
- type: "response_item",
6362
- payload: {
6363
- type: "custom_tool_call",
6364
- name: tool2,
6365
- call_id: item.id,
6366
- input,
6367
- status: "in_progress"
6368
- }
6369
- }];
6838
+ recordGoalChange(goal, force = false) {
6839
+ const nextGoal = goal ? aspGoalToChatGoal(goal) : null;
6840
+ if (!force && JSON.stringify(this.currentGoal) === JSON.stringify(nextGoal)) {
6841
+ return;
6370
6842
  }
6371
- const failed = item.type === "mcpToolCall" && item.status === "failed" || item.type === "dynamicToolCall" && item.success === false;
6372
- return [{
6373
- type: "response_item",
6374
- payload: {
6375
- type: "custom_tool_call_output",
6376
- call_id: item.id,
6377
- output: JSON.stringify({
6378
- output: failed ? "Failed" : "Done",
6379
- metadata: { exit_code: failed ? 1 : 0 }
6380
- })
6381
- }
6382
- }];
6843
+ this.currentGoal = nextGoal;
6844
+ const event = this.recordHistoryEvent(
6845
+ CHAT_GOAL_EVENT_TYPE,
6846
+ { goal: nextGoal }
6847
+ );
6848
+ this.onEvent(event);
6383
6849
  }
6384
- return [];
6385
- }
6386
- function threadToHistoryEvents(thread) {
6387
- const events = [];
6388
- const turns = thread.turns.map((turn, index) => ({ turn, index })).sort((a, b) => (a.turn.startedAt ?? a.turn.completedAt ?? Number.MAX_SAFE_INTEGER) - (b.turn.startedAt ?? b.turn.completedAt ?? Number.MAX_SAFE_INTEGER) || a.index - b.index);
6389
- for (const { turn } of turns) {
6390
- const startedAt = timestampFromSeconds(turn.startedAt);
6391
- const completedAt = timestampFromSeconds(turn.completedAt ?? turn.startedAt);
6392
- const userMessages = turn.items.filter((item) => item.type === "userMessage");
6393
- const agentItems = turn.items.filter((item) => item.type !== "userMessage");
6394
- for (const item of userMessages) {
6395
- const message = item.content.filter((input) => input.type === "text").map((input) => input.text).join("\n");
6396
- if (message) {
6397
- events.push({
6398
- timestamp: startedAt,
6399
- type: "event_msg",
6400
- payload: {
6401
- type: "user_message",
6402
- message,
6403
- [CODEX_ASP_ITEM_ID_PAYLOAD_KEY]: item.id
6404
- }
6405
- });
6406
- }
6407
- }
6408
- for (const item of agentItems) {
6409
- for (const draft of itemToAgentEventDrafts(item, "started")) {
6410
- events.push({ timestamp: startedAt, ...draft });
6411
- }
6412
- for (const draft of itemToAgentEventDrafts(item, "completed")) {
6413
- events.push({ timestamp: completedAt, ...draft });
6414
- }
6415
- }
6850
+ recordHistoryEvent(type, payload) {
6851
+ const event = {
6852
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6853
+ type,
6854
+ payload
6855
+ };
6856
+ this.historyEvents.push(event);
6857
+ return event;
6416
6858
  }
6417
- return events;
6418
- }
6419
- async function buildThreadStartParams(workingDirectory, request, developerInstructions) {
6420
- const additionalDirectories = await getAgentAdditionalDirectories();
6421
- return {
6422
- model: request.model ?? DEFAULT_MODEL2,
6423
- cwd: workingDirectory,
6424
- runtimeWorkspaceRoots: additionalDirectories,
6425
- sandbox: "danger-full-access",
6426
- developerInstructions: developerInstructions ?? null,
6427
- config: { web_search: "live" },
6428
- experimentalRawEvents: false,
6429
- persistExtendedHistory: false
6430
- };
6431
- }
6432
- async function buildThreadResumeParams(workingDirectory, threadId, request, developerInstructions) {
6433
- const additionalDirectories = await getAgentAdditionalDirectories();
6434
- return {
6435
- threadId,
6436
- model: request.model ?? DEFAULT_MODEL2,
6437
- cwd: workingDirectory,
6438
- runtimeWorkspaceRoots: additionalDirectories,
6439
- sandbox: "danger-full-access",
6440
- developerInstructions: developerInstructions ?? null,
6441
- config: { web_search: "live" },
6442
- excludeTurns: false,
6443
- persistExtendedHistory: false
6444
- };
6445
- }
6446
- async function buildTurnInput(request) {
6447
- const input = [{
6448
- type: "text",
6449
- text: request.message,
6450
- text_elements: []
6451
- }];
6452
- if (!request.images || request.images.length === 0) {
6453
- return { input, tempImagePaths: [] };
6859
+ emitTranscriptUpdated(threadId) {
6860
+ const updatedAt = this.codexAspTranscript?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
6861
+ const transcript = this.codexAspTranscript ? structuredClone(this.codexAspTranscript) : null;
6862
+ const updatePayload = {
6863
+ updatedAt,
6864
+ transcript,
6865
+ threadId
6866
+ };
6867
+ this.onEvent({
6868
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6869
+ type: CODEX_ASP_TRANSCRIPT_UPDATED_EVENT_TYPE,
6870
+ payload: updatePayload
6871
+ });
6454
6872
  }
6455
- const normalizedImages = await normalizeImages(request.images);
6456
- const tempImagePaths = await saveNormalizedImagesToTempFiles(normalizedImages);
6457
- input.push(...tempImagePaths.map((path4) => ({
6458
- type: "localImage",
6459
- path: path4
6460
- })));
6461
- return { input, tempImagePaths };
6462
- }
6463
- async function buildTurnStartParams(threadId, request, developerInstructions) {
6464
- const effort = toReasoningEffort(request.thinkingLevel);
6465
- const model = request.model ?? DEFAULT_MODEL2;
6466
- const { input, tempImagePaths } = await buildTurnInput(request);
6467
- return {
6468
- params: {
6469
- threadId,
6470
- input,
6471
- model,
6472
- ...effort ? { effort } : {},
6473
- ...developerInstructions ? {
6474
- collaborationMode: {
6475
- mode: "default",
6476
- settings: {
6477
- model,
6478
- reasoning_effort: effort ?? null,
6479
- developer_instructions: developerInstructions
6480
- }
6481
- }
6482
- } : {}
6483
- },
6484
- tempImagePaths
6485
- };
6486
- }
6487
-
6488
- // src/managers/codex-asp/notification-dispatch.ts
6489
- var TURN_STARTED_METHOD = "turn/started";
6490
- var TURN_COMPLETED_METHOD = "turn/completed";
6491
- var TURN_PLAN_UPDATED_METHOD = "turn/plan/updated";
6492
- var THREAD_GOAL_UPDATED_METHOD = "thread/goal/updated";
6493
- var THREAD_GOAL_CLEARED_METHOD = "thread/goal/cleared";
6494
- var ITEM_STARTED_METHOD = "item/started";
6495
- var ITEM_COMPLETED_METHOD = "item/completed";
6496
- var AGENT_MESSAGE_DELTA_METHOD = "item/agentMessage/delta";
6497
- var ACCOUNT_RATE_LIMITS_UPDATED_METHOD = "account/rateLimits/updated";
6498
- var THREAD_TOKEN_USAGE_UPDATED_METHOD = "thread/tokenUsage/updated";
6499
- var THREAD_COMPACTED_METHOD = "thread/compacted";
6500
- var COMMAND_APPROVAL_METHOD = "item/commandExecution/requestApproval";
6501
- var FILE_CHANGE_APPROVAL_METHOD = "item/fileChange/requestApproval";
6502
- function dispatchAspNotification(notification, handlers) {
6503
- const handler = handlers[notification.method];
6504
- if (!handler) return;
6505
- handler(notification);
6506
- }
6873
+ handleRateLimits(rateLimits) {
6874
+ const snapshot = extractRateLimitsSnapshot(rateLimits);
6875
+ if (snapshot) {
6876
+ this.emitQuotaStatus(snapshot);
6877
+ }
6878
+ }
6879
+ async refreshQuotaSnapshot(host) {
6880
+ try {
6881
+ const response = await host.client.request(
6882
+ ACCOUNT_RATE_LIMITS_READ_METHOD,
6883
+ void 0
6884
+ );
6885
+ this.handleRateLimits(response.rateLimits);
6886
+ } catch {
6887
+ }
6888
+ }
6889
+ emitQuotaStatus(snapshot, force = false) {
6890
+ const event = this.quotaStatus.apply(snapshot, force);
6891
+ if (!event) return;
6892
+ this.historyEvents.push(event);
6893
+ this.onEvent(event);
6894
+ }
6895
+ emitCodexTokenUsage(tokenUsage, model) {
6896
+ const payload = buildCodexTokenUsageContextUsagePayload({
6897
+ model,
6898
+ modelContextWindow: tokenUsage.modelContextWindow,
6899
+ last: {
6900
+ inputTokens: tokenUsage.last.inputTokens,
6901
+ outputTokens: tokenUsage.last.outputTokens,
6902
+ totalTokens: tokenUsage.last.totalTokens,
6903
+ cachedInputTokens: tokenUsage.last.cachedInputTokens,
6904
+ reasoningOutputTokens: tokenUsage.last.reasoningOutputTokens
6905
+ },
6906
+ total: {
6907
+ totalTokens: tokenUsage.total.totalTokens
6908
+ },
6909
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
6910
+ });
6911
+ const event = this.emitContextUsage(payload);
6912
+ this.historyEvents.push(event);
6913
+ }
6914
+ };
6507
6915
 
6508
- // src/managers/codex-asp/codex-asp-manager.ts
6509
- function historyEventKey(event) {
6510
- const payloadType = typeof event.payload.type === "string" ? event.payload.type : null;
6511
- const callId = typeof event.payload.call_id === "string" ? event.payload.call_id : null;
6512
- const itemId = typeof event.payload[CODEX_ASP_ITEM_ID_PAYLOAD_KEY] === "string" ? event.payload[CODEX_ASP_ITEM_ID_PAYLOAD_KEY] : null;
6513
- const messageId = typeof event.payload[USER_MESSAGE_ID_PAYLOAD_KEY] === "string" ? event.payload[USER_MESSAGE_ID_PAYLOAD_KEY] : null;
6514
- if (event.type === "response_item" && payloadType && callId) {
6515
- return `${event.type}:${payloadType}:${callId}`;
6516
- }
6517
- if (event.type === "event_msg" && event.payload.type === "user_message") {
6518
- if (messageId) return `${event.type}:user_message:message:${messageId}`;
6519
- if (itemId) return `${event.type}:user_message:item:${itemId}`;
6520
- const command = typeof event.payload.command === "string" ? event.payload.command : "";
6521
- const message = typeof event.payload.message === "string" ? event.payload.message : "";
6522
- return `${event.type}:user_message:${event.timestamp}:${command}:${message}`;
6523
- }
6524
- if (itemId && payloadType) {
6525
- return `${event.type}:${payloadType}:item:${itemId}`;
6526
- }
6527
- return `${event.type}:${event.timestamp}:${JSON.stringify(event.payload)}`;
6528
- }
6529
- function areDuplicateHistoryEvents(a, b) {
6530
- if (historyEventKey(a) === historyEventKey(b)) return true;
6531
- return areSameUserMessageEvents(a, b);
6532
- }
6533
- function mergeHistoryEvent(current, candidate) {
6534
- if (getUserMessage(current) && getUserMessage(current) === getUserMessage(candidate)) {
6535
- return {
6536
- ...current,
6537
- timestamp: getEventTimestampMs(current) <= getEventTimestampMs(candidate) ? current.timestamp : candidate.timestamp,
6538
- payload: {
6539
- ...current.payload,
6540
- ...candidate.payload
6541
- }
6542
- };
6916
+ // src/managers/codex-manager.ts
6917
+ import { Codex } from "@openai/codex-sdk";
6918
+ import { readdir as readdir3, stat as stat2, writeFile as writeFile8, mkdir as mkdir10, readFile as readFile7 } from "fs/promises";
6919
+ import { existsSync as existsSync6 } from "fs";
6920
+ import { join as join14 } from "path";
6921
+ import { homedir as homedir12 } from "os";
6922
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
6923
+ var DEFAULT_MODEL2 = "gpt-5.5";
6924
+ var CODEX_CONFIG_PATH = join14(homedir12(), ".codex", "config.toml");
6925
+ function isJsonlEvent2(value) {
6926
+ if (!isRecord4(value)) {
6927
+ return false;
6543
6928
  }
6544
- return candidate;
6929
+ return typeof value.timestamp === "string" && typeof value.type === "string" && isRecord4(value.payload);
6545
6930
  }
6546
- function mergeHistoryEvents(primary, supplemental) {
6547
- return mergeAgentEvents(primary, supplemental, {
6548
- areDuplicates: areDuplicateHistoryEvents,
6549
- mergeEvent: mergeHistoryEvent
6550
- });
6931
+ function sleep(ms) {
6932
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
6551
6933
  }
6552
- var CodexAspManager = class extends CodingAgentManager {
6934
+ var CodexManager = class extends CodingAgentManager {
6935
+ codex;
6553
6936
  currentThreadId = null;
6554
- activeTurnId = null;
6555
- threadAttached = false;
6556
- historyEvents = [];
6557
- emittedItemStages = /* @__PURE__ */ new Set();
6937
+ currentThread = null;
6938
+ activeAbortController = null;
6558
6939
  quotaStatus = new CodexQuotaStatusTracker();
6559
- currentGoal = null;
6560
6940
  constructor(options) {
6561
6941
  super(options);
6942
+ this.codex = this.createCodexClient();
6562
6943
  this.initializeManager(this.processMessageInternal.bind(this));
6563
6944
  }
6945
+ createCodexClient() {
6946
+ const codexApiKey = resolveCodexApiKey();
6947
+ return new Codex({
6948
+ env: buildCodexAgentEnv(),
6949
+ ...codexApiKey ? { apiKey: codexApiKey } : {}
6950
+ });
6951
+ }
6952
+ resetCodexClient() {
6953
+ this.codex = this.createCodexClient();
6954
+ this.currentThread = null;
6955
+ }
6564
6956
  async initialize() {
6565
6957
  if (this.initialSessionId) {
6566
6958
  this.currentThreadId = this.initialSessionId;
6959
+ console.log(`[CodexManager] Restored thread ID from persisted state: ${this.currentThreadId}`);
6567
6960
  }
6568
6961
  }
6569
- async interruptActiveTurn() {
6570
- if (!this.currentThreadId || !this.activeTurnId) {
6571
- return;
6572
- }
6962
+ async flushQuotaSnapshotFromCurrentSession() {
6963
+ if (!this.currentThreadId) return;
6573
6964
  try {
6574
- const host = await getCodexAspHost();
6575
- const params = {
6576
- threadId: this.currentThreadId,
6577
- turnId: this.activeTurnId
6578
- };
6579
- await host.client.request(TURN_INTERRUPT_METHOD, params);
6965
+ const sessionFile = await this.findSessionFile(this.currentThreadId);
6966
+ if (!sessionFile) return;
6967
+ const content = await readFile7(sessionFile, "utf-8");
6968
+ const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
6969
+ let latest = null;
6970
+ for (const line of lines) {
6971
+ try {
6972
+ const parsed = JSON.parse(line);
6973
+ const snapshot = extractCodexRateLimitsSnapshotFromJsonl(parsed);
6974
+ if (snapshot) {
6975
+ latest = snapshot;
6976
+ }
6977
+ } catch {
6978
+ }
6979
+ }
6980
+ if (latest) {
6981
+ this.emitQuotaStatus(latest);
6982
+ }
6580
6983
  } catch (error) {
6581
- console.warn("[CodexAspManager] Failed to interrupt active turn:", error);
6984
+ console.warn("[CodexManager] Failed to flush quota snapshot from session:", error);
6582
6985
  }
6583
6986
  }
6584
- async getHistory() {
6585
- if (!this.currentThreadId) {
6586
- return { thread_id: null, events: [], goal: null };
6987
+ emitQuotaStatus(snapshot, force = false) {
6988
+ const event = this.quotaStatus.apply(snapshot, force);
6989
+ if (event) this.onEvent(event);
6990
+ }
6991
+ async interruptActiveTurn() {
6992
+ if (this.activeAbortController) {
6993
+ this.activeAbortController.abort();
6587
6994
  }
6995
+ }
6996
+ /**
6997
+ * Update the developer_instructions in ~/.codex/config.toml
6998
+ * This sets the system prompt that Codex will use for this turn
6999
+ */
7000
+ async updateCodexConfig(developerInstructions) {
6588
7001
  try {
6589
- const host = await getCodexAspHost();
6590
- const [response] = await Promise.all([
6591
- host.client.request(
6592
- THREAD_READ_METHOD,
6593
- { threadId: this.currentThreadId, includeTurns: true }
6594
- ),
6595
- this.refreshThreadGoal(host, this.currentThreadId)
6596
- ]);
6597
- const events = mergeHistoryEvents(this.historyEvents, threadToHistoryEvents(response.thread));
6598
- this.historyEvents.splice(0, this.historyEvents.length, ...events);
6599
- return {
6600
- thread_id: this.currentThreadId,
6601
- events,
6602
- goal: this.currentGoal
6603
- };
6604
- } catch {
7002
+ const codexDir = join14(homedir12(), ".codex");
7003
+ await mkdir10(codexDir, { recursive: true });
7004
+ let config = {};
7005
+ if (existsSync6(CODEX_CONFIG_PATH)) {
7006
+ try {
7007
+ const existingContent = await readFile7(CODEX_CONFIG_PATH, "utf-8");
7008
+ const parsed = parseToml(existingContent);
7009
+ if (isRecord4(parsed)) {
7010
+ config = parsed;
7011
+ }
7012
+ } catch (parseError) {
7013
+ console.warn("[CodexManager] Failed to parse existing config.toml, starting fresh:", parseError);
7014
+ }
7015
+ }
7016
+ if (developerInstructions) {
7017
+ config.developer_instructions = developerInstructions;
7018
+ } else {
7019
+ delete config.developer_instructions;
7020
+ }
7021
+ const tomlContent = stringifyToml(config);
7022
+ await writeFile8(CODEX_CONFIG_PATH, tomlContent, "utf-8");
7023
+ console.log("[CodexManager] Updated config.toml with developer_instructions");
7024
+ } catch (error) {
7025
+ console.error("[CodexManager] Failed to update config.toml:", error);
6605
7026
  }
6606
- return {
6607
- thread_id: this.currentThreadId,
6608
- events: [...this.historyEvents],
6609
- goal: this.currentGoal
6610
- };
6611
- }
6612
- getGoal() {
6613
- return this.currentGoal;
6614
7027
  }
6615
7028
  async processMessageInternal(request) {
6616
- let userMessageRecorded = false;
6617
- const recordUserMessage = (extraPayload = {}) => {
6618
- if (userMessageRecorded) return;
6619
- userMessageRecorded = true;
6620
- this.recordHistoryEvent("event_msg", {
6621
- type: "user_message",
6622
- message: request.message,
6623
- ...extraPayload
6624
- });
6625
- };
6626
- const goalCommand = parseGoalCommand(request.message);
6627
- const dispatch = async () => {
6628
- if (goalCommand?.type === "clear") {
6629
- await this.executeGoalClearCommand(request, recordUserMessage);
6630
- return;
6631
- }
6632
- if (goalCommand?.type === "set") {
6633
- await this.executeAspTurn(request, recordUserMessage, {
6634
- runTurn: (host, threadId) => this.runGoalTurn(host, threadId, request, goalCommand.objective),
6635
- userMessagePayload: { command: "goal" }
6636
- });
6637
- return;
6638
- }
6639
- await this.executeAspTurn(request, recordUserMessage);
6640
- };
6641
7029
  try {
6642
- await dispatch();
7030
+ await this.executeCodexTurn(request);
6643
7031
  } catch (error) {
6644
7032
  if (isCodexAuthError(error)) {
6645
7033
  const refreshed = await codexTokenManager.fetchFreshCredentials(error instanceof Error ? error.message : String(error));
6646
7034
  if (refreshed) {
6647
- await restartCodexAspHost();
6648
- this.threadAttached = false;
6649
- await dispatch();
7035
+ this.resetCodexClient();
7036
+ await this.executeCodexTurn(request);
6650
7037
  return;
6651
- }
6652
- }
6653
- throw error;
6654
- } finally {
6655
- this.activeTurnId = null;
6656
- await this.onTurnComplete();
6657
- }
6658
- }
6659
- async executeGoalClearCommand(request, recordUserMessage) {
6660
- const host = await getCodexAspHost();
6661
- const developerInstructions = this.buildCombinedInstructions(request.customInstructions);
6662
- recordUserMessage({ command: "goal" });
6663
- const threadId = await this.ensureThread(host, request, developerInstructions);
6664
- await host.client.request(
6665
- THREAD_GOAL_CLEAR_METHOD,
6666
- { threadId }
6667
- );
6668
- this.recordGoalChange(null, true);
6669
- }
6670
- async executeAspTurn(request, recordUserMessage, options = {}) {
6671
- const host = await getCodexAspHost();
6672
- if (this.quotaStatus.blocked && this.quotaStatus.latestSnapshot) {
6673
- await this.refreshQuotaSnapshot(host);
6674
- if (this.quotaStatus.blocked && this.quotaStatus.latestSnapshot) {
6675
- recordUserMessage(options.userMessagePayload);
6676
- this.emitQuotaStatus(this.quotaStatus.latestSnapshot, true);
6677
- return;
6678
- }
6679
- }
6680
- const developerInstructions = this.buildCombinedInstructions(request.customInstructions);
6681
- recordUserMessage(options.userMessagePayload);
6682
- const threadId = await this.ensureThread(host, request, developerInstructions);
6683
- const runTurn = options.runTurn ?? ((aspHost, aspThreadId, aspInstructions) => this.runTurn(aspHost, aspThreadId, request, aspInstructions));
6684
- let completedTurn;
6685
- try {
6686
- completedTurn = await runTurn(host, threadId, developerInstructions);
6687
- } catch (error) {
6688
- await this.refreshQuotaSnapshot(host);
6689
- if (this.quotaStatus.blocked) {
6690
- return;
6691
- }
6692
- throw error;
6693
- }
6694
- if (completedTurn.status === "failed") {
6695
- await this.refreshQuotaSnapshot(host);
6696
- if (this.quotaStatus.blocked) {
6697
- return;
6698
- }
6699
- throw new Error(formatTurnFailure(completedTurn));
6700
- }
6701
- if (completedTurn.status === "completed") {
6702
- this.emitTurnCompletedItems(completedTurn);
6703
- }
6704
- }
6705
- async ensureThread(host, request, developerInstructions) {
6706
- if (this.currentThreadId) {
6707
- if (!this.threadAttached) {
6708
- const response = await host.client.request(
6709
- THREAD_RESUME_METHOD,
6710
- await buildThreadResumeParams(this.workingDirectory, this.currentThreadId, request, developerInstructions)
6711
- );
6712
- this.currentThreadId = response.thread.id;
6713
- this.threadAttached = true;
6714
- this.seedHistoryFromThread(response.thread);
6715
- await this.onSaveSessionId(this.currentThreadId);
7038
+ }
6716
7039
  }
6717
- return this.currentThreadId;
7040
+ throw error;
6718
7041
  }
6719
- const threadStartResponse = await host.client.request(
6720
- THREAD_START_METHOD,
6721
- await buildThreadStartParams(this.workingDirectory, request, developerInstructions)
6722
- );
6723
- this.currentThreadId = threadStartResponse.thread.id;
6724
- this.threadAttached = true;
6725
- await this.onSaveSessionId(this.currentThreadId);
6726
- return this.currentThreadId;
6727
- }
6728
- async runTurn(host, threadId, request, developerInstructions) {
6729
- const { params, tempImagePaths } = await buildTurnStartParams(threadId, request, developerInstructions);
6730
- return this.observeTurn(host, threadId, request, async () => {
6731
- const turnStartResponse = await host.client.request(
6732
- TURN_START_METHOD,
6733
- params
6734
- );
6735
- return { turn: turnStartResponse.turn, tempImagePaths };
6736
- });
6737
- }
6738
- async runGoalTurn(host, threadId, request, objective) {
6739
- return this.observeTurn(host, threadId, request, async () => {
6740
- const response = await host.client.request(
6741
- THREAD_GOAL_SET_METHOD,
6742
- { threadId, objective, status: "active" }
6743
- );
6744
- this.recordGoalChange(response.goal, true);
6745
- return { turn: null, tempImagePaths: [] };
6746
- });
6747
7042
  }
6748
- async observeTurn(host, threadId, request, startTurn) {
6749
- let resolveCompleted;
6750
- const completed = new Promise((resolve3) => {
6751
- resolveCompleted = resolve3;
6752
- });
6753
- let rejectDisposed = () => {
6754
- };
6755
- const disposed = new Promise((_resolve, reject) => {
6756
- rejectDisposed = reject;
6757
- });
6758
- void disposed.catch(() => {
6759
- });
6760
- let observedTurnId = null;
6761
- const completedItems = [];
6762
- const agentMessageDeltas = /* @__PURE__ */ new Map();
7043
+ async executeCodexTurn(request) {
7044
+ if (this.quotaStatus.blocked && this.quotaStatus.latestSnapshot) {
7045
+ await this.flushQuotaSnapshotFromCurrentSession();
7046
+ if (this.quotaStatus.blocked && this.quotaStatus.latestSnapshot) {
7047
+ this.emitQuotaStatus(this.quotaStatus.latestSnapshot, true);
7048
+ try {
7049
+ await this.onTurnComplete();
7050
+ } catch (error) {
7051
+ console.error("[CodexManager] onTurnComplete failed during quota-blocked turn:", error);
7052
+ }
7053
+ return;
7054
+ }
7055
+ }
7056
+ const {
7057
+ message,
7058
+ model,
7059
+ customInstructions,
7060
+ images,
7061
+ permissionMode,
7062
+ thinkingLevel
7063
+ } = request;
6763
7064
  const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
6764
- const model = request.model ?? DEFAULT_MODEL2;
6765
7065
  let tempImagePaths = [];
6766
- const linearForwarder = new LinearEventForwarder(linearSessionId);
6767
- const matchesTurn = (notificationThreadId, notificationTurnId) => notificationThreadId === threadId && (!observedTurnId || notificationTurnId === null || notificationTurnId === observedTurnId);
6768
- const handlers = {
6769
- [ACCOUNT_RATE_LIMITS_UPDATED_METHOD]: (notification) => {
6770
- this.handleRateLimits(notification.params.rateLimits);
6771
- },
6772
- [THREAD_TOKEN_USAGE_UPDATED_METHOD]: (notification) => {
6773
- if (notification.params.threadId !== threadId) return;
6774
- this.emitCodexTokenUsage(notification.params.tokenUsage, model);
6775
- },
6776
- [THREAD_GOAL_UPDATED_METHOD]: (notification) => {
6777
- if (notification.params.threadId !== threadId) return;
6778
- this.recordGoalChange(notification.params.goal);
6779
- },
6780
- [THREAD_GOAL_CLEARED_METHOD]: (notification) => {
6781
- if (notification.params.threadId !== threadId) return;
6782
- this.recordGoalChange(null);
6783
- },
6784
- [THREAD_COMPACTED_METHOD]: (notification) => {
6785
- if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
6786
- this.setCompacting(false);
6787
- },
6788
- [TURN_STARTED_METHOD]: (notification) => {
6789
- if (notification.params.threadId !== threadId) return;
6790
- observedTurnId = notification.params.turn.id;
6791
- this.activeTurnId = notification.params.turn.id;
6792
- linearForwarder.sendEvent(convertCodexAspNotification(notification, linearSessionId ?? ""));
6793
- },
6794
- [TURN_PLAN_UPDATED_METHOD]: (notification) => {
6795
- if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
6796
- this.emitPlanUpdate(notification.params.plan);
6797
- linearForwarder.sendPlan(extractPlanFromCodexAspNotification(notification));
6798
- },
6799
- [ITEM_STARTED_METHOD]: (notification) => {
6800
- if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
6801
- if (notification.params.item.type === "contextCompaction") {
6802
- this.setCompacting(true);
6803
- }
6804
- this.emitThreadItemLifecycle(notification.params.item, "started");
6805
- linearForwarder.sendEvent(convertCodexAspNotification(notification, linearSessionId ?? ""));
6806
- },
6807
- [ITEM_COMPLETED_METHOD]: (notification) => {
6808
- if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
6809
- completedItems.push(notification.params.item);
6810
- if (notification.params.item.type === "contextCompaction") {
6811
- this.setCompacting(false);
6812
- }
6813
- this.emitThreadItemLifecycle(notification.params.item, "completed");
6814
- linearForwarder.sendEvent(convertCodexAspNotification(notification, linearSessionId ?? ""));
6815
- },
6816
- [AGENT_MESSAGE_DELTA_METHOD]: (notification) => {
6817
- if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
6818
- const currentText = agentMessageDeltas.get(notification.params.itemId) ?? "";
6819
- agentMessageDeltas.set(notification.params.itemId, currentText + notification.params.delta);
6820
- },
6821
- [TURN_COMPLETED_METHOD]: (notification) => {
6822
- if (notification.params.threadId !== threadId) return;
6823
- observedTurnId = notification.params.turn.id;
6824
- const turn = notification.params.turn;
6825
- const items = turn.items.length > 0 ? [...turn.items] : [];
6826
- const itemIds = new Set(items.map((item) => item.id));
6827
- for (const item of completedItems) {
6828
- if (!itemIds.has(item.id)) {
6829
- items.push(item);
6830
- itemIds.add(item.id);
7066
+ let stopTail = null;
7067
+ let abortController = null;
7068
+ try {
7069
+ if (images && images.length > 0) {
7070
+ const normalizedImages = await normalizeImages(images);
7071
+ tempImagePaths = await saveNormalizedImagesToTempFiles(normalizedImages);
7072
+ }
7073
+ const developerInstructions = this.buildCombinedInstructions(customInstructions);
7074
+ await this.updateCodexConfig(developerInstructions);
7075
+ const sandboxMode = "danger-full-access";
7076
+ const webSearchMode = "live";
7077
+ const codexReasoningEffort = codexReasoningEffortForThinkingLevel(thinkingLevel);
7078
+ const additionalDirectories = await getAgentAdditionalDirectories();
7079
+ const threadOptions = {
7080
+ workingDirectory: this.workingDirectory,
7081
+ skipGitRepoCheck: true,
7082
+ sandboxMode,
7083
+ model: model || DEFAULT_MODEL2,
7084
+ webSearchMode,
7085
+ additionalDirectories,
7086
+ ...codexReasoningEffort ? { modelReasoningEffort: codexReasoningEffort } : {}
7087
+ };
7088
+ abortController = new AbortController();
7089
+ this.activeAbortController = abortController;
7090
+ if (this.currentThreadId) {
7091
+ this.currentThread = this.codex.resumeThread(this.currentThreadId, threadOptions);
7092
+ } else {
7093
+ this.currentThread = this.codex.startThread(threadOptions);
7094
+ const { events } = await this.currentThread.runStreamed("Hello", { signal: abortController.signal });
7095
+ for await (const event of events) {
7096
+ if (event.type === "thread.started") {
7097
+ this.currentThreadId = event.thread_id;
7098
+ await this.onSaveSessionId(this.currentThreadId);
7099
+ console.log(`[CodexManager] Captured and persisted thread ID: ${this.currentThreadId}`);
6831
7100
  }
6832
7101
  }
6833
- for (const [itemId, text] of agentMessageDeltas) {
6834
- if (!itemIds.has(itemId)) {
6835
- items.push({
6836
- type: "agentMessage",
6837
- id: itemId,
6838
- text,
6839
- phase: null,
6840
- memoryCitation: null
6841
- });
6842
- }
7102
+ if (!this.currentThreadId && this.currentThread.id) {
7103
+ this.currentThreadId = this.currentThread.id;
7104
+ await this.onSaveSessionId(this.currentThreadId);
7105
+ console.log(`[CodexManager] Captured and persisted thread ID from thread.id: ${this.currentThreadId}`);
6843
7106
  }
6844
- resolveCompleted(items.length > 0 ? { ...turn, items, itemsView: "full" } : turn);
6845
7107
  }
6846
- };
6847
- const onNotification = (notification) => {
6848
- dispatchAspNotification(notification, handlers);
6849
- };
6850
- const onServerRequest = (serverRequest) => {
6851
- if (serverRequest.method !== COMMAND_APPROVAL_METHOD && serverRequest.method !== FILE_CHANGE_APPROVAL_METHOD) {
6852
- return;
7108
+ stopTail = this.currentThreadId ? await this.startSessionTail(this.currentThreadId) : null;
7109
+ let input;
7110
+ if (tempImagePaths.length > 0) {
7111
+ const inputItems = [
7112
+ { type: "text", text: message },
7113
+ ...tempImagePaths.map((path4) => ({ type: "local_image", path: path4 }))
7114
+ ];
7115
+ input = inputItems;
7116
+ } else {
7117
+ input = message;
6853
7118
  }
6854
- console.warn("[CodexAspManager] approval requested while sandbox is danger-full-access");
6855
- host.client.respond(serverRequest.id, { decision: "accept" });
6856
- };
6857
- const onDispose = (reason) => {
6858
- this.threadAttached = false;
6859
- this.activeTurnId = null;
6860
- const turnLabel = observedTurnId ? ` ${observedTurnId}` : "";
6861
- rejectDisposed(new Error(`Codex ASP client disposed before turn${turnLabel} completed: ${reason.message}`));
6862
- };
6863
- host.client.on("notification", onNotification);
6864
- host.client.on("serverRequest", onServerRequest);
6865
- host.client.on("dispose", onDispose);
6866
- try {
6867
- const started = await startTurn();
6868
- tempImagePaths = started.tempImagePaths;
6869
- if (started.turn) {
6870
- observedTurnId = started.turn.id;
6871
- this.activeTurnId = started.turn.id;
7119
+ try {
7120
+ const { events } = await this.currentThread.runStreamed(input, { signal: abortController.signal });
7121
+ const linearForwarder = new LinearEventForwarder(linearSessionId);
7122
+ for await (const event of events) {
7123
+ if (linearSessionId) {
7124
+ linearForwarder.sendPlan(extractPlanFromCodexEvent(event));
7125
+ linearForwarder.sendEvent(convertCodexEvent(event, linearSessionId));
7126
+ }
7127
+ }
7128
+ linearForwarder.flushThoughtAsResponse();
7129
+ } catch (error) {
7130
+ await this.flushQuotaSnapshotFromCurrentSession();
7131
+ if (this.quotaStatus.blocked) {
7132
+ return;
7133
+ }
7134
+ throw error;
6872
7135
  }
6873
- const turn = await Promise.race([completed, disposed]);
6874
- linearForwarder.flushThoughtAsResponse();
6875
- return turn;
6876
7136
  } finally {
6877
- host.client.off("notification", onNotification);
6878
- host.client.off("serverRequest", onServerRequest);
6879
- host.client.off("dispose", onDispose);
7137
+ if (stopTail) {
7138
+ await stopTail();
7139
+ }
6880
7140
  await removeTempImageFiles(tempImagePaths);
6881
- if (host.client.isDisposed) {
6882
- this.threadAttached = false;
7141
+ try {
7142
+ await this.onTurnComplete();
7143
+ } catch (error) {
7144
+ console.error("[CodexManager] onTurnComplete failed:", error);
6883
7145
  }
7146
+ this.activeAbortController = null;
6884
7147
  }
6885
7148
  }
6886
- emitThreadItemLifecycle(item, lifecycle) {
6887
- const stageKey = `${lifecycle}:${item.id}`;
6888
- if (this.emittedItemStages.has(stageKey)) {
6889
- return;
6890
- }
6891
- if (lifecycle === "completed" && !this.emittedItemStages.has(`started:${item.id}`)) {
6892
- this.emittedItemStages.add(`started:${item.id}`);
6893
- this.recordAndEmitDrafts(itemToAgentEventDrafts(item, "started"));
7149
+ async getHistory() {
7150
+ if (!this.currentThreadId) {
7151
+ return {
7152
+ thread_id: null,
7153
+ events: []
7154
+ };
6894
7155
  }
6895
- this.emittedItemStages.add(stageKey);
6896
- this.recordAndEmitDrafts(itemToAgentEventDrafts(item, lifecycle));
6897
- }
6898
- emitTurnCompletedItems(turn) {
6899
- for (const item of turn.items) {
6900
- this.emitThreadItemLifecycle(item, "completed");
7156
+ const sessionFile = await this.findSessionFile(this.currentThreadId);
7157
+ if (!sessionFile) {
7158
+ return {
7159
+ thread_id: this.currentThreadId,
7160
+ events: []
7161
+ };
6901
7162
  }
7163
+ const events = await readJSONL(sessionFile);
7164
+ return {
7165
+ thread_id: this.currentThreadId,
7166
+ events
7167
+ };
6902
7168
  }
6903
- emitPlanUpdate(plan) {
6904
- if (plan.length === 0) return;
6905
- this.recordAndEmitDrafts([{
6906
- type: "response_item",
6907
- payload: {
6908
- type: "function_call",
6909
- name: "update_plan",
6910
- call_id: `plan-${Date.now()}`,
6911
- arguments: JSON.stringify({ plan })
7169
+ // Helper methods for finding session files
7170
+ async findSessionFile(threadId) {
7171
+ const sessionsDir = join14(homedir12(), ".codex", "sessions");
7172
+ try {
7173
+ const now = /* @__PURE__ */ new Date();
7174
+ const year = now.getFullYear();
7175
+ const month = String(now.getMonth() + 1).padStart(2, "0");
7176
+ const day = String(now.getDate()).padStart(2, "0");
7177
+ const todayDir = join14(sessionsDir, String(year), month, day);
7178
+ const file = await this.findFileInDirectory(todayDir, threadId);
7179
+ if (file) return file;
7180
+ for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
7181
+ const date = new Date(now);
7182
+ date.setDate(date.getDate() - daysAgo);
7183
+ const searchYear = date.getFullYear();
7184
+ const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
7185
+ const searchDay = String(date.getDate()).padStart(2, "0");
7186
+ const searchDir = join14(sessionsDir, String(searchYear), searchMonth, searchDay);
7187
+ const file2 = await this.findFileInDirectory(searchDir, threadId);
7188
+ if (file2) return file2;
6912
7189
  }
6913
- }]);
6914
- }
6915
- seedHistoryFromThread(thread) {
6916
- const events = threadToHistoryEvents(thread);
6917
- this.historyEvents.splice(0, this.historyEvents.length, ...mergeHistoryEvents(this.historyEvents, events));
7190
+ return null;
7191
+ } catch (error) {
7192
+ return null;
7193
+ }
6918
7194
  }
6919
- async refreshThreadGoal(host, threadId) {
7195
+ async findFileInDirectory(directory, threadId) {
6920
7196
  try {
6921
- const response = await host.client.request(
6922
- THREAD_GOAL_GET_METHOD,
6923
- { threadId }
6924
- );
6925
- this.currentGoal = response.goal ? aspGoalToChatGoal(response.goal) : null;
7197
+ const files = await readdir3(directory);
7198
+ for (const file of files) {
7199
+ if (file.endsWith(".jsonl") && file.includes(threadId)) {
7200
+ const fullPath = join14(directory, file);
7201
+ const stats = await stat2(fullPath);
7202
+ if (stats.isFile()) {
7203
+ return fullPath;
7204
+ }
7205
+ }
7206
+ }
7207
+ return null;
6926
7208
  } catch (error) {
6927
- console.warn("[CodexAspManager] Failed to read ASP thread goal:", error);
7209
+ return null;
6928
7210
  }
6929
7211
  }
6930
- recordGoalChange(goal, force = false) {
6931
- const nextGoal = goal ? aspGoalToChatGoal(goal) : null;
6932
- if (!force && JSON.stringify(this.currentGoal) === JSON.stringify(nextGoal)) {
6933
- return;
7212
+ async waitForSessionFile(threadId, timeoutMs = 5e3) {
7213
+ const start = Date.now();
7214
+ while (Date.now() - start < timeoutMs) {
7215
+ const sessionFile = await this.findSessionFile(threadId);
7216
+ if (sessionFile) {
7217
+ return sessionFile;
7218
+ }
7219
+ await sleep(100);
6934
7220
  }
6935
- this.currentGoal = nextGoal;
6936
- const event = this.recordHistoryEvent(
6937
- CHAT_GOAL_EVENT_TYPE,
6938
- { goal: nextGoal }
6939
- );
6940
- this.onEvent(event);
7221
+ return null;
6941
7222
  }
6942
- recordAndEmitDrafts(drafts) {
6943
- for (const draft of drafts) {
6944
- const event = this.recordHistoryEvent(draft.type, draft.payload);
6945
- this.onEvent(event);
7223
+ // @openai/codex-sdk doesn't expose manual /compact (TUI-only); we only mirror the auto-compaction rollout entries to the UI.
7224
+ trackNativeCompaction(event) {
7225
+ if (event.type === "compacted") {
7226
+ this.setCompacting(false);
7227
+ return;
6946
7228
  }
6947
- }
6948
- recordHistoryEvent(type, payload) {
6949
- const event = {
6950
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6951
- type,
6952
- payload
6953
- };
6954
- this.historyEvents.push(event);
6955
- return event;
6956
- }
6957
- handleRateLimits(rateLimits) {
6958
- const snapshot = extractRateLimitsSnapshot(rateLimits);
6959
- if (snapshot) {
6960
- this.emitQuotaStatus(snapshot);
7229
+ if (event.type !== "event_msg") return;
7230
+ const msg = event.payload.msg;
7231
+ if (!msg) return;
7232
+ const itemType = msg.payload?.item?.type;
7233
+ if (itemType !== "context_compaction") return;
7234
+ if (msg.type === "item_started") {
7235
+ this.setCompacting(true);
7236
+ } else if (msg.type === "item_completed") {
7237
+ this.setCompacting(false);
6961
7238
  }
6962
7239
  }
6963
- async refreshQuotaSnapshot(host) {
6964
- try {
6965
- const response = await host.client.request(
6966
- ACCOUNT_RATE_LIMITS_READ_METHOD,
6967
- void 0
6968
- );
6969
- this.handleRateLimits(response.rateLimits);
6970
- } catch {
7240
+ async startSessionTail(threadId) {
7241
+ const sessionFile = await this.waitForSessionFile(threadId);
7242
+ if (!sessionFile) {
7243
+ return async () => {
7244
+ };
6971
7245
  }
6972
- }
6973
- emitQuotaStatus(snapshot, force = false) {
6974
- const event = this.quotaStatus.apply(snapshot, force);
6975
- if (!event) return;
6976
- this.historyEvents.push(event);
6977
- this.onEvent(event);
6978
- }
6979
- emitCodexTokenUsage(tokenUsage, model) {
6980
- const payload = buildCodexTokenUsageContextUsagePayload({
6981
- model,
6982
- modelContextWindow: tokenUsage.modelContextWindow,
6983
- last: {
6984
- inputTokens: tokenUsage.last.inputTokens,
6985
- outputTokens: tokenUsage.last.outputTokens,
6986
- totalTokens: tokenUsage.last.totalTokens,
6987
- cachedInputTokens: tokenUsage.last.cachedInputTokens,
6988
- reasoningOutputTokens: tokenUsage.last.reasoningOutputTokens
6989
- },
6990
- total: {
6991
- totalTokens: tokenUsage.total.totalTokens
6992
- },
6993
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
6994
- });
6995
- const event = this.emitContextUsage(payload);
6996
- this.historyEvents.push(event);
7246
+ let active = true;
7247
+ const seenLines = /* @__PURE__ */ new Set();
7248
+ const seedSeenLines = async () => {
7249
+ try {
7250
+ const content = await readFile7(sessionFile, "utf-8");
7251
+ const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
7252
+ let latest = null;
7253
+ for (const line of lines) {
7254
+ seenLines.add(line);
7255
+ try {
7256
+ const parsed = JSON.parse(line);
7257
+ const snapshot = extractCodexRateLimitsSnapshotFromJsonl(parsed);
7258
+ if (snapshot) latest = snapshot;
7259
+ } catch {
7260
+ }
7261
+ }
7262
+ if (latest) {
7263
+ this.quotaStatus.prime(latest);
7264
+ }
7265
+ } catch {
7266
+ }
7267
+ };
7268
+ await seedSeenLines();
7269
+ const pump = async () => {
7270
+ let emitted = 0;
7271
+ try {
7272
+ const content = await readFile7(sessionFile, "utf-8");
7273
+ const lines = content.split("\n");
7274
+ const completeLines = content.endsWith("\n") ? lines : lines.slice(0, -1);
7275
+ for (const line of completeLines) {
7276
+ const trimmed = line.trim();
7277
+ if (!trimmed || seenLines.has(trimmed)) {
7278
+ continue;
7279
+ }
7280
+ seenLines.add(trimmed);
7281
+ try {
7282
+ const parsed = JSON.parse(trimmed);
7283
+ const snapshot = extractCodexRateLimitsSnapshotFromJsonl(parsed);
7284
+ if (snapshot) {
7285
+ this.emitQuotaStatus(snapshot);
7286
+ }
7287
+ if (isJsonlEvent2(parsed)) {
7288
+ this.trackNativeCompaction(parsed);
7289
+ this.onEvent(parsed);
7290
+ emitted += 1;
7291
+ }
7292
+ } catch {
7293
+ }
7294
+ }
7295
+ } catch {
7296
+ }
7297
+ return emitted;
7298
+ };
7299
+ const loop = (async () => {
7300
+ while (active) {
7301
+ await pump();
7302
+ await sleep(100);
7303
+ }
7304
+ await pump();
7305
+ })();
7306
+ return async () => {
7307
+ active = false;
7308
+ await loop;
7309
+ const deadline = Date.now() + 1500;
7310
+ while (Date.now() < deadline) {
7311
+ const emitted = await pump();
7312
+ if (emitted > 0) {
7313
+ continue;
7314
+ }
7315
+ await sleep(100);
7316
+ }
7317
+ };
6997
7318
  }
6998
7319
  };
6999
7320
 
@@ -7610,18 +7931,58 @@ var RELAY_HISTORY_DIR = join15(ENGINE_DIR2, "relay-histories");
7610
7931
  var CHAT_SENDERS_DIR = join15(ENGINE_DIR2, "chat-senders");
7611
7932
  var CODEX_AUTH_PATH2 = join15(homedir13(), ".codex", "auth.json");
7612
7933
  function isChatMessageSender(value) {
7613
- if (!isRecord3(value)) return false;
7934
+ if (!isRecord4(value)) return false;
7614
7935
  return typeof value.senderUserId === "string" && typeof value.senderEmail === "string" && typeof value.recordedAt === "string";
7615
7936
  }
7616
7937
  function isCodexAvailable() {
7617
7938
  return existsSync7(CODEX_AUTH_PATH2) || Boolean(ENGINE_ENV.OPENAI_API_KEY);
7618
7939
  }
7940
+ function isSameAcceptedUserEvent(event, acceptedEvent) {
7941
+ if (areSameUserMessageEvents(event, acceptedEvent)) return true;
7942
+ const eventMessage = getUserMessage(event);
7943
+ const acceptedMessage = getUserMessage(acceptedEvent);
7944
+ return Boolean(eventMessage) && eventMessage === acceptedMessage && Math.abs(getEventTimestampMs(event) - getEventTimestampMs(acceptedEvent)) <= 3e4;
7945
+ }
7946
+ function getCodexTranscriptUserMessages(transcript) {
7947
+ if (!transcript) return [];
7948
+ const messages = [];
7949
+ for (const turn of transcript.turns) {
7950
+ for (const item of turn.items) {
7951
+ if (item.type === "userMessage" && item.content.trim()) {
7952
+ messages.push(item.content);
7953
+ }
7954
+ }
7955
+ }
7956
+ return messages;
7957
+ }
7958
+ function getCodexTranscriptFromEvent(event) {
7959
+ if (event.type !== CODEX_ASP_TRANSCRIPT_UPDATED_EVENT_TYPE) return null;
7960
+ const transcript = event.payload.transcript;
7961
+ return isCodexAspTranscript(transcript) ? transcript : null;
7962
+ }
7963
+ function acceptedEventInCodexTranscript(acceptedEvent, transcript) {
7964
+ const message = getUserMessage(acceptedEvent);
7965
+ if (!message) return false;
7966
+ return getCodexTranscriptUserMessages(transcript).includes(message);
7967
+ }
7619
7968
  function isPersistedChat(value) {
7620
- if (!isRecord3(value)) {
7969
+ if (!isRecord4(value)) {
7621
7970
  return false;
7622
7971
  }
7623
7972
  const candidate = value;
7624
- return typeof candidate.id === "string" && (candidate.provider === "claude" || candidate.provider === "codex" || candidate.provider === "relay") && typeof candidate.title === "string" && typeof candidate.createdAt === "string" && typeof candidate.updatedAt === "string" && (candidate.providerSessionId === null || typeof candidate.providerSessionId === "string") && (candidate.parentChatId === void 0 || candidate.parentChatId === null || typeof candidate.parentChatId === "string");
7973
+ return typeof candidate.id === "string" && (candidate.provider === "claude" || candidate.provider === "codex" || candidate.provider === "relay") && typeof candidate.title === "string" && typeof candidate.createdAt === "string" && typeof candidate.updatedAt === "string" && (candidate.providerSessionId === null || typeof candidate.providerSessionId === "string") && (candidate.parentChatId === void 0 || candidate.parentChatId === null || typeof candidate.parentChatId === "string") && (candidate.codexBackend === void 0 || candidate.codexBackend === "sdk" || candidate.codexBackend === "asp");
7974
+ }
7975
+ function codexBackendForChat(chat) {
7976
+ if (chat.provider !== "codex") return "asp";
7977
+ if (chat.codexBackend) return chat.codexBackend;
7978
+ return chat.providerSessionId ? "sdk" : "asp";
7979
+ }
7980
+ function normalizePersistedChat(chat) {
7981
+ return {
7982
+ ...chat,
7983
+ parentChatId: chat.parentChatId ?? null,
7984
+ ...chat.provider === "codex" ? { codexBackend: codexBackendForChat(chat) } : {}
7985
+ };
7625
7986
  }
7626
7987
  function createUserMessageEvent(message, messageId) {
7627
7988
  return {
@@ -7635,7 +7996,6 @@ function createUserMessageEvent(message, messageId) {
7635
7996
  }
7636
7997
  };
7637
7998
  }
7638
- var isSameUserMessageEvent = areSameUserMessageEvents;
7639
7999
  var ChatService = class {
7640
8000
  constructor(workingDirectory) {
7641
8001
  this.workingDirectory = workingDirectory;
@@ -7699,7 +8059,8 @@ var ChatService = class {
7699
8059
  createdAt: now,
7700
8060
  updatedAt: now,
7701
8061
  providerSessionId: null,
7702
- parentChatId
8062
+ parentChatId,
8063
+ ...request.provider === "codex" ? { codexBackend: "asp" } : {}
7703
8064
  };
7704
8065
  const runtime = this.createRuntimeChat(persisted);
7705
8066
  this.chats.set(persisted.id, runtime);
@@ -7796,6 +8157,37 @@ var ChatService = class {
7796
8157
  });
7797
8158
  return result;
7798
8159
  }
8160
+ async clearGoal(chatId) {
8161
+ const chat = this.requireChat(chatId);
8162
+ if (!chat.provider.clearGoal) {
8163
+ return { interrupted: false, queue: [], goal: null };
8164
+ }
8165
+ const interruptResult = await chat.provider.interrupt();
8166
+ chat.hasActiveTurn = false;
8167
+ chat.activeMessageId = null;
8168
+ keepAliveService.stop();
8169
+ chat.pendingMessageIds = [];
8170
+ chat.acceptedUserEvents.clear();
8171
+ const goal = await chat.provider.clearGoal();
8172
+ this.touch(chat);
8173
+ await this.publish({
8174
+ type: "chat.interrupted",
8175
+ payload: {
8176
+ chatId,
8177
+ interrupted: interruptResult.interrupted,
8178
+ queue: interruptResult.queue
8179
+ }
8180
+ });
8181
+ await this.publish({
8182
+ type: "chat.updated",
8183
+ payload: { chat: this.toSummary(chat) }
8184
+ });
8185
+ return {
8186
+ interrupted: interruptResult.interrupted,
8187
+ queue: interruptResult.queue,
8188
+ goal
8189
+ };
8190
+ }
7799
8191
  getChatQueue(chatId) {
7800
8192
  const chat = this.requireChat(chatId);
7801
8193
  return {
@@ -7874,10 +8266,17 @@ var ChatService = class {
7874
8266
  chat.provider.getHistory(),
7875
8267
  this.readSenders(chatId)
7876
8268
  ]);
7877
- const acceptedEvents = [...chat.acceptedUserEvents.values()].filter((acceptedEvent) => !history.events.some((event) => isSameUserMessageEvent(event, acceptedEvent)));
8269
+ for (const [messageId, acceptedEvent] of chat.acceptedUserEvents) {
8270
+ if (acceptedEventInCodexTranscript(acceptedEvent, history.codexAspTranscript)) {
8271
+ chat.acceptedUserEvents.delete(messageId);
8272
+ }
8273
+ }
8274
+ const acceptedEvents = [...chat.acceptedUserEvents.values()].filter((acceptedEvent) => !acceptedEventInCodexTranscript(acceptedEvent, history.codexAspTranscript) && !history.events.some((event) => isSameAcceptedUserEvent(event, acceptedEvent)));
8275
+ const events = [...history.events, ...acceptedEvents].sort((a, b) => getEventTimestampMs(a) - getEventTimestampMs(b));
7878
8276
  return {
7879
8277
  thread_id: history.thread_id,
7880
- events: [...history.events, ...acceptedEvents],
8278
+ events,
8279
+ codexAspTranscript: history.codexAspTranscript ?? null,
7881
8280
  goal: history.goal ?? chat.provider.getGoal?.() ?? null,
7882
8281
  senders
7883
8282
  };
@@ -7932,7 +8331,7 @@ var ChatService = class {
7932
8331
  codexAvailable: isCodexAvailable()
7933
8332
  });
7934
8333
  } else {
7935
- const CodexProviderCtor = ENGINE_ENV.CODEX_ASP_ENABLED ? CodexAspManager : CodexManager;
8334
+ const CodexProviderCtor = codexBackendForChat(persisted) === "sdk" ? CodexManager : CodexAspManager;
7936
8335
  provider = new CodexProviderCtor({
7937
8336
  workingDirectory: this.workingDirectory,
7938
8337
  initialSessionId: persisted.providerSessionId,
@@ -8013,12 +8412,20 @@ var ChatService = class {
8013
8412
  };
8014
8413
  }
8015
8414
  for (const [messageId, acceptedEvent] of chat.acceptedUserEvents) {
8016
- if (isSameUserMessageEvent(event, acceptedEvent)) {
8415
+ if (isSameAcceptedUserEvent(event, acceptedEvent)) {
8017
8416
  chat.acceptedUserEvents.delete(messageId);
8018
8417
  break;
8019
8418
  }
8020
8419
  }
8021
8420
  }
8421
+ const codexTranscript = getCodexTranscriptFromEvent(event);
8422
+ if (codexTranscript) {
8423
+ for (const [messageId, acceptedEvent] of chat.acceptedUserEvents) {
8424
+ if (acceptedEventInCodexTranscript(acceptedEvent, codexTranscript)) {
8425
+ chat.acceptedUserEvents.delete(messageId);
8426
+ }
8427
+ }
8428
+ }
8022
8429
  this.touch(chat);
8023
8430
  this.observeCurrentBranches(chat).catch(() => {
8024
8431
  });
@@ -8072,7 +8479,7 @@ var ChatService = class {
8072
8479
  if (!Array.isArray(parsed)) {
8073
8480
  return [];
8074
8481
  }
8075
- return parsed.filter((entry) => isPersistedChat(entry));
8482
+ return parsed.filter((entry) => isPersistedChat(entry)).map((entry) => normalizePersistedChat(entry));
8076
8483
  } catch (error) {
8077
8484
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
8078
8485
  return [];
@@ -9067,6 +9474,17 @@ function createV1Routes(deps) {
9067
9474
  return c.json(jsonError("Failed to interrupt chat", error instanceof Error ? error.message : "Unknown error"), 404);
9068
9475
  }
9069
9476
  });
9477
+ app2.post("/chats/:chatId/goal/clear", async (c) => {
9478
+ try {
9479
+ const result = await deps.chatService.clearGoal(c.req.param("chatId"));
9480
+ return c.json(result);
9481
+ } catch (error) {
9482
+ if (error instanceof ChatNotFoundError) {
9483
+ return c.json(jsonError("Failed to clear goal", error.message), 404);
9484
+ }
9485
+ return c.json(jsonError("Failed to clear goal", error instanceof Error ? error.message : "Unknown error"), 404);
9486
+ }
9487
+ });
9070
9488
  app2.get("/chats/:chatId/queue", (c) => {
9071
9489
  try {
9072
9490
  const result = deps.chatService.getChatQueue(c.req.param("chatId"));