lark-remote 0.1.5 → 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 (55) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.en.md +0 -4
  3. package/README.md +0 -4
  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 +402 -21
  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 +77 -6
  15. package/dist/card/run-state.d.ts +25 -3
  16. package/dist/card/run-state.js +203 -2
  17. package/dist/config/dir.js +1 -1
  18. package/dist/config/index.d.ts +42 -0
  19. package/dist/config/index.js +23 -1
  20. package/dist/connector/index.d.ts +5 -0
  21. package/dist/connector/index.js +12 -0
  22. package/dist/index.js +44 -22
  23. package/dist/router/config/codex.d.ts +1 -1
  24. package/dist/router/config/codex.js +49 -0
  25. package/dist/router/config/common/render.js +4 -0
  26. package/dist/router/config/types.d.ts +1 -1
  27. package/dist/router/index.d.ts +27 -2
  28. package/dist/router/index.js +109 -28
  29. package/dist/router/utils.d.ts +2 -0
  30. package/dist/router/utils.js +5 -2
  31. package/dist/runner/codex/app-server/client.d.ts +82 -0
  32. package/dist/runner/codex/app-server/client.js +234 -0
  33. package/dist/runner/codex/app-server/connection-manager.d.ts +56 -0
  34. package/dist/runner/codex/app-server/connection-manager.js +170 -0
  35. package/dist/runner/codex/app-server/protocol-types.d.ts +527 -0
  36. package/dist/runner/codex/app-server/protocol-types.js +67 -0
  37. package/dist/runner/codex/app-server/runner.d.ts +121 -0
  38. package/dist/runner/codex/app-server/runner.js +554 -0
  39. package/dist/runner/codex/app-server/translator.d.ts +109 -0
  40. package/dist/runner/codex/app-server/translator.js +734 -0
  41. package/dist/runner/codex/app-server/transport.d.ts +49 -0
  42. package/dist/runner/codex/app-server/transport.js +231 -0
  43. package/dist/runner/codex/factory.d.ts +29 -0
  44. package/dist/runner/codex/factory.js +39 -0
  45. package/dist/runner/codex/index.d.ts +2 -0
  46. package/dist/runner/codex/index.js +2 -0
  47. package/dist/runner/codex/runner.d.ts +3 -0
  48. package/dist/runner/codex/runner.js +5 -0
  49. package/dist/runner/common/usage.d.ts +2 -0
  50. package/dist/runner/common/usage.js +2 -0
  51. package/dist/runner/index.d.ts +2 -2
  52. package/dist/runner/index.js +1 -1
  53. package/dist/runner/types.d.ts +123 -2
  54. package/dist/session/codex/rollout-reader.js +43 -0
  55. package/package.json +1 -1
@@ -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
@@ -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,
@@ -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';
@@ -21,6 +21,75 @@ interface SystemCompactEvent {
21
21
  preTokens: number;
22
22
  };
23
23
  }
24
+ export interface ApprovalRequestedEvent {
25
+ type: 'approval_requested';
26
+ /** JSON-RPC id of the server request. Wire type is string | integer — keep raw. */
27
+ requestId: number | string;
28
+ kind: 'command' | 'file' | 'permissions';
29
+ threadId: string;
30
+ turnId: string;
31
+ itemId: string;
32
+ view: ApprovalView;
33
+ timestamp?: string;
34
+ }
35
+ export interface ApprovalResolvedEvent {
36
+ type: 'approval_resolved';
37
+ requestId: number | string;
38
+ outcome: 'resolved' | 'expired';
39
+ timestamp?: string;
40
+ }
41
+ /** Approval card content update (out-of-order item/started arrives late). */
42
+ export interface ApprovalViewUpdatedEvent {
43
+ type: 'approval_view_updated';
44
+ requestId: number | string;
45
+ view: ApprovalView;
46
+ timestamp?: string;
47
+ }
48
+ /** Approval expired locally (bridge-side timeout) — card should show expired UI. */
49
+ export interface ApprovalExpiredEvent {
50
+ type: 'approval_expired';
51
+ requestId: number | string;
52
+ timestamp?: string;
53
+ }
54
+ export interface ApprovalView {
55
+ requestId: number | string;
56
+ kind: 'command' | 'file' | 'permissions';
57
+ reason?: string;
58
+ threadShort: string;
59
+ turnShort: string;
60
+ workspace: string;
61
+ command?: string;
62
+ commandCwd?: string;
63
+ fileChanges?: Array<{
64
+ path: string;
65
+ kind: 'add' | 'update' | 'delete';
66
+ diff?: string;
67
+ }>;
68
+ network?: {
69
+ host: string;
70
+ protocol: string;
71
+ };
72
+ permissions?: {
73
+ networkEnabled?: boolean;
74
+ fileSystemRead?: string[];
75
+ fileSystemWrite?: string[];
76
+ items: Array<{
77
+ id: string;
78
+ label: string;
79
+ target: {
80
+ kind: 'network';
81
+ } | {
82
+ kind: 'fsRead' | 'fsWrite';
83
+ path: string;
84
+ };
85
+ selected: boolean;
86
+ }>;
87
+ };
88
+ availableDecisions: string[];
89
+ /** Raw payloads for structured decisions (e.g. acceptWithExecpolicyAmendment). */
90
+ decisionPayloads?: Record<string, unknown>;
91
+ pendingTotal: number;
92
+ }
24
93
  interface ThinkingContent {
25
94
  type: 'thinking';
26
95
  thinking: string;
@@ -58,7 +127,7 @@ export interface UserEvent {
58
127
  }
59
128
  export interface ResultEvent {
60
129
  type: 'result';
61
- subtype: 'success' | 'error';
130
+ subtype: 'success' | 'error' | 'interrupted';
62
131
  session_id: string;
63
132
  timestamp?: string;
64
133
  usage?: {
@@ -75,6 +144,9 @@ export interface ResultEvent {
75
144
  cache_creation_input_tokens?: number;
76
145
  /** Agent-declared total; when present the display uses max(total, sum of parts). */
77
146
  total_tokens?: number;
147
+ /** 当前模型 context window 上限(codex app-server tokenUsage.modelContextWindow
148
+ * 透传)。仅 app-server 提供;缺省时卡片只显示绝对量、不显示百分比。 */
149
+ context_limit?: number;
78
150
  };
79
151
  total_cost_usd?: number;
80
152
  errorMessage?: string;
@@ -99,7 +171,50 @@ export interface FileChangeEvent {
99
171
  operation: 'create' | 'edit' | 'delete' | 'read';
100
172
  timestamp?: string;
101
173
  }
102
- export type AgentEvent = SystemInitEvent | SystemCompactEvent | AssistantEvent | UserEvent | ResultEvent | PlanEvent | FileChangeEvent;
174
+ export interface TurnStartedEvent {
175
+ type: 'turn_started';
176
+ threadId: string;
177
+ turnId: string;
178
+ operationKind: 'turn' | 'compaction';
179
+ timestamp?: string;
180
+ }
181
+ export interface TurnDiffEvent {
182
+ type: 'turn_diff';
183
+ /**
184
+ * Thread item id this snapshot belongs to (app-server item-scoped stream).
185
+ * Each content item (reasoning / agentMessage / commandExecution / plan /
186
+ * fileChange) is tracked independently, so interleaved items keep their
187
+ * real chronology and update their own block instead of a shared one.
188
+ */
189
+ itemId: string;
190
+ /** Full accumulated assistant text snapshot for this item (replaces previous). */
191
+ text?: string;
192
+ /** Full accumulated reasoning snapshot for this item (replaces previous). */
193
+ reasoning?: string;
194
+ /** Full accumulated tool output snapshot for this item (replaces previous). */
195
+ toolOutput?: string;
196
+ /** Full accumulated plan text snapshot for this item (replaces previous). */
197
+ plan?: string;
198
+ /** Current set of file changes for this item (replaces previous). */
199
+ fileChanges?: Array<{
200
+ path: string;
201
+ kind: 'add' | 'update' | 'delete';
202
+ diff?: string;
203
+ }>;
204
+ /**
205
+ * Authoritative completion snapshot (item/completed or turn/completed).
206
+ * The reducer finalizes the block in place: thinking/plan active=false,
207
+ * tool status ok, completedAt stamped. Never reorders blocks.
208
+ */
209
+ complete?: boolean;
210
+ /** Authoritative tool status at completion (commandExecution item). */
211
+ toolStatus?: 'ok' | 'error';
212
+ threadId: string;
213
+ turnId: string;
214
+ timestamp?: string;
215
+ }
216
+ export type RunnerLifetime = 'turn' | 'workspace';
217
+ export type AgentEvent = SystemInitEvent | SystemCompactEvent | ApprovalRequestedEvent | ApprovalResolvedEvent | ApprovalViewUpdatedEvent | ApprovalExpiredEvent | AssistantEvent | UserEvent | ResultEvent | PlanEvent | FileChangeEvent | TurnStartedEvent | TurnDiffEvent;
103
218
  export interface SpawnOptions {
104
219
  cwd: string;
105
220
  sessionId?: string;
@@ -117,6 +232,8 @@ export interface SpawnOptions {
117
232
  */
118
233
  export interface Runner {
119
234
  readonly isRunning: boolean;
235
+ readonly lifetime?: RunnerLifetime;
236
+ dispose?(): Promise<void>;
120
237
  run(message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
121
238
  /**
122
239
  * Stop the running process. SIGTERM → grace → SIGKILL on the per-workspace
@@ -158,6 +275,8 @@ export interface AgentSessionUsage {
158
275
  contextLimit?: number;
159
276
  /** Claude auto-compact 次数(数 compact_boundary 事件);codex/opencode 无此概念,留 undefined。 */
160
277
  compactCount?: number;
278
+ /** 压缩前上下文水位(codex compact 后从会话 jsonl 读取,仅会话以压缩收尾时有值)。 */
279
+ compactPreContextLength?: number;
161
280
  /** 从缓存读取的 token 数(节省的费用)。 */
162
281
  cacheReadTokens?: number;
163
282
  /** 新建缓存的 token 数。 */
@@ -236,6 +355,8 @@ export interface AgentRunner extends Runner {
236
355
  readonly sessionReader: AgentSessionReader;
237
356
  /** Return current status info for /status display. */
238
357
  getStatusInfo(): AgentStatusInfo;
358
+ /** Whether this runner provides live usage data (vs. file-based). */
359
+ getUsageAuthority?(): 'live' | 'jsonl';
239
360
  }
240
361
  /** Agent self-describing status info for /status display. */
241
362
  export interface AgentStatusInfo {
@@ -58,6 +58,18 @@ export function readCodexRollout(filePath) {
58
58
  // Run card's "累计" display. Codex emits this alongside last_token_usage.
59
59
  let lastTotalUsage;
60
60
  let previousTotals;
61
+ // Compaction 统计:codex 的 compact turn 在会话文件里写顶层 `compacted`
62
+ // 事件(含摘要),压缩前后的上下文水位由相邻 token_count 表达。压缩收尾的
63
+ // token_count 增量是 input/cached/output 全 0、只有 total_tokens 有值
64
+ // (窗口被摘要+replacement history 占据),现有 zero-filter 会跳过它,
65
+ // 因此这里必须单独捕获,不能复用 lastRawUsage。
66
+ // 单位口径(review P3-9):前后两侧统一用 total_tokens —— 压缩前取最近一次
67
+ // 非零 token_count 的 total_tokens(input+output,即该 turn 在窗口内的全部
68
+ // token),压缩后取收尾事件的 total_tokens(压缩后窗口)。不得一边 input
69
+ // 一边 total,否则「压缩前 X → 压缩后 Y」两边单位不一致。
70
+ let compactCount = 0;
71
+ let compactPreContextLength;
72
+ let compactPostContextLength;
61
73
  for (const line of lines) {
62
74
  const trimmed = line.trim();
63
75
  if (!trimmed)
@@ -101,6 +113,27 @@ export function readCodexRollout(filePath) {
101
113
  const ctxWindow = info?.model_context_window;
102
114
  lastContextLimit = typeof ctxWindow === 'number' ? ctxWindow : undefined;
103
115
  }
116
+ // 压缩后的水位:压缩收尾 token_count 全 0(raw 不满足上面 zero-filter),
117
+ // 但 total_tokens 表达真实窗口;之后若出现普通 turn(input>0),
118
+ // 水位回归该 turn,post-compact 值失效。
119
+ if (compactCount > 0 && raw) {
120
+ if (raw.total_tokens &&
121
+ !raw.input_tokens &&
122
+ !raw.cached_input_tokens &&
123
+ !raw.output_tokens) {
124
+ compactPostContextLength = raw.total_tokens;
125
+ }
126
+ else if (raw.input_tokens > 0) {
127
+ compactPostContextLength = undefined;
128
+ }
129
+ }
130
+ }
131
+ }
132
+ else if (parsed.type === 'compacted') {
133
+ // 顶层 compacted 事件:压缩次数 +1,压缩前水位取最近一次非零 token_count。
134
+ compactCount++;
135
+ if (lastRawUsage?.total_tokens !== undefined) {
136
+ compactPreContextLength = lastRawUsage.total_tokens;
104
137
  }
105
138
  }
106
139
  else if (parsed.type === 'response_item' && parsed.payload) {
@@ -170,6 +203,16 @@ export function readCodexRollout(filePath) {
170
203
  usage.cumulativeCacheReadTokens = cumCached;
171
204
  usage.cumulativeCacheCreationTokens = 0; // Codex 不报告 cache creation
172
205
  }
206
+ // 压缩统计:会话内出现过 compacted 事件就计数(供 /resume 与 Compact 卡
207
+ // 展示);仅当会话以压缩收尾时(post-compact 水位存在),contextLength
208
+ // 覆盖为压缩后水位并暴露压缩前水位——后续还有普通 turn 时水位回归该 turn。
209
+ if (compactCount > 0) {
210
+ usage.compactCount = compactCount;
211
+ if (compactPostContextLength !== undefined) {
212
+ usage.contextLength = compactPostContextLength;
213
+ usage.compactPreContextLength = compactPreContextLength;
214
+ }
215
+ }
173
216
  }
174
217
  return {
175
218
  threadId: sessionId,