lark-remote 0.1.4 → 0.1.6

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 (75) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.en.md +0 -1
  3. package/README.md +0 -1
  4. package/dist/bridge/approval-coordinator.d.ts +108 -0
  5. package/dist/bridge/approval-coordinator.js +258 -0
  6. package/dist/bridge/index.d.ts +62 -0
  7. package/dist/bridge/index.js +423 -20
  8. package/dist/bridge/queue-manager.d.ts +9 -0
  9. package/dist/bridge/queue-manager.js +19 -11
  10. package/dist/card/approval-render.d.ts +16 -0
  11. package/dist/card/approval-render.js +247 -0
  12. package/dist/card/card-shared.d.ts +14 -0
  13. package/dist/card/card-shared.js +28 -0
  14. package/dist/card/run-renderer.js +78 -7
  15. package/dist/card/run-state.d.ts +25 -3
  16. package/dist/card/run-state.js +210 -2
  17. package/dist/config/dir.d.ts +1 -0
  18. package/dist/config/dir.js +4 -0
  19. package/dist/config/index.d.ts +43 -0
  20. package/dist/config/index.js +25 -1
  21. package/dist/connector/index.d.ts +5 -0
  22. package/dist/connector/index.js +32 -0
  23. package/dist/index.js +95 -23
  24. package/dist/router/config/codex.d.ts +1 -1
  25. package/dist/router/config/codex.js +49 -0
  26. package/dist/router/config/common/render.js +4 -0
  27. package/dist/router/config/index.d.ts +0 -10
  28. package/dist/router/config/index.js +0 -17
  29. package/dist/router/config/types.d.ts +1 -1
  30. package/dist/router/index.d.ts +61 -1
  31. package/dist/router/index.js +323 -48
  32. package/dist/router/utils.d.ts +2 -0
  33. package/dist/router/utils.js +5 -2
  34. package/dist/runner/codex/app-server/client.d.ts +82 -0
  35. package/dist/runner/codex/app-server/client.js +234 -0
  36. package/dist/runner/codex/app-server/connection-manager.d.ts +56 -0
  37. package/dist/runner/codex/app-server/connection-manager.js +170 -0
  38. package/dist/runner/codex/app-server/protocol-types.d.ts +527 -0
  39. package/dist/runner/codex/app-server/protocol-types.js +67 -0
  40. package/dist/runner/codex/app-server/runner.d.ts +121 -0
  41. package/dist/runner/codex/app-server/runner.js +554 -0
  42. package/dist/runner/codex/app-server/translator.d.ts +109 -0
  43. package/dist/runner/codex/app-server/translator.js +734 -0
  44. package/dist/runner/codex/app-server/transport.d.ts +49 -0
  45. package/dist/runner/codex/app-server/transport.js +231 -0
  46. package/dist/runner/codex/argv.d.ts +1 -1
  47. package/dist/runner/codex/argv.js +1 -1
  48. package/dist/runner/codex/factory.d.ts +29 -0
  49. package/dist/runner/codex/factory.js +39 -0
  50. package/dist/runner/codex/index.d.ts +2 -0
  51. package/dist/runner/codex/index.js +2 -0
  52. package/dist/runner/codex/runner.d.ts +3 -0
  53. package/dist/runner/codex/runner.js +5 -0
  54. package/dist/runner/common/runner-utils.d.ts +12 -0
  55. package/dist/runner/common/runner-utils.js +21 -0
  56. package/dist/runner/common/spawning-runner.js +9 -1
  57. package/dist/runner/common/usage.d.ts +2 -0
  58. package/dist/runner/common/usage.js +2 -0
  59. package/dist/runner/index.d.ts +2 -2
  60. package/dist/runner/index.js +1 -1
  61. package/dist/runner/types.d.ts +123 -2
  62. package/dist/session/claude/session-index.d.ts +93 -0
  63. package/dist/session/claude/session-index.js +374 -0
  64. package/dist/session/claude/sessions.d.ts +0 -18
  65. package/dist/session/claude/sessions.js +102 -319
  66. package/dist/session/codex/rollout-reader.js +46 -3
  67. package/dist/update/index.d.ts +10 -0
  68. package/dist/update/index.js +11 -0
  69. package/dist/update/install.d.ts +29 -0
  70. package/dist/update/install.js +93 -0
  71. package/dist/update/startup-hint.d.ts +5 -0
  72. package/dist/update/startup-hint.js +11 -0
  73. package/dist/update/version-check.d.ts +31 -0
  74. package/dist/update/version-check.js +149 -0
  75. package/package.json +1 -14
@@ -0,0 +1,49 @@
1
+ /**
2
+ * JSONL-RPC transport for the Codex app-server protocol.
3
+ *
4
+ * Wraps a child process (spawned from `codex app-server`) with
5
+ * JSON-line-based bidirectional communication: each line on stdout is a JSON
6
+ * message from the server; each `write()` sends a JSON line to stdin.
7
+ */
8
+ export interface TransportEvents {
9
+ onMessage(msg: unknown): void;
10
+ onClose(reason: string): void;
11
+ }
12
+ export declare class JsonlRpcTransport {
13
+ private proc;
14
+ private _closed;
15
+ private events;
16
+ private stopper;
17
+ private readonly binary;
18
+ private readonly args;
19
+ private readonly cwd;
20
+ private readonly env;
21
+ /** JSON lines buffered while stdin is under backpressure (bounded by callers). */
22
+ private writeQueue;
23
+ private flushing;
24
+ constructor(opts: {
25
+ binary: string;
26
+ args: string[];
27
+ cwd: string;
28
+ env?: Record<string, string | undefined>;
29
+ });
30
+ get closed(): boolean;
31
+ /**
32
+ * Spawn the child process and start reading its stdout.
33
+ * Returns when the process closes (or immediately if spawn fails).
34
+ */
35
+ start(events: TransportEvents): Promise<void>;
36
+ /**
37
+ * Write a JSON message as a single line to the process's stdin.
38
+ * On EPIPE, triggers onClose('epipe') without throwing.
39
+ */
40
+ write(msg: object): void;
41
+ /** Send queued lines to stdin, pausing on backpressure until drain. */
42
+ private flush;
43
+ /**
44
+ * Gracefully stop the child process: SIGTERM → grace → SIGKILL.
45
+ * Idempotent — safe to call multiple times.
46
+ */
47
+ close(): Promise<void>;
48
+ private handleClose;
49
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * JSONL-RPC transport for the Codex app-server protocol.
3
+ *
4
+ * Wraps a child process (spawned from `codex app-server`) with
5
+ * JSON-line-based bidirectional communication: each line on stdout is a JSON
6
+ * message from the server; each `write()` sends a JSON line to stdin.
7
+ */
8
+ import { spawn } from 'node:child_process';
9
+ import { ProcessStopper } from '../../common/process-stopper.js';
10
+ import { getLogger } from '../../../logger/index.js';
11
+ /** Max bytes for a single line before we disconnect (10MB). */
12
+ const MAX_LINE_BYTES = 10 * 1024 * 1024;
13
+ /** Max bytes retained from stderr for diagnostics (64KB). */
14
+ const MAX_STDERR_BYTES = 64 * 1024;
15
+ export class JsonlRpcTransport {
16
+ proc = null;
17
+ _closed = false;
18
+ events = null;
19
+ stopper;
20
+ binary;
21
+ args;
22
+ cwd;
23
+ env;
24
+ /** JSON lines buffered while stdin is under backpressure (bounded by callers). */
25
+ writeQueue = [];
26
+ flushing = false;
27
+ constructor(opts) {
28
+ this.binary = opts.binary;
29
+ this.args = opts.args;
30
+ this.cwd = opts.cwd;
31
+ this.env = opts.env ?? {};
32
+ this.stopper = new ProcessStopper({ graceMs: 5000 });
33
+ }
34
+ get closed() {
35
+ return this._closed;
36
+ }
37
+ /**
38
+ * Spawn the child process and start reading its stdout.
39
+ * Returns when the process closes (or immediately if spawn fails).
40
+ */
41
+ async start(events) {
42
+ this.events = events;
43
+ // 与 exec 模式对齐(SpawningRunner.getSpawnEnv 全量透传 process.env):
44
+ // codex 是用户自己的可信二进制,provider 认证依赖 OPENAI_API_KEY /
45
+ // ANTHROPIC_API_KEY / 自定义 provider 的 env_key(codex-config.ts
46
+ // providerEnvKeys),代理环境依赖 HTTP(S)_PROXY、TMPDIR 等;任何白名单
47
+ // 收窄都会打断认证或网络。调用方 this.env 覆盖 process.env 同名键。
48
+ const childEnv = { ...process.env, ...this.env };
49
+ const proc = spawn(this.binary, this.args, {
50
+ cwd: this.cwd,
51
+ stdio: ['pipe', 'pipe', 'pipe'],
52
+ detached: true,
53
+ env: childEnv,
54
+ });
55
+ this.proc = proc;
56
+ if (proc.pid === undefined) {
57
+ // ENOENT early check
58
+ const err = await new Promise((resolve) => {
59
+ proc.once('error', (e) => resolve(e));
60
+ setTimeout(() => resolve(undefined), 5000);
61
+ });
62
+ this._closed = true;
63
+ const reason = err ? `ENOENT: ${this.binary} not found` : `spawn failed: ${this.binary}`;
64
+ events.onClose(reason);
65
+ return;
66
+ }
67
+ getLogger().info(`[codex-transport] spawned pid=${proc.pid} binary=${this.binary}`);
68
+ proc.stdin?.on('error', (err) => {
69
+ getLogger().warn(`[codex-transport] stdin error: ${err.message}`);
70
+ this.handleClose('epipe');
71
+ });
72
+ // Stderr: rolling buffer, last 64KB retained, only log
73
+ let stderrBuf = '';
74
+ proc.stderr?.on('data', (chunk) => {
75
+ const text = chunk.toString('utf-8');
76
+ stderrBuf = (stderrBuf + text).slice(-MAX_STDERR_BYTES);
77
+ getLogger().debug(`[codex-transport] stderr: ${text.trimEnd()}`);
78
+ });
79
+ // Stdout: line-split with 10MB line limit
80
+ let remainder = '';
81
+ proc.stdout?.on('data', (chunk) => {
82
+ remainder += chunk.toString('utf-8');
83
+ const lines = remainder.split('\n');
84
+ // Lines except the last (incomplete) one
85
+ remainder = lines.pop() ?? '';
86
+ for (const line of lines) {
87
+ if (line.length === 0)
88
+ continue;
89
+ if (line.length > MAX_LINE_BYTES) {
90
+ getLogger().error(`[codex-transport] line exceeds ${MAX_LINE_BYTES} bytes, disconnecting`);
91
+ this.handleClose('parse_error');
92
+ return;
93
+ }
94
+ try {
95
+ const msg = JSON.parse(line);
96
+ events.onMessage(msg);
97
+ }
98
+ catch (err) {
99
+ getLogger().warn(`[codex-transport] failed to parse JSON: ${err.message} line=${line.slice(0, 200)}`);
100
+ }
101
+ }
102
+ });
103
+ // Handle process exit
104
+ const onExit = (code, signal) => {
105
+ this._closed = true;
106
+ this.proc = null;
107
+ // Flush remaining buffer
108
+ if (remainder.length > 0) {
109
+ try {
110
+ const msg = JSON.parse(remainder);
111
+ events.onMessage(msg);
112
+ }
113
+ catch {
114
+ // ignore trailing incomplete line
115
+ }
116
+ }
117
+ const reason = signal ? `signal:${signal}` : `exit:${code}`;
118
+ events.onClose(reason);
119
+ getLogger().info(`[codex-transport] process exited pid=${proc.pid} code=${code} signal=${signal}`);
120
+ };
121
+ proc.once('exit', onExit);
122
+ proc.once('error', (err) => {
123
+ getLogger().error(`[codex-transport] process error: ${err.message}`);
124
+ this.handleClose(`error:${err.message}`);
125
+ });
126
+ }
127
+ /**
128
+ * Write a JSON message as a single line to the process's stdin.
129
+ * On EPIPE, triggers onClose('epipe') without throwing.
130
+ */
131
+ write(msg) {
132
+ if (this._closed || !this.proc?.stdin) {
133
+ return;
134
+ }
135
+ try {
136
+ // 入队后顺序 flush;write() 返回 false(高水位)时暂停直到 drain,
137
+ // 避免大消息/突发消息在 Node 内部无限缓冲(review P3-8)。
138
+ this.writeQueue.push(Buffer.from(JSON.stringify(msg) + '\n', 'utf8'));
139
+ this.flush();
140
+ }
141
+ catch {
142
+ this.handleClose('epipe');
143
+ }
144
+ }
145
+ /** Send queued lines to stdin, pausing on backpressure until drain. */
146
+ flush() {
147
+ const stdin = this.proc?.stdin;
148
+ if (this._closed || !stdin || this.flushing)
149
+ return;
150
+ this.flushing = true;
151
+ const next = () => {
152
+ if (this._closed) {
153
+ this.writeQueue = [];
154
+ this.flushing = false;
155
+ return;
156
+ }
157
+ const chunk = this.writeQueue.shift();
158
+ if (!chunk) {
159
+ this.flushing = false;
160
+ return;
161
+ }
162
+ try {
163
+ const ok = stdin.write(chunk, (err) => {
164
+ if (err) {
165
+ this.handleClose('epipe');
166
+ }
167
+ });
168
+ if (!ok) {
169
+ // 流满:等待 drain 再发下一条,队列有界(调用方同步节奏)。
170
+ stdin.once('drain', next);
171
+ }
172
+ else {
173
+ next();
174
+ }
175
+ }
176
+ catch {
177
+ this.handleClose('epipe');
178
+ }
179
+ };
180
+ next();
181
+ }
182
+ /**
183
+ * Gracefully stop the child process: SIGTERM → grace → SIGKILL.
184
+ * Idempotent — safe to call multiple times.
185
+ */
186
+ async close() {
187
+ if (this._closed)
188
+ return;
189
+ this._closed = true;
190
+ const proc = this.proc;
191
+ if (proc) {
192
+ try {
193
+ await this.stopper.stop(proc);
194
+ }
195
+ catch {
196
+ // Ignore stop errors — process may already be dead
197
+ }
198
+ if (proc.stdin && !proc.stdin.destroyed) {
199
+ proc.stdin.end();
200
+ }
201
+ }
202
+ }
203
+ handleClose(reason) {
204
+ if (this._closed)
205
+ return;
206
+ this._closed = true;
207
+ this.writeQueue = [];
208
+ // Remove the exit listener to avoid double-firing
209
+ if (this.proc) {
210
+ this.proc.removeAllListeners('exit');
211
+ this.proc.removeAllListeners('error');
212
+ }
213
+ const proc = this.proc;
214
+ this.proc = null;
215
+ if (proc) {
216
+ if (proc.stdin && !proc.stdin.destroyed) {
217
+ proc.stdin.end();
218
+ }
219
+ // 异常路径(EPIPE/超长行/进程 error)也要收掉子进程,避免孤儿进程
220
+ // 继续运行(如超长行场景子进程还在往 stdout 灌数据)。
221
+ void this.stopper.stop(proc).catch((err) => {
222
+ getLogger().warn(`[codex-transport] failed to stop process during close: ${err instanceof Error ? err.message : String(err)}`);
223
+ });
224
+ }
225
+ // 统一关闭出口:EPIPE/超长行/进程 error 都必须通知上层(connection-manager
226
+ // 靠 onClose 删除 slot、client 靠它 failPending),否则 slot 悬挂死连接。
227
+ this.events?.onClose(reason);
228
+ getLogger().info(`[codex-transport] closed: ${reason}`);
229
+ }
230
+ }
231
+ //# sourceMappingURL=transport.js.map
@@ -5,7 +5,7 @@
5
5
  * - `approval_policy="never"` eliminates the approval handshake (root cause ①).
6
6
  * - Prompt goes via stdin (`-` flag) to avoid argv escaping issues.
7
7
  * - `--skip-git-repo-check`: lark-remote's cwd may not be a git repo.
8
- * - User's `~/.codex/config.toml` and AGENTS.md/CLAUDE.md rules are preserved
8
+ * - User's `~/.codex/config.toml` and project rules are preserved
9
9
  * (no `--ignore-user-config` / `--ignore-rules`).
10
10
  */
11
11
  interface BuildCodexExecArgsInput {
@@ -5,7 +5,7 @@
5
5
  * - `approval_policy="never"` eliminates the approval handshake (root cause ①).
6
6
  * - Prompt goes via stdin (`-` flag) to avoid argv escaping issues.
7
7
  * - `--skip-git-repo-check`: lark-remote's cwd may not be a git repo.
8
- * - User's `~/.codex/config.toml` and AGENTS.md/CLAUDE.md rules are preserved
8
+ * - User's `~/.codex/config.toml` and project rules are preserved
9
9
  * (no `--ignore-user-config` / `--ignore-rules`).
10
10
  */
11
11
  /**
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Codex runner factory: selects the concrete runner based on `serviceMode`.
3
+ *
4
+ * `exec` (default) → CodexExecRunner (spawn-per-message `codex exec`).
5
+ * `app-server` → CodexAppServerRunner (persistent `codex app-server` JSON-RPC
6
+ * connection with approval support and sandbox enforcement).
7
+ */
8
+ import type { AgentSessionReader } from '../types.js';
9
+ import { CodexExecRunner } from './runner.js';
10
+ import { CodexAppServerRunner } from './app-server/runner.js';
11
+ import type { AskForApproval, SandboxMode } from './app-server/protocol-types.js';
12
+ export interface CreateCodexRunnerOptions {
13
+ model?: string;
14
+ modelProvider?: string;
15
+ reasoningEffort?: string;
16
+ stopGraceMs?: number;
17
+ pidDir: string;
18
+ workspace: string;
19
+ sessionReader: AgentSessionReader;
20
+ serviceMode?: 'exec' | 'app-server';
21
+ sandbox?: SandboxMode;
22
+ approvalPolicy?: AskForApproval;
23
+ appServerBinary?: string;
24
+ appServerArgs?: string[];
25
+ appServerRequestTimeoutMs?: number;
26
+ appServerIdleTtlMs?: number;
27
+ appServerTurnIdleTimeoutMinutes?: number;
28
+ }
29
+ export declare function createCodexRunner(opts: CreateCodexRunnerOptions): CodexExecRunner | CodexAppServerRunner;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Codex runner factory: selects the concrete runner based on `serviceMode`.
3
+ *
4
+ * `exec` (default) → CodexExecRunner (spawn-per-message `codex exec`).
5
+ * `app-server` → CodexAppServerRunner (persistent `codex app-server` JSON-RPC
6
+ * connection with approval support and sandbox enforcement).
7
+ */
8
+ import { CodexExecRunner } from './runner.js';
9
+ import { CodexAppServerRunner } from './app-server/runner.js';
10
+ export function createCodexRunner(opts) {
11
+ if (opts.serviceMode === 'app-server') {
12
+ return new CodexAppServerRunner({
13
+ kind: 'codex',
14
+ sessionReader: opts.sessionReader,
15
+ binary: opts.appServerBinary ?? 'codex',
16
+ appServerArgs: opts.appServerArgs,
17
+ requestTimeoutMs: opts.appServerRequestTimeoutMs,
18
+ idleTtlMs: opts.appServerIdleTtlMs,
19
+ turnTimeoutMs: opts.appServerTurnIdleTimeoutMinutes != null
20
+ ? opts.appServerTurnIdleTimeoutMinutes * 60_000
21
+ : undefined,
22
+ model: opts.model,
23
+ modelProvider: opts.modelProvider,
24
+ reasoningEffort: opts.reasoningEffort,
25
+ sandbox: opts.sandbox,
26
+ approvalPolicy: opts.approvalPolicy,
27
+ });
28
+ }
29
+ return new CodexExecRunner({
30
+ model: opts.model,
31
+ modelProvider: opts.modelProvider,
32
+ reasoningEffort: opts.reasoningEffort,
33
+ stopGraceMs: opts.stopGraceMs,
34
+ pidDir: opts.pidDir,
35
+ workspace: opts.workspace,
36
+ sessionReader: opts.sessionReader,
37
+ });
38
+ }
39
+ //# sourceMappingURL=factory.js.map
@@ -1 +1,3 @@
1
1
  export { CodexExecRunner } from './runner.js';
2
+ export { CodexAppServerRunner } from './app-server/runner.js';
3
+ export { createCodexRunner } from './factory.js';
@@ -1,2 +1,4 @@
1
1
  export { CodexExecRunner } from './runner.js';
2
+ export { CodexAppServerRunner } from './app-server/runner.js';
3
+ export { createCodexRunner } from './factory.js';
2
4
  //# sourceMappingURL=index.js.map
@@ -52,6 +52,9 @@ export declare class CodexExecRunner extends SpawningRunner implements AgentRunn
52
52
  private readonly reasoningEffort?;
53
53
  constructor(opts: CodexExecRunnerOptions);
54
54
  getStatusInfo(): AgentStatusInfo;
55
+ /** exec 模式的 turn.completed.usage 是会话累计值,flow 字段必须 jsonl 优先
56
+ * (review P3-7:桥侧统一走本接口,替代 agentKind 硬编码)。 */
57
+ getUsageAuthority(): 'jsonl';
55
58
  protected buildArgv(opts: SpawnOptions): string[];
56
59
  protected getStdio(): ('ignore' | 'pipe')[];
57
60
  protected setupStdin(proc: ChildProcess, message: string): void;
@@ -72,6 +72,11 @@ export class CodexExecRunner extends SpawningRunner {
72
72
  extras: { sandbox: 'danger-full-access' },
73
73
  };
74
74
  }
75
+ /** exec 模式的 turn.completed.usage 是会话累计值,flow 字段必须 jsonl 优先
76
+ * (review P3-7:桥侧统一走本接口,替代 agentKind 硬编码)。 */
77
+ getUsageAuthority() {
78
+ return 'jsonl';
79
+ }
75
80
  buildArgv(opts) {
76
81
  return buildCodexExecArgs({
77
82
  cwd: opts.cwd,
@@ -11,6 +11,18 @@ export declare function pipeAllStdio(): ('ignore' | 'pipe')[];
11
11
  * rather than argv.
12
12
  */
13
13
  export declare function endStdinWithPrompt(proc: ChildProcess, message: string): void;
14
+ /**
15
+ * Build a synthetic system.init event for pre-spawn failure paths.
16
+ *
17
+ * When the runner exits before spawning a process (validateBeforeRun failure,
18
+ * ENOENT spawn failure), no real system.init arrives from stdout. Without a
19
+ * synthetic init, the bridge's pre-init result guard (§9.22) and the run-state
20
+ * reducer's sessionId check would silently drop the error result, leaving the
21
+ * card showing "输出流已结束,但未收到 result 事件" instead of the actual
22
+ * error message. The synthetic init satisfies both guards so the real error
23
+ * result is processed normally.
24
+ */
25
+ export declare function syntheticInitEvent(sessionId?: string): AgentEvent;
14
26
  /**
15
27
  * Build a standardized auth/error result event for yield.
16
28
  *
@@ -46,6 +46,27 @@ export function endStdinWithPrompt(proc, message) {
46
46
  }
47
47
  }
48
48
  }
49
+ /**
50
+ * Build a synthetic system.init event for pre-spawn failure paths.
51
+ *
52
+ * When the runner exits before spawning a process (validateBeforeRun failure,
53
+ * ENOENT spawn failure), no real system.init arrives from stdout. Without a
54
+ * synthetic init, the bridge's pre-init result guard (§9.22) and the run-state
55
+ * reducer's sessionId check would silently drop the error result, leaving the
56
+ * card showing "输出流已结束,但未收到 result 事件" instead of the actual
57
+ * error message. The synthetic init satisfies both guards so the real error
58
+ * result is processed normally.
59
+ */
60
+ export function syntheticInitEvent(sessionId = '') {
61
+ return {
62
+ type: 'system',
63
+ subtype: 'init',
64
+ session_id: sessionId,
65
+ cwd: '',
66
+ model: '',
67
+ timestamp: new Date().toISOString(),
68
+ };
69
+ }
49
70
  /**
50
71
  * Build a standardized auth/error result event for yield.
51
72
  *
@@ -6,7 +6,7 @@ import { getLogger } from '../../logger/index.js';
6
6
  import { ProcessStopper } from './process-stopper.js';
7
7
  import { SpawnHeartbeat } from './spawn-heartbeat.js';
8
8
  import { createJSONLStream } from './jsonl-stream.js';
9
- import { authErrorEvent } from './runner-utils.js';
9
+ import { authErrorEvent, syntheticInitEvent } from './runner-utils.js';
10
10
  import { DEFAULT_STOP_GRACE_MS } from '../../config/index.js';
11
11
  /**
12
12
  * Magic-number constants (Clean Code P3-1, G25 Replace Magic Numbers with
@@ -255,6 +255,11 @@ export class SpawningRunner {
255
255
  // Pre-spawn validation hook (e.g. codex API key check)
256
256
  const validationError = this.validateBeforeRun(opts);
257
257
  if (validationError) {
258
+ // §9.22: yield synthetic init so the bridge's pre-init result guard and
259
+ // the run-state reducer's sessionId check don't silently drop the error
260
+ // result. Without init, the card would show a generic "输出流已结束"
261
+ // instead of the specific validation error message.
262
+ yield syntheticInitEvent(opts.sessionId);
258
263
  yield validationError;
259
264
  return;
260
265
  }
@@ -287,6 +292,9 @@ export class SpawningRunner {
287
292
  // so the user is not misdiagnosed into reinstalling the binary.
288
293
  const baseMsg = `${this.binary} 命令不可用(未找到或不可执行),请检查是否已安装或在 PATH 中`;
289
294
  const msg = spawnErr ? `${baseMsg}:${spawnErr.message}` : baseMsg;
295
+ // §9.22: yield synthetic init before the error result so bridge/run-state
296
+ // guards don't silently drop it (same rationale as validateBeforeRun).
297
+ yield syntheticInitEvent(opts.sessionId);
290
298
  yield authErrorEvent(msg);
291
299
  return;
292
300
  }
@@ -23,6 +23,8 @@ interface NormalizedResultUsage {
23
23
  cacheReadTokens?: number;
24
24
  cacheCreationTokens?: number;
25
25
  totalTokens?: number;
26
+ /** Model context window limit (codex app-server `modelContextWindow` passthrough). */
27
+ contextLimit?: number;
26
28
  /** Reconstructed or agent-declared context length (see JSDoc above). */
27
29
  contextLength: number;
28
30
  }
@@ -14,6 +14,7 @@ export function normalizeResultUsage(usage) {
14
14
  const cacheRead = usage.cache_read_tokens ?? usage.cache_read_input_tokens;
15
15
  const cacheCreation = usage.cache_creation_tokens ?? usage.cache_creation_input_tokens;
16
16
  const totalTokens = usage.total_tokens;
17
+ const contextLimit = usage.context_limit;
17
18
  // total_tokens (agent-declared total) takes priority; otherwise reconstruct
18
19
  // from the parts: input + cacheRead + cacheCreation + output.
19
20
  const contextLength = totalTokens ?? inputTokens + (cacheRead ?? 0) + (cacheCreation ?? 0) + outputTokens;
@@ -23,6 +24,7 @@ export function normalizeResultUsage(usage) {
23
24
  cacheReadTokens: cacheRead,
24
25
  cacheCreationTokens: cacheCreation,
25
26
  totalTokens,
27
+ contextLimit,
26
28
  contextLength,
27
29
  };
28
30
  }
@@ -8,12 +8,12 @@
8
8
  * (共享工具由消费方直接 import `common/*` 具体模块,不走 barrel)
9
9
  */
10
10
  export { ClaudeRunner } from './claude/index.js';
11
- export { CodexExecRunner } from './codex/index.js';
11
+ export { CodexExecRunner, CodexAppServerRunner, createCodexRunner } from './codex/index.js';
12
12
  export { OpencodeExecRunner } from './opencode/index.js';
13
13
  export { PiRunner } from './pi/index.js';
14
14
  export { KimiRunner } from './kimi/index.js';
15
15
  export { BashProcessRunner, type BashRunner } from './bash/index.js';
16
16
  export { AgentRegistry } from './registry.js';
17
- export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, AgentSessionReader, AgentRunner, } from './types.js';
17
+ export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, } from './types.js';
18
18
  export { resolveAgentChoices } from './resolve-agent-choices.js';
19
19
  export { syncAgentChoices } from './sync-agent-choices.js';
@@ -9,7 +9,7 @@
9
9
  */
10
10
  // Re-export AgentRunner 实现(从各 agent 目录)
11
11
  export { ClaudeRunner } from './claude/index.js';
12
- export { CodexExecRunner } from './codex/index.js';
12
+ export { CodexExecRunner, CodexAppServerRunner, createCodexRunner } from './codex/index.js';
13
13
  export { OpencodeExecRunner } from './opencode/index.js';
14
14
  export { PiRunner } from './pi/index.js';
15
15
  export { KimiRunner } from './kimi/index.js';