@keyou007/agent-bot 0.1.20-alpha.1 → 0.1.21

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 (92) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +27 -4
  3. package/README.zh.md +23 -4
  4. package/dist/cli/AgentPrerequisites.js +11 -47
  5. package/dist/cli/AgentPrerequisites.js.map +1 -1
  6. package/dist/cli/DefaultAgentSelection.js +1 -1
  7. package/dist/cli/DefaultAgentSelection.js.map +1 -1
  8. package/dist/cli.js +15 -0
  9. package/dist/cli.js.map +1 -1
  10. package/dist/codex/CodexEventMapper.js +21 -1
  11. package/dist/codex/CodexEventMapper.js.map +1 -1
  12. package/dist/codex/CodexLocalActivityDetector.js +32 -10
  13. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  14. package/dist/codex/CodexProcessManager.js +56 -7
  15. package/dist/codex/CodexProcessManager.js.map +1 -1
  16. package/dist/codex/CodexRuntime.js +361 -168
  17. package/dist/codex/CodexRuntime.js.map +1 -1
  18. package/dist/codex/CodexVersion.js +18 -0
  19. package/dist/codex/CodexVersion.js.map +1 -0
  20. package/dist/codex/ProviderModels.js +159 -0
  21. package/dist/codex/ProviderModels.js.map +1 -0
  22. package/dist/codex/ProviderSwitch.js +32 -0
  23. package/dist/codex/ProviderSwitch.js.map +1 -0
  24. package/dist/commands/CommandRouter.js +8 -3
  25. package/dist/commands/CommandRouter.js.map +1 -1
  26. package/dist/console/ConsoleTurnPresenter.js +2 -1
  27. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  28. package/dist/feishu/CardRenderer.js +82 -27
  29. package/dist/feishu/CardRenderer.js.map +1 -1
  30. package/dist/feishu/FeishuConnector.js +17 -5
  31. package/dist/feishu/FeishuConnector.js.map +1 -1
  32. package/dist/feishu/FeishuTurnPresenter.js +34 -8
  33. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  34. package/dist/feishu/InboundText.js +11 -0
  35. package/dist/feishu/InboundText.js.map +1 -1
  36. package/dist/feishu/MergedForwardMessage.js +17 -5
  37. package/dist/feishu/MergedForwardMessage.js.map +1 -1
  38. package/dist/index.js +3 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/local-files/LocalFileViewerServer.js +164 -19
  41. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  42. package/dist/local-files/MarkdownFileLinks.js +65 -0
  43. package/dist/local-files/MarkdownFileLinks.js.map +1 -0
  44. package/dist/local-files/TurnPreviewPage.js +725 -0
  45. package/dist/local-files/TurnPreviewPage.js.map +1 -0
  46. package/dist/presentation/OutboundRouter.js +6 -1
  47. package/dist/presentation/OutboundRouter.js.map +1 -1
  48. package/dist/presentation/TurnStateReducer.js +142 -3
  49. package/dist/presentation/TurnStateReducer.js.map +1 -1
  50. package/dist/proxy/ProxySessionController.js +573 -325
  51. package/dist/proxy/ProxySessionController.js.map +1 -1
  52. package/dist/state/StateStore.js +65 -65
  53. package/dist/state/StateStore.js.map +1 -1
  54. package/dist/utils/formatStorageSize.js +15 -0
  55. package/dist/utils/formatStorageSize.js.map +1 -0
  56. package/dist/utils/semanticVersion.js +39 -0
  57. package/dist/utils/semanticVersion.js.map +1 -0
  58. package/docs/technical-reference.md +11 -3
  59. package/docs/technical-reference.zh.md +11 -3
  60. package/npm-shrinkwrap.json +2 -2
  61. package/package.json +1 -1
  62. package/skills/agent-bot/SKILL.md +11 -1
  63. package/src/cli/AgentPrerequisites.ts +14 -40
  64. package/src/cli/DefaultAgentSelection.ts +1 -1
  65. package/src/cli.ts +13 -0
  66. package/src/codex/CodexEventMapper.ts +31 -1
  67. package/src/codex/CodexLocalActivityDetector.ts +41 -9
  68. package/src/codex/CodexProcessManager.ts +48 -7
  69. package/src/codex/CodexRuntime.ts +397 -204
  70. package/src/codex/CodexVersion.ts +18 -0
  71. package/src/codex/ProviderModels.ts +185 -0
  72. package/src/codex/ProviderSwitch.ts +36 -0
  73. package/src/commands/CommandRouter.ts +7 -2
  74. package/src/commands/commandTypes.ts +1 -1
  75. package/src/console/ConsoleTurnPresenter.ts +3 -0
  76. package/src/feishu/CardRenderer.ts +84 -43
  77. package/src/feishu/FeishuConnector.ts +15 -5
  78. package/src/feishu/FeishuTurnPresenter.ts +46 -4
  79. package/src/feishu/InboundText.ts +11 -0
  80. package/src/feishu/MergedForwardMessage.ts +15 -5
  81. package/src/index.ts +3 -1
  82. package/src/local-files/LocalFileViewerServer.ts +179 -20
  83. package/src/local-files/MarkdownFileLinks.ts +64 -0
  84. package/src/local-files/TurnPreviewPage.ts +792 -0
  85. package/src/presentation/OutboundRouter.ts +8 -0
  86. package/src/presentation/TurnStateReducer.ts +175 -2
  87. package/src/presentation/turnViewTypes.ts +13 -0
  88. package/src/proxy/ProxySessionController.ts +547 -297
  89. package/src/runtime/types.ts +33 -3
  90. package/src/state/StateStore.ts +86 -90
  91. package/src/utils/formatStorageSize.ts +13 -0
  92. package/src/utils/semanticVersion.ts +28 -0
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@keyou007/agent-bot",
3
- "version": "0.1.20-alpha.1",
3
+ "version": "0.1.21",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@keyou007/agent-bot",
9
- "version": "0.1.20-alpha.1",
9
+ "version": "0.1.21",
10
10
  "license": "GPL-3.0-only",
11
11
  "dependencies": {
12
12
  "@agentclientprotocol/sdk": "1.2.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyou007/agent-bot",
3
- "version": "0.1.20-alpha.1",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "description": "Use Codex, TraeX, and compatible coding agents from Lark/Feishu.",
6
6
  "license": "GPL-3.0-only",
@@ -96,6 +96,10 @@ agentbot task forkgroup [title]
96
96
 
97
97
  Choose `new` or `newgroup` for fresh context. Choose `fork` or `forkgroup` when the new task must retain conversation history through the latest completed Turn. Forking must not interrupt an active source turn.
98
98
 
99
+ Feishu `/sessions [keyword]` lists and searches tasks across projects and all Providers, not just the App Server's current Provider. Switching Provider does not hide existing tasks; listing still uses paged metadata and the latest Turn summary only.
100
+
101
+ Fork creation stores source-task and branch-Turn references without synchronizing the full local Turn list. The Feishu Turn card reuses local records and loads only the summary pages needed for the requested page, not the entire history. `task turns` loads the first page if necessary and returns available local records. This does not change the Agent's inherited context. Retry listing Turns after a temporary history-read failure; unsupported summary pagination never falls back to downloading full history.
102
+
99
103
  ## Change Settings
100
104
 
101
105
  ```powershell
@@ -108,6 +112,8 @@ agentbot task permissions [auto|confirm]
108
112
 
109
113
  Omit the value to inspect the current setting and available choices. `agent` changes the default Agent for future tasks in that conversation. The other settings affect the specified task from its next request and become the saved defaults for that Agent.
110
114
 
115
+ Provider changes require an idle task and verify the actual Provider and model before saving. Custom Providers discover models through their OpenAI-compatible `/models` endpoint when available. Switching keeps the previous model when supported, otherwise uses the Provider default or its first model. A missing model-list endpoint falls back to the current or configured model and does not by itself block switching. Wait for active work to finish; do not stop it without the user's request. Switching unloads only the selected thread, not the shared App Server. On failure, previous settings are retained and recovery is attempted. If recovery fails, resolve the reported configuration or ownership issue and retry the Provider switch before sending another prompt. Never replace a forked task with an empty task to work around a switch failure.
116
+
111
117
  ## Goals And Turns
112
118
 
113
119
  ```powershell
@@ -119,7 +125,9 @@ agentbot task turns
119
125
  agentbot task reset <turn-id>
120
126
  ```
121
127
 
122
- Use `turns` to obtain a real Turn ID before `reset`. Reset changes conversation context only; it does not revert local files.
128
+ In Feishu, `/turn` (also `/turns`) opens history. `/turn <Turn ID>` or `/turn <index>` shows saved runtime details for the current task without resetting, resuming, or interrupting it. Indices match the history card: one-based, newest first, including a running Turn at the top. Missing records use only necessary summary pages; Fork queries stay within the branch anchor. Summary-only external Turns do not contain unrecorded tool output. This does not change the CLI's `task turns [task]` task-reference argument.
129
+
130
+ Use `turns` to obtain a real Turn ID before `reset`. Reset changes conversation context only; it does not revert local files. Agent Bot announces an interactive Reset when it starts and queues every new message after its Reaction until the replacement thread is ready.
123
131
 
124
132
  ## Files And Local Commands
125
133
 
@@ -168,6 +176,8 @@ agentbot --profile ~/.agent-bot-rescue server status
168
176
 
169
177
  Initialization prepares configuration, checks supported Agents, configures Feishu, and starts the server. On first initialization and `init --reset`, let the user choose whether all group messages are accepted or an explicit @ mention is required; only the first choice requests the additional all-group-message permission. Relay authorization links and wait for the user; never choose a response mode, skip authorization, or choose maintenance actions without permission.
170
178
 
179
+ Codex requires version 0.153.4 or later. Init checks this locally, and runtime startup validates the actual App Server version. For an unsupported version, show the upgrade instruction (`codex update` or `npm install -g @openai/codex@latest`); do not work around it by reading full history through legacy APIs. Safely restart Agent Bot after an authorized upgrade. This version requirement does not apply to TraeX or ACP Agents.
180
+
171
181
  Use `init --reset` only for an explicitly requested full reset. Without `--profile` it resets the default Profile; pass `--profile <directory>` to reset another Profile. It preserves backups under `.reset-backups`.
172
182
 
173
183
  ## After Code Changes
@@ -1,4 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { codexVersionIssue, MINIMUM_CODEX_VERSION } from "../codex/CodexVersion.js";
3
+ import { compareSemanticVersions } from "../utils/semanticVersion.js";
4
+ export { compareSemanticVersions } from "../utils/semanticVersion.js";
2
5
 
3
6
  export type SupportedAgentId = "codex" | "traex";
4
7
  export type SupportedAgentState = "missing" | "outdated" | "ready";
@@ -29,6 +32,8 @@ export interface SupportedAgentInspection {
29
32
  installedVersion?: string;
30
33
  latestVersion?: string;
31
34
  latestCheckFailed?: boolean;
35
+ minimumVersion?: string;
36
+ compatibilityIssue?: string;
32
37
  action?: {
33
38
  kind: AgentMaintenanceKind;
34
39
  command: string;
@@ -100,6 +105,15 @@ async function inspectCodex(
100
105
  const installedVersion = commandVersion(installed);
101
106
  if (!installedVersion) return missingInspection("codex", "Codex", platform, env);
102
107
 
108
+ const compatibilityIssue = codexVersionIssue(installedVersion);
109
+ if (compatibilityIssue) {
110
+ return {
111
+ id: "codex", name: "Codex", state: "outdated", installedVersion,
112
+ minimumVersion: MINIMUM_CODEX_VERSION, compatibilityIssue,
113
+ action: maintenanceAction("codex", "upgrade", platform, env),
114
+ };
115
+ }
116
+
103
117
  const latest = await run(capturedCommand(
104
118
  "npm",
105
119
  ["view", CODEX_PACKAGE, "version", "--json"],
@@ -294,46 +308,6 @@ function versionInText(value: string): string | undefined {
294
308
  return /\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\b/u.exec(value)?.[1];
295
309
  }
296
310
 
297
- export function compareSemanticVersions(left: string, right: string): number {
298
- const leftVersion = parseSemanticVersion(left);
299
- const rightVersion = parseSemanticVersion(right);
300
- if (!leftVersion || !rightVersion) return 0;
301
- for (let index = 0; index < 3; index += 1) {
302
- const difference = leftVersion.core[index]! - rightVersion.core[index]!;
303
- if (difference !== 0) return difference;
304
- }
305
- if (!leftVersion.prerelease && !rightVersion.prerelease) return 0;
306
- if (!leftVersion.prerelease) return 1;
307
- if (!rightVersion.prerelease) return -1;
308
- const length = Math.max(leftVersion.prerelease.length, rightVersion.prerelease.length);
309
- for (let index = 0; index < length; index += 1) {
310
- const leftPart = leftVersion.prerelease[index];
311
- const rightPart = rightVersion.prerelease[index];
312
- if (leftPart === undefined) return -1;
313
- if (rightPart === undefined) return 1;
314
- if (leftPart === rightPart) continue;
315
- const leftNumber = /^\d+$/u.test(leftPart) ? Number(leftPart) : undefined;
316
- const rightNumber = /^\d+$/u.test(rightPart) ? Number(rightPart) : undefined;
317
- if (leftNumber !== undefined && rightNumber !== undefined) return leftNumber - rightNumber;
318
- if (leftNumber !== undefined) return -1;
319
- if (rightNumber !== undefined) return 1;
320
- return leftPart.localeCompare(rightPart);
321
- }
322
- return 0;
323
- }
324
-
325
- function parseSemanticVersion(value: string): {
326
- core: [number, number, number];
327
- prerelease?: string[];
328
- } | undefined {
329
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
330
- if (!match) return undefined;
331
- return {
332
- core: [Number(match[1]), Number(match[2]), Number(match[3])],
333
- ...(match[4] ? { prerelease: match[4].split(".") } : {}),
334
- };
335
- }
336
-
337
311
  export function runAgentCommand(command: AgentCommandSpec): Promise<AgentCommandResult> {
338
312
  return new Promise((resolve) => {
339
313
  const invocation = resolveInvocation(command);
@@ -12,7 +12,7 @@ export function selectableDefaultAgents(
12
12
  const supported = new Map(inspections.map((inspection) => [inspection.id, inspection]));
13
13
  return configured.flatMap((agent) => {
14
14
  const inspection = supported.get(agent.name as SupportedAgentInspection["id"]);
15
- if (!inspection?.installedVersion) return [];
15
+ if (!inspection?.installedVersion || inspection.compatibilityIssue) return [];
16
16
  return [{
17
17
  ...agent,
18
18
  installedVersion: inspection.installedVersion,
package/src/cli.ts CHANGED
@@ -338,6 +338,9 @@ async function initCommand(
338
338
  );
339
339
  const configuredAgents = readConfiguredAgentSelection(paths.config.path).agents.map((agent) => agent.name);
340
340
  const configuredAgentNames = new Set(configuredAgents);
341
+ for (const agent of inspectedAgents) {
342
+ if (configuredAgentNames.has(agent.id) && agent.compatibilityIssue) throw new Error(agent.compatibilityIssue);
343
+ }
341
344
  const agents = inspectedAgents.map((agent) => ({
342
345
  ...agent,
343
346
  configured: configuredAgentNames.has(agent.id),
@@ -554,6 +557,8 @@ async function configureAgentsAndDefault(
554
557
 
555
558
  const choices = selectableDefaultAgents(configured.agents, inspections);
556
559
  if (choices.length === 0) {
560
+ const issue = inspections.find((inspection) => inspection.compatibilityIssue)?.compatibilityIssue;
561
+ if (issue) throw new Error(issue);
557
562
  throw new Error(cliText(
558
563
  "No installed supported Agent was detected. Install Codex or TraeX, then run agentbot init again.",
559
564
  "没有检测到已安装且受支持的 Agent。请安装 Codex 或 TraeX,然后重新运行 agentbot init。",
@@ -679,6 +684,10 @@ function printInteractiveAgentInspection(
679
684
  inspection: SupportedAgentInspection,
680
685
  ui: InitUi,
681
686
  ): void {
687
+ if (inspection.compatibilityIssue) {
688
+ ui.warn(inspection.compatibilityIssue);
689
+ return;
690
+ }
682
691
  if (inspection.state === "missing") {
683
692
  ui.info(cliText(
684
693
  `${inspection.name}: not installed`,
@@ -710,6 +719,10 @@ function printAgentInspection(
710
719
  inspection: SupportedAgentInspection,
711
720
  output: NodeJS.WriteStream,
712
721
  ): void {
722
+ if (inspection.compatibilityIssue) {
723
+ output.write(` ${inspection.compatibilityIssue}\n`);
724
+ return;
725
+ }
713
726
  if (inspection.state === "missing") {
714
727
  output.write(cliText(
715
728
  ` ${inspection.name}: not installed\n`,
@@ -8,6 +8,15 @@ export type MappedCodexNotification =
8
8
  turnId: string;
9
9
  lastTokens: number;
10
10
  cumulativeTokens: number;
11
+ contextTokens?: number;
12
+ }
13
+ | {
14
+ kind: "context_compaction";
15
+ threadId: string;
16
+ turnId: string;
17
+ phase: "started" | "completed";
18
+ compactionId?: string;
19
+ timestampMs?: number;
11
20
  }
12
21
  | { kind: "agent_delta"; threadId: string; turnId: string; itemId: string; text: string }
13
22
  | {
@@ -61,7 +70,18 @@ export function mapCodexNotification(method: string, params: unknown): MappedCod
61
70
  const lastTokens = effectiveTokenCount(params.tokenUsage.last);
62
71
  const cumulativeTokens = effectiveTokenCount(params.tokenUsage.total);
63
72
  if (lastTokens === undefined || cumulativeTokens === undefined) return undefined;
64
- return { kind: "token_usage", threadId, turnId, lastTokens, cumulativeTokens };
73
+ return {
74
+ kind: "token_usage",
75
+ threadId,
76
+ turnId,
77
+ lastTokens,
78
+ cumulativeTokens,
79
+ contextTokens: numberValue(params.tokenUsage.last.totalTokens),
80
+ };
81
+ }
82
+
83
+ if (method === "thread/compacted") {
84
+ return { kind: "context_compaction", threadId, turnId, phase: "completed" };
65
85
  }
66
86
 
67
87
  if (method === "item/agentMessage/delta") {
@@ -98,6 +118,16 @@ export function mapCodexNotification(method: string, params: unknown): MappedCod
98
118
  return { kind: "plan", threadId, turnId, steps };
99
119
  }
100
120
  if ((method === "item/started" || method === "item/completed") && isRecord(params.item)) {
121
+ if (params.item.type === "contextCompaction") {
122
+ return {
123
+ kind: "context_compaction",
124
+ threadId,
125
+ turnId,
126
+ phase: method === "item/started" ? "started" : "completed",
127
+ compactionId: stringValue(params.item.id),
128
+ timestampMs: numberValue(method === "item/started" ? params.startedAtMs : params.completedAtMs),
129
+ };
130
+ }
101
131
  if (params.item.type === "agentMessage") {
102
132
  const itemId = stringValue(params.item.id);
103
133
  const phase = messagePhase(params.item.phase);
@@ -9,6 +9,16 @@ const TASK_COMPLETE = Buffer.from('"type":"task_complete"');
9
9
  const TURN_ABORTED = Buffer.from('"type":"turn_aborted"');
10
10
  const PATTERN_OVERLAP = Math.max(TASK_STARTED.length, TASK_COMPLETE.length, TURN_ABORTED.length) - 1;
11
11
 
12
+ interface RolloutState {
13
+ size: number;
14
+ mtimeMs: number;
15
+ ctimeMs: number;
16
+ ino: number;
17
+ dev: number;
18
+ tail: Buffer;
19
+ task: { active: boolean; turnId?: string };
20
+ }
21
+
12
22
  interface ThreadPathRow {
13
23
  id: string;
14
24
  rollout_path: string;
@@ -35,6 +45,7 @@ export interface CodexLocalThreadSettings {
35
45
  * even while Codex Desktop is actively appending events to its rollout.
36
46
  */
37
47
  export class CodexLocalActivityDetector {
48
+ private readonly rollouts = new Map<string, RolloutState>();
38
49
  constructor(private readonly codexHome: string) {}
39
50
 
40
51
  async activeThreadIds(threadIds: string[]): Promise<Set<string>> {
@@ -64,7 +75,7 @@ export class CodexLocalActivityDetector {
64
75
 
65
76
  const active = new Map<string, string | undefined>();
66
77
  for (const row of rows) {
67
- const task = await latestTask(row.rollout_path);
78
+ const task = await latestTask(row.rollout_path, this.rollouts);
68
79
  if (task.active) active.set(row.id, task.turnId);
69
80
  }
70
81
  return active;
@@ -104,16 +115,31 @@ export class CodexLocalActivityDetector {
104
115
  }
105
116
  }
106
117
 
107
- async function latestTask(rolloutPath: string): Promise<{ active: boolean; turnId?: string }> {
118
+ async function latestTask(rolloutPath: string, cache: Map<string, RolloutState>): Promise<{ active: boolean; turnId?: string }> {
108
119
  let file;
109
120
  try {
110
121
  file = await open(rolloutPath, "r");
111
- const size = (await file.stat()).size;
122
+ const { size, mtimeMs, ctimeMs, ino, dev } = await file.stat();
123
+ const previous = cache.get(rolloutPath);
124
+ const sameFile = previous?.ino === ino && previous.dev === dev;
125
+ if (sameFile && previous.size === size && previous.mtimeMs === mtimeMs && previous.ctimeMs === ctimeMs) {
126
+ return previous.task;
127
+ }
128
+ let stopAt = 0;
129
+ let task: RolloutState["task"] = { active: false };
130
+ if (sameFile && size > previous.size) {
131
+ const boundary = Buffer.alloc(previous.tail.length);
132
+ await file.read(boundary, 0, boundary.length, previous.size - boundary.length);
133
+ if (boundary.equals(previous.tail)) {
134
+ stopAt = Math.max(0, previous.size - PATTERN_OVERLAP);
135
+ task = previous.task;
136
+ }
137
+ }
112
138
  let position = size;
113
139
  let laterPrefix = Buffer.alloc(0);
114
140
 
115
- while (position > 0) {
116
- const length = Math.min(READ_CHUNK_SIZE, position);
141
+ while (position > stopAt) {
142
+ const length = Math.min(READ_CHUNK_SIZE, position - stopAt);
117
143
  position -= length;
118
144
  const chunk = Buffer.allocUnsafe(length);
119
145
  await file.read(chunk, 0, length, position);
@@ -123,14 +149,20 @@ async function latestTask(rolloutPath: string): Promise<{ active: boolean; turnI
123
149
  const abortedAt = searchable.lastIndexOf(TURN_ABORTED);
124
150
  const terminalAt = Math.max(completedAt, abortedAt);
125
151
  if (startedAt >= 0 || terminalAt >= 0) {
126
- if (startedAt <= terminalAt) return { active: false };
127
- const absoluteOffset = position + startedAt;
128
- return { active: true, turnId: await readTurnId(file, absoluteOffset, size) };
152
+ task = startedAt <= terminalAt ? { active: false }
153
+ : { active: true, turnId: await readTurnId(file, position + startedAt, size) };
154
+ break;
129
155
  }
130
156
  laterPrefix = chunk.subarray(0, Math.min(PATTERN_OVERLAP, chunk.length));
131
157
  }
132
- return { active: false };
158
+ const tail = Buffer.alloc(Math.min(1_024, size));
159
+ await file.read(tail, 0, tail.length, size - tail.length);
160
+ cache.delete(rolloutPath);
161
+ cache.set(rolloutPath, { size, mtimeMs, ctimeMs, ino, dev, tail, task });
162
+ if (cache.size > 512) cache.delete(cache.keys().next().value!);
163
+ return task;
133
164
  } catch {
165
+ cache.delete(rolloutPath);
134
166
  return { active: false };
135
167
  } finally {
136
168
  await file?.close();
@@ -11,12 +11,15 @@ import {
11
11
  } from "../runtime/agentEnvironment.js";
12
12
  import type { AgentProcessInfo } from "../runtime/types.js";
13
13
  import { spawnStdioCommand } from "../utils/spawnCommand.js";
14
+ import { assertSupportedCodexVersion } from "./CodexVersion.js";
14
15
 
15
16
  export class CodexProcessManager implements AppServerClientProvider {
16
17
  private static readonly RELEASE_TIMEOUT_MS = 5_000;
17
18
  private client?: AppServerConnection;
18
19
  private child?: ChildProcessWithoutNullStreams;
19
20
  private version?: string;
21
+ private agentFamily?: "codex" | "traex";
22
+ private initializing?: Promise<AppServerClient>;
20
23
  private readonly disconnectListeners = new Set<(error: Error) => void>();
21
24
 
22
25
  constructor(
@@ -28,7 +31,18 @@ export class CodexProcessManager implements AppServerClientProvider {
28
31
  ) {}
29
32
 
30
33
  async getClient(): Promise<AppServerClient> {
34
+ if (this.initializing) return this.initializing;
31
35
  if (this.client) return this.client;
36
+ const initialization = this.startClient();
37
+ this.initializing = initialization;
38
+ try {
39
+ return await initialization;
40
+ } finally {
41
+ if (this.initializing === initialization) this.initializing = undefined;
42
+ }
43
+ }
44
+
45
+ private async startClient(): Promise<AppServerClient> {
32
46
  this.version = undefined;
33
47
  const environmentContext = this.environmentContext();
34
48
  const child = spawnStdioCommand(
@@ -52,13 +66,25 @@ export class CodexProcessManager implements AppServerClientProvider {
52
66
  const error = new Error(`App Server exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`);
53
67
  for (const listener of this.disconnectListeners) listener(error);
54
68
  });
55
- const initializeResult = await client.request("initialize", {
56
- clientInfo: { name: "agent-bot", title: "Agent Bot", version: "0.1.0" },
57
- capabilities: { experimentalApi: true },
58
- });
59
- this.version = initializedAgentVersion(initializeResult);
60
- client.notify("initialized", {});
61
- return client;
69
+ try {
70
+ const initializeResult = await client.request("initialize", {
71
+ clientInfo: { name: "agent-bot", title: "Agent Bot", version: "0.1.0" },
72
+ capabilities: { experimentalApi: true },
73
+ });
74
+ this.version = initializedAgentVersion(initializeResult);
75
+ const userAgent = initializeResult && typeof initializeResult === "object"
76
+ ? (initializeResult as Record<string, unknown>).userAgent : undefined;
77
+ if (!this.getAgentFamily() && typeof userAgent === "string") {
78
+ if (/^codex(?:-cli)?[ /]/iu.test(userAgent)) this.agentFamily = "codex";
79
+ else if (/^trae(?:cli|x)?[ /]/iu.test(userAgent)) this.agentFamily = "traex";
80
+ }
81
+ if (this.getAgentFamily() === "codex") assertSupportedCodexVersion(this.version);
82
+ client.notify("initialized", {});
83
+ return client;
84
+ } catch (error) {
85
+ if (this.client === client) this.close();
86
+ throw error;
87
+ }
62
88
  }
63
89
 
64
90
  getProcessInfo(): AgentProcessInfo {
@@ -70,10 +96,25 @@ export class CodexProcessManager implements AppServerClientProvider {
70
96
  };
71
97
  }
72
98
 
99
+ getAgentFamily(): "codex" | "traex" | undefined {
100
+ if (this.agentFamily) return this.agentFamily;
101
+ const candidates = [this.command, this.args[0], this.environmentContext().agentName];
102
+ for (const value of candidates) {
103
+ const executable = value?.replaceAll("\\", "/").split("/").at(-1) ?? "";
104
+ if (/^codex(?:\.(?:exe|cmd|bat|m?js))?$/iu.test(executable)) return "codex";
105
+ if (/^(?:traex|traecli)(?:\.(?:exe|cmd|bat|m?js))?$/iu.test(executable)) return "traex";
106
+ }
107
+ return undefined;
108
+ }
109
+
73
110
  getCodexHome(): string {
74
111
  return this.env.CODEX_HOME ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
75
112
  }
76
113
 
114
+ getEnvironmentVariable(name: string): string | undefined {
115
+ return this.env[name] ?? process.env[name];
116
+ }
117
+
77
118
  close(): void {
78
119
  this.client?.close();
79
120
  if (this.child && !this.child.killed) this.child.kill();