@keyou007/agent-bot 0.1.20-alpha.0 → 0.1.20

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 (66) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +18 -1
  3. package/README.zh.md +9 -0
  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/CodexLocalActivityDetector.js +32 -10
  11. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  12. package/dist/codex/CodexProcessManager.js +53 -7
  13. package/dist/codex/CodexProcessManager.js.map +1 -1
  14. package/dist/codex/CodexRuntime.js +225 -164
  15. package/dist/codex/CodexRuntime.js.map +1 -1
  16. package/dist/codex/CodexVersion.js +18 -0
  17. package/dist/codex/CodexVersion.js.map +1 -0
  18. package/dist/codex/ProviderSwitch.js +32 -0
  19. package/dist/codex/ProviderSwitch.js.map +1 -0
  20. package/dist/config/AgentExecutionDefaultsStore.js +1 -1
  21. package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
  22. package/dist/feishu/CardRenderer.js +36 -11
  23. package/dist/feishu/CardRenderer.js.map +1 -1
  24. package/dist/feishu/FeishuConnector.js +17 -5
  25. package/dist/feishu/FeishuConnector.js.map +1 -1
  26. package/dist/feishu/InboundText.js +11 -0
  27. package/dist/feishu/InboundText.js.map +1 -1
  28. package/dist/feishu/MergedForwardMessage.js +17 -5
  29. package/dist/feishu/MergedForwardMessage.js.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/local-files/LocalFileViewerServer.js +25 -7
  33. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  34. package/dist/local-files/MarkdownFileLinks.js +65 -0
  35. package/dist/local-files/MarkdownFileLinks.js.map +1 -0
  36. package/dist/proxy/ProxySessionController.js +400 -259
  37. package/dist/proxy/ProxySessionController.js.map +1 -1
  38. package/dist/state/StateStore.js +99 -65
  39. package/dist/state/StateStore.js.map +1 -1
  40. package/dist/utils/semanticVersion.js +39 -0
  41. package/dist/utils/semanticVersion.js.map +1 -0
  42. package/docs/technical-reference.md +5 -3
  43. package/docs/technical-reference.zh.md +5 -3
  44. package/npm-shrinkwrap.json +2 -2
  45. package/package.json +1 -1
  46. package/skills/agent-bot/SKILL.md +7 -1
  47. package/src/cli/AgentPrerequisites.ts +14 -40
  48. package/src/cli/DefaultAgentSelection.ts +1 -1
  49. package/src/cli.ts +13 -0
  50. package/src/codex/CodexLocalActivityDetector.ts +41 -9
  51. package/src/codex/CodexProcessManager.ts +44 -7
  52. package/src/codex/CodexRuntime.ts +244 -195
  53. package/src/codex/CodexVersion.ts +18 -0
  54. package/src/codex/ProviderSwitch.ts +36 -0
  55. package/src/config/AgentExecutionDefaultsStore.ts +1 -1
  56. package/src/feishu/CardRenderer.ts +37 -27
  57. package/src/feishu/FeishuConnector.ts +15 -5
  58. package/src/feishu/InboundText.ts +11 -0
  59. package/src/feishu/MergedForwardMessage.ts +15 -5
  60. package/src/index.ts +1 -1
  61. package/src/local-files/LocalFileViewerServer.ts +26 -7
  62. package/src/local-files/MarkdownFileLinks.ts +64 -0
  63. package/src/proxy/ProxySessionController.ts +370 -240
  64. package/src/runtime/types.ts +15 -2
  65. package/src/state/StateStore.ts +117 -90
  66. package/src/utils/semanticVersion.ts +28 -0
@@ -96,6 +96,8 @@ 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
+ 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.
100
+
99
101
  ## Change Settings
100
102
 
101
103
  ```powershell
@@ -108,6 +110,8 @@ agentbot task permissions [auto|confirm]
108
110
 
109
111
  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
112
 
113
+ Provider changes require an idle task and verify the actual Provider and model before saving. 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.
114
+
111
115
  ## Goals And Turns
112
116
 
113
117
  ```powershell
@@ -119,7 +123,7 @@ agentbot task turns
119
123
  agentbot task reset <turn-id>
120
124
  ```
121
125
 
122
- Use `turns` to obtain a real Turn ID before `reset`. Reset changes conversation context only; it does not revert local files.
126
+ 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
127
 
124
128
  ## Files And Local Commands
125
129
 
@@ -168,6 +172,8 @@ agentbot --profile ~/.agent-bot-rescue server status
168
172
 
169
173
  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
174
 
175
+ 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.
176
+
171
177
  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
178
 
173
179
  ## 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`,
@@ -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,6 +96,17 @@ 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
  }