@yuandc/aica 0.1.0

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 (49) hide show
  1. package/README.md +9 -0
  2. package/dist/acp/agent.js +54 -0
  3. package/dist/acp/client/acp-client.js +102 -0
  4. package/dist/acp/client/acp-content.js +13 -0
  5. package/dist/acp/client/acp-events.js +106 -0
  6. package/dist/acp/client/acp-process.js +34 -0
  7. package/dist/acp/client/acp-runtime-pool.js +248 -0
  8. package/dist/acp/client/context-usage.js +29 -0
  9. package/dist/acp/client/json-rpc.js +128 -0
  10. package/dist/acp/provider-types.js +1 -0
  11. package/dist/acp/providers/codex/codex-process.js +51 -0
  12. package/dist/acp/providers/codex/events.js +1473 -0
  13. package/dist/acp/providers/codex/permissions.js +49 -0
  14. package/dist/acp/providers/codex/provider.js +376 -0
  15. package/dist/acp/providers/codex-acp/adapter.js +947 -0
  16. package/dist/acp/providers/codex-acp/context-maintenance.js +148 -0
  17. package/dist/acp/providers/codex-acp/launch.js +35 -0
  18. package/dist/acp/providers/codex-acp/provider.js +486 -0
  19. package/dist/acp/providers/mimo/provider.js +448 -0
  20. package/dist/acp/providers/opencode/provider.js +489 -0
  21. package/dist/acp/providers/registry.js +23 -0
  22. package/dist/acp/standard-events.js +167 -0
  23. package/dist/commands/start.js +137 -0
  24. package/dist/commands/worker-auth.js +100 -0
  25. package/dist/commands/worker-project.js +57 -0
  26. package/dist/core/aca-config.js +74 -0
  27. package/dist/core/aca-server-client.js +57 -0
  28. package/dist/core/acp-event-coalescer.js +108 -0
  29. package/dist/core/acp-event-upload-filter.js +16 -0
  30. package/dist/core/acp-orphan-cleanup.js +91 -0
  31. package/dist/core/affected-files.js +268 -0
  32. package/dist/core/auth.js +36 -0
  33. package/dist/core/file-transfer-worker.js +169 -0
  34. package/dist/core/fs.js +28 -0
  35. package/dist/core/heartbeat.js +578 -0
  36. package/dist/core/job-permission-policy.js +42 -0
  37. package/dist/core/job-worker.js +749 -0
  38. package/dist/core/logger.js +42 -0
  39. package/dist/core/long-poll-worker.js +26 -0
  40. package/dist/core/machine-filesystem-worker.js +352 -0
  41. package/dist/core/paths.js +26 -0
  42. package/dist/core/process-identity.js +34 -0
  43. package/dist/core/process.js +33 -0
  44. package/dist/core/provider-health.js +54 -0
  45. package/dist/core/runtime-options.js +38 -0
  46. package/dist/core/worktree.js +95 -0
  47. package/dist/worker-cli.js +27 -0
  48. package/dist/worker-single-cli.js +17 -0
  49. package/package.json +35 -0
@@ -0,0 +1,947 @@
1
+ import { JsonLineRpcClient } from "../../client/json-rpc.js";
2
+ import { startCodexAppServer } from "../codex/codex-process.js";
3
+ import { codexAcpSessionUpdatesFromNotification } from "../codex/events.js";
4
+ import { isCodexApprovalRequest, resolveCodexApprovalRequest } from "../codex/permissions.js";
5
+ import { buildCodexContinuityInstructions, codexTokenUsageFromNotification, decideCodexContextMaintenance, readCodexContextMaintenancePolicy, rolloutFileSize } from "./context-maintenance.js";
6
+ class CodexAcpAdapter {
7
+ lineBuffer = "";
8
+ started = null;
9
+ rpc = null;
10
+ // ACP 请求可能在初始化完成前并发到达。用共享 Promise 保证一个 Adapter
11
+ // 只启动一个 Codex app-server,避免 thread/start 和 turn/start 落到不同进程。
12
+ codexStartupPromise = null;
13
+ initialized = false;
14
+ currentSessionId = null;
15
+ currentCwd = process.cwd();
16
+ promptActive = false;
17
+ currentTurnId = null;
18
+ nextParentRequestId = 1;
19
+ pendingParentRequests = new Map();
20
+ completedTurns = new Map();
21
+ pendingTurnCompletions = new Map();
22
+ agentMessagePhases = new Map();
23
+ config = {};
24
+ sessionAliases = new Map();
25
+ currentAffectedFiles = new Set();
26
+ latestTokenUsage = null;
27
+ currentRolloutPath = null;
28
+ threadTurnCount = 0;
29
+ currentPromptText = "";
30
+ currentAssistantText = "";
31
+ previousUserText = "";
32
+ previousAssistantText = "";
33
+ previousAffectedFiles = new Set();
34
+ maintenanceActive = false;
35
+ pendingCompaction = null;
36
+ shutdownPromise = null;
37
+ constructor() {
38
+ process.stdin.setEncoding("utf8");
39
+ process.stdin.on("data", (chunk) => this.handleInput(String(chunk)));
40
+ const parentDisconnected = () => {
41
+ void this.shutdown().finally(() => process.exit(0));
42
+ };
43
+ process.stdin.once("end", parentDisconnected);
44
+ process.stdin.once("close", parentDisconnected);
45
+ process.stdin.once("error", parentDisconnected);
46
+ process.once("SIGTERM", () => {
47
+ void this.shutdown().finally(() => process.exit(0));
48
+ });
49
+ process.once("SIGINT", () => {
50
+ void this.shutdown().finally(() => process.exit(130));
51
+ });
52
+ }
53
+ handleInput(chunk) {
54
+ this.lineBuffer += chunk;
55
+ const lines = this.lineBuffer.split("\n");
56
+ this.lineBuffer = lines.pop() ?? "";
57
+ for (const line of lines) {
58
+ const trimmed = line.trim();
59
+ if (!trimmed)
60
+ continue;
61
+ let message;
62
+ try {
63
+ message = JSON.parse(trimmed);
64
+ }
65
+ catch {
66
+ continue;
67
+ }
68
+ void this.handleMessage(message);
69
+ }
70
+ }
71
+ async handleMessage(message) {
72
+ if ("id" in message && ("result" in message || "error" in message) && typeof message.method !== "string") {
73
+ this.handleParentResponse(message);
74
+ return;
75
+ }
76
+ const method = typeof message.method === "string" ? message.method : "";
77
+ const id = message.id;
78
+ if (!method)
79
+ return;
80
+ if (id === undefined) {
81
+ await this.handleNotification(method, objectOrEmpty(message.params));
82
+ return;
83
+ }
84
+ try {
85
+ const result = await this.handleRequest(method, objectOrEmpty(message.params));
86
+ this.write({ jsonrpc: "2.0", id, result });
87
+ }
88
+ catch (error) {
89
+ this.write({
90
+ jsonrpc: "2.0",
91
+ id,
92
+ error: {
93
+ code: -32603,
94
+ message: error instanceof Error ? error.message : String(error)
95
+ }
96
+ });
97
+ }
98
+ }
99
+ async handleNotification(method, params) {
100
+ if (method === "session/cancel") {
101
+ await this.cancel(params);
102
+ }
103
+ }
104
+ async handleRequest(method, params) {
105
+ switch (method) {
106
+ case "initialize":
107
+ return this.initializeResponse();
108
+ case "session/new":
109
+ return this.newSession(params);
110
+ case "session/resume":
111
+ case "session/load":
112
+ return this.resumeSession(params);
113
+ case "session/prompt":
114
+ return this.prompt(params);
115
+ case "session/compact":
116
+ return this.manualCompact(params);
117
+ case "session/cancel":
118
+ await this.cancel(params);
119
+ return {};
120
+ case "session/close":
121
+ await this.closeSession(params);
122
+ return {};
123
+ case "session/set_config_option":
124
+ return this.setConfigOption(params);
125
+ case "session/set_mode":
126
+ return this.setMode(params);
127
+ case "fs/read_text_file":
128
+ case "fs/write_text_file":
129
+ case "terminal/create":
130
+ case "terminal/output":
131
+ case "terminal/wait_for_exit":
132
+ case "terminal/kill":
133
+ case "terminal/release":
134
+ return {};
135
+ default:
136
+ throw new Error(`Unsupported ACP method: ${method}`);
137
+ }
138
+ }
139
+ initializeResponse() {
140
+ return {
141
+ protocolVersion: 1,
142
+ agentCapabilities: {
143
+ loadSession: true,
144
+ sessionCapabilities: {
145
+ resume: true,
146
+ close: true
147
+ }
148
+ }
149
+ };
150
+ }
151
+ async newSession(params) {
152
+ const cwd = stringField(params, "cwd") || process.cwd();
153
+ this.applyInitialConfig(params);
154
+ await this.ensureCodex(cwd);
155
+ const created = await this.requireRpc().request("thread/start", {
156
+ cwd,
157
+ approvalsReviewer: "user",
158
+ ...this.codexConfig(),
159
+ ephemeral: false,
160
+ threadSource: "aca-codex-acp"
161
+ }, 60_000);
162
+ const sessionId = threadIdFromResponse(created);
163
+ if (!sessionId)
164
+ throw new Error("Codex thread/start did not return thread id");
165
+ this.currentSessionId = sessionId;
166
+ this.currentCwd = cwd;
167
+ this.captureThreadMetadata(created);
168
+ return this.sessionResponse(sessionId);
169
+ }
170
+ async resumeSession(params) {
171
+ const sessionId = stringField(params, "sessionId");
172
+ if (!sessionId)
173
+ throw new Error("sessionId is required");
174
+ const cwd = stringField(params, "cwd") || this.currentCwd;
175
+ this.applyInitialConfig(params);
176
+ await this.ensureCodex(cwd);
177
+ const resumed = await this.requireRpc().request("thread/resume", {
178
+ threadId: sessionId,
179
+ cwd,
180
+ approvalsReviewer: "user",
181
+ ...this.codexConfig()
182
+ }, 60_000);
183
+ this.currentSessionId = threadIdFromResponse(resumed) || sessionId;
184
+ this.currentCwd = cwd;
185
+ this.captureThreadMetadata(resumed);
186
+ return this.sessionResponse(this.currentSessionId);
187
+ }
188
+ async prompt(params) {
189
+ const requestedSessionId = stringField(params, "sessionId") || this.currentSessionId;
190
+ let sessionId = this.resolveSessionAlias(requestedSessionId);
191
+ if (!sessionId)
192
+ throw new Error("sessionId is required");
193
+ const prompt = Array.isArray(params.prompt) ? params.prompt : [];
194
+ const rpc = this.requireRpc();
195
+ this.currentSessionId = sessionId;
196
+ this.promptActive = true;
197
+ this.currentTurnId = null;
198
+ this.currentPromptText = acpPromptText(prompt);
199
+ this.currentAssistantText = "";
200
+ this.currentAffectedFiles.clear();
201
+ this.agentMessagePhases.clear();
202
+ let promptResponse;
203
+ try {
204
+ promptResponse = await rpc.request("turn/start", {
205
+ threadId: sessionId,
206
+ clientUserMessageId: `aca-acp-${Date.now()}-${Math.random().toString(16).slice(2)}`,
207
+ input: acpPromptToCodexInput(prompt),
208
+ cwd: this.currentCwd,
209
+ ...turnConfigFromConfig(this.config)
210
+ }, readPromptTimeoutMs());
211
+ const turnId = String(promptResponse?.turn?.id ?? "");
212
+ this.currentTurnId = turnId || null;
213
+ const turn = turnId ? await this.waitForTurnCompleted(turnId, readPromptTimeoutMs()) : promptResponse?.turn;
214
+ this.threadTurnCount += 1;
215
+ this.previousUserText = this.currentPromptText;
216
+ this.previousAssistantText = this.currentAssistantText;
217
+ this.previousAffectedFiles.clear();
218
+ for (const filePath of this.currentAffectedFiles)
219
+ this.previousAffectedFiles.add(filePath);
220
+ return {
221
+ stopReason: "end_turn",
222
+ sessionId: this.currentSessionId,
223
+ turn,
224
+ _meta: {
225
+ acaContextUsage: contextUsageFromTokenSnapshot(this.latestTokenUsage)
226
+ }
227
+ };
228
+ }
229
+ finally {
230
+ this.promptActive = false;
231
+ this.currentTurnId = null;
232
+ }
233
+ }
234
+ async cancel(params) {
235
+ const sessionId = stringField(params, "sessionId") || this.currentSessionId;
236
+ if (!sessionId || !this.rpc)
237
+ return;
238
+ await this.rpc.request("turn/interrupt", { threadId: sessionId }).catch(() => void 0);
239
+ if (this.currentTurnId) {
240
+ const pending = this.pendingTurnCompletions.get(this.currentTurnId);
241
+ if (pending) {
242
+ this.pendingTurnCompletions.delete(this.currentTurnId);
243
+ clearTimeout(pending.timer);
244
+ pending.reject(new Error("Codex prompt cancelled"));
245
+ }
246
+ }
247
+ }
248
+ async manualCompact(params) {
249
+ const requestedSessionId = stringField(params, "sessionId") || this.currentSessionId;
250
+ const sessionId = this.resolveSessionAlias(requestedSessionId);
251
+ if (!sessionId)
252
+ throw new Error("sessionId is required");
253
+ if (this.promptActive || this.maintenanceActive)
254
+ throw new Error("Codex session is busy");
255
+ this.currentSessionId = sessionId;
256
+ const maintenance = await this.maintainContext("manual", {
257
+ userRequest: this.previousUserText,
258
+ assistantResponse: this.previousAssistantText,
259
+ affectedFiles: [...this.previousAffectedFiles]
260
+ }, {
261
+ force: true,
262
+ allowRollover: params.allowRollover === true
263
+ });
264
+ return {
265
+ sessionId: this.currentSessionId,
266
+ maintenance,
267
+ contextUsage: contextUsageFromMaintenance(maintenance)
268
+ };
269
+ }
270
+ async closeSession(_params) {
271
+ await this.shutdown();
272
+ }
273
+ async ensureCodex(cwd) {
274
+ if (this.rpc && this.started && this.started.child.exitCode === null && this.started.child.signalCode === null)
275
+ return;
276
+ if (this.codexStartupPromise) {
277
+ await this.codexStartupPromise;
278
+ return;
279
+ }
280
+ const startup = (async () => {
281
+ if (this.rpc && this.started && this.started.child.exitCode === null && this.started.child.signalCode === null)
282
+ return;
283
+ const started = startCodexAppServer(cwd);
284
+ const rpc = new JsonLineRpcClient(started.child, {
285
+ peerName: "Codex app-server",
286
+ onNotification: (method, params) => this.handleCodexNotification(method, params),
287
+ onRequest: async (method, params) => this.handleCodexRequest(method, params)
288
+ });
289
+ this.started = started;
290
+ this.rpc = rpc;
291
+ this.initialized = false;
292
+ await rpc.request("initialize", {
293
+ clientInfo: { name: "aca-codex-acp", version: "0.1.0" },
294
+ capabilities: {
295
+ experimentalApi: true,
296
+ requestAttestation: false
297
+ }
298
+ }, 60_000);
299
+ this.initialized = true;
300
+ })();
301
+ this.codexStartupPromise = startup;
302
+ try {
303
+ await startup;
304
+ }
305
+ catch (error) {
306
+ if (this.rpc && this.started && !this.initialized) {
307
+ await waitForClose(this.started.child, 1_000);
308
+ }
309
+ this.rpc = null;
310
+ this.started = null;
311
+ throw error;
312
+ }
313
+ finally {
314
+ if (this.codexStartupPromise === startup)
315
+ this.codexStartupPromise = null;
316
+ }
317
+ }
318
+ handleCodexNotification(method, params) {
319
+ this.captureContextNotification(method, params);
320
+ if (this.maintenanceActive)
321
+ return;
322
+ if (!this.promptActive && method !== "turn/completed")
323
+ return;
324
+ this.rpc?.refreshPendingRequestTimeout("turn/start");
325
+ this.captureAgentMessagePhase(method, params);
326
+ if (method === "turn/completed")
327
+ this.resolveTurnCompleted(params);
328
+ for (const update of codexAcpSessionUpdatesFromNotification(method, params, { agentMessagePhases: this.agentMessagePhases })) {
329
+ this.captureUpdateEvidence(update);
330
+ this.notify("session/update", {
331
+ sessionId: this.currentSessionId,
332
+ update
333
+ });
334
+ }
335
+ }
336
+ async handleCodexRequest(method, params) {
337
+ if (isCodexApprovalRequest(method)) {
338
+ return resolveCodexApprovalRequest(method, params, async (request) => {
339
+ const response = await this.requestParent("session/request_permission", {
340
+ sessionId: this.currentSessionId,
341
+ request: {
342
+ ...request,
343
+ params,
344
+ method
345
+ }
346
+ });
347
+ const record = objectOrEmpty(response);
348
+ const outcome = objectOrEmpty(record.outcome);
349
+ if (outcome.outcome === "selected" && typeof outcome.optionId === "string") {
350
+ return { outcome: { outcome: "selected", optionId: outcome.optionId } };
351
+ }
352
+ return { outcome: { outcome: "cancelled" } };
353
+ });
354
+ }
355
+ if (isCodexUserInputRequest(method)) {
356
+ const response = await this.requestParent("elicitation/create", {
357
+ sessionId: this.currentSessionId,
358
+ request: {
359
+ ...objectOrEmpty(params),
360
+ method
361
+ }
362
+ });
363
+ const record = objectOrEmpty(response);
364
+ const outcome = objectOrEmpty(record.outcome);
365
+ if (outcome.outcome === "submitted") {
366
+ return {
367
+ outcome: {
368
+ outcome: "submitted",
369
+ value: typeof outcome.value === "string" ? outcome.value : ""
370
+ }
371
+ };
372
+ }
373
+ return { outcome: { outcome: "cancelled" } };
374
+ }
375
+ return {};
376
+ }
377
+ captureAgentMessagePhase(method, params) {
378
+ if (method !== "item/started" && method !== "item/completed")
379
+ return;
380
+ const item = objectField(params, "item");
381
+ if (stringField(item, "type") !== "agentMessage")
382
+ return;
383
+ const id = stringField(item, "id");
384
+ if (!id)
385
+ return;
386
+ this.agentMessagePhases.set(id, stringField(item, "phase"));
387
+ }
388
+ waitForTurnCompleted(turnId, timeoutMs) {
389
+ const completed = this.completedTurns.get(turnId);
390
+ if (completed)
391
+ return Promise.resolve(completed);
392
+ return new Promise((resolve, reject) => {
393
+ const timer = setTimeout(() => {
394
+ this.pendingTurnCompletions.delete(turnId);
395
+ reject(new Error("Codex app-server request timed out while waiting for turn/completed"));
396
+ }, timeoutMs);
397
+ timer.unref();
398
+ this.pendingTurnCompletions.set(turnId, { resolve, reject, timer });
399
+ });
400
+ }
401
+ resolveTurnCompleted(params) {
402
+ const turn = objectField(params, "turn");
403
+ const turnId = stringField(turn, "id");
404
+ if (!turnId)
405
+ return;
406
+ this.completedTurns.set(turnId, turn);
407
+ const pending = this.pendingTurnCompletions.get(turnId);
408
+ if (!pending)
409
+ return;
410
+ this.pendingTurnCompletions.delete(turnId);
411
+ clearTimeout(pending.timer);
412
+ if (stringField(turn, "status") === "failed") {
413
+ pending.reject(new Error(`Codex turn failed: ${JSON.stringify(turn.error ?? {})}`));
414
+ return;
415
+ }
416
+ pending.resolve(turn);
417
+ }
418
+ async shutdown() {
419
+ if (this.shutdownPromise)
420
+ return this.shutdownPromise;
421
+ this.shutdownPromise = this.performShutdown();
422
+ return this.shutdownPromise;
423
+ }
424
+ async performShutdown() {
425
+ if (this.pendingCompaction) {
426
+ clearTimeout(this.pendingCompaction.timer);
427
+ this.pendingCompaction.reject(new Error("Codex ACP adapter closed"));
428
+ this.pendingCompaction = null;
429
+ }
430
+ for (const pending of this.pendingParentRequests.values()) {
431
+ clearTimeout(pending.timer);
432
+ pending.reject(new Error("Codex ACP adapter closed"));
433
+ }
434
+ this.pendingParentRequests.clear();
435
+ for (const pending of this.pendingTurnCompletions.values()) {
436
+ clearTimeout(pending.timer);
437
+ pending.reject(new Error("Codex ACP adapter closed"));
438
+ }
439
+ this.pendingTurnCompletions.clear();
440
+ const child = this.started?.child;
441
+ if (!child || child.exitCode !== null || child.signalCode !== null)
442
+ return;
443
+ child.stdin.end();
444
+ if (await waitForClose(child, 2_000))
445
+ return;
446
+ child.kill("SIGTERM");
447
+ if (await waitForClose(child, 1_000))
448
+ return;
449
+ child.kill("SIGKILL");
450
+ await waitForClose(child, 1_000);
451
+ }
452
+ sessionResponse(sessionId) {
453
+ return {
454
+ sessionId,
455
+ modes: {
456
+ currentModeId: "workspace-write",
457
+ availableModes: [
458
+ { id: "read-only", name: "Read Only" },
459
+ { id: "workspace-write", name: "Workspace Write" },
460
+ { id: "danger-full-access", name: "Full Access" }
461
+ ]
462
+ },
463
+ configOptions: [
464
+ { id: "model", category: "model", type: "string" },
465
+ { id: "approvalPolicy", category: "approval", type: "string" },
466
+ { id: "sandbox", category: "mode", type: "string" }
467
+ ]
468
+ };
469
+ }
470
+ requireRpc() {
471
+ if (!this.rpc)
472
+ throw new Error("Codex app-server is not started");
473
+ return this.rpc;
474
+ }
475
+ notify(method, params) {
476
+ this.write({ jsonrpc: "2.0", method, params });
477
+ }
478
+ requestParent(method, params, timeoutMs = 10 * 60 * 1000) {
479
+ const id = this.nextParentRequestId++;
480
+ return new Promise((resolve, reject) => {
481
+ const timer = setTimeout(() => {
482
+ this.pendingParentRequests.delete(id);
483
+ reject(new Error(`ACP client request timed out: ${method}`));
484
+ }, timeoutMs);
485
+ timer.unref();
486
+ this.pendingParentRequests.set(id, { resolve, reject, timer });
487
+ this.write({ jsonrpc: "2.0", id, method, params });
488
+ });
489
+ }
490
+ handleParentResponse(message) {
491
+ const id = Number(message.id);
492
+ const pending = this.pendingParentRequests.get(id);
493
+ if (!pending)
494
+ return;
495
+ this.pendingParentRequests.delete(id);
496
+ clearTimeout(pending.timer);
497
+ if ("error" in message) {
498
+ const error = objectOrEmpty(message.error);
499
+ pending.reject(new Error(stringField(error, "message") || "ACP client request failed"));
500
+ return;
501
+ }
502
+ pending.resolve(message.result);
503
+ }
504
+ setConfigOption(params) {
505
+ const configId = stringField(params, "configId");
506
+ const value = params.value;
507
+ if (!configId)
508
+ throw new Error("configId is required");
509
+ if (typeof value !== "string" && typeof value !== "boolean")
510
+ throw new Error("config value must be string or boolean");
511
+ this.config[configId] = value;
512
+ this.notify("session/update", {
513
+ sessionId: this.currentSessionId,
514
+ update: {
515
+ sessionUpdate: "config_option_update",
516
+ configId,
517
+ value
518
+ }
519
+ });
520
+ return {};
521
+ }
522
+ setMode(params) {
523
+ const modeId = stringField(params, "modeId");
524
+ if (!modeId)
525
+ throw new Error("modeId is required");
526
+ this.config.sandbox = modeId;
527
+ this.notify("session/update", {
528
+ sessionId: this.currentSessionId,
529
+ update: {
530
+ sessionUpdate: "current_mode_update",
531
+ modeId
532
+ }
533
+ });
534
+ return {};
535
+ }
536
+ applyInitialConfig(params) {
537
+ const config = objectField(params, "config");
538
+ for (const [key, value] of Object.entries(config)) {
539
+ if (typeof value === "string" || typeof value === "boolean") {
540
+ this.config[key] = value;
541
+ }
542
+ }
543
+ }
544
+ codexConfig() {
545
+ return {
546
+ ...(typeof this.config.model === "string" && this.config.model ? { model: this.config.model } : {}),
547
+ approvalPolicy: typeof this.config.approvalPolicy === "string" ? this.config.approvalPolicy : "on-request",
548
+ sandbox: typeof this.config.sandbox === "string" ? this.config.sandbox : "workspace-write"
549
+ };
550
+ }
551
+ captureContextNotification(method, params) {
552
+ if (method === "thread/tokenUsage/updated") {
553
+ this.latestTokenUsage = codexTokenUsageFromNotification(params) ?? this.latestTokenUsage;
554
+ }
555
+ if (method === "thread/compacted" && this.pendingCompaction) {
556
+ const pending = this.pendingCompaction;
557
+ this.pendingCompaction = null;
558
+ clearTimeout(pending.timer);
559
+ pending.resolve();
560
+ }
561
+ if (method === "item/completed") {
562
+ const item = objectField(params, "item");
563
+ if (stringField(item, "type") === "agentMessage" && stringField(item, "phase") === "final_answer") {
564
+ this.currentAssistantText = textFromUnknown(item) || this.currentAssistantText;
565
+ }
566
+ }
567
+ }
568
+ captureUpdateEvidence(update) {
569
+ if (String(update.sessionUpdate || "") === "agent_message_chunk") {
570
+ this.currentAssistantText += textFromUnknown(update.content);
571
+ }
572
+ collectAffectedPaths(update, this.currentAffectedFiles);
573
+ }
574
+ captureThreadMetadata(response) {
575
+ const thread = objectField(response, "thread");
576
+ this.currentRolloutPath = stringField(thread, "path") || null;
577
+ this.threadTurnCount = Array.isArray(thread.turns) ? thread.turns.length : 0;
578
+ const continuity = continuityFromTurns(thread.turns);
579
+ if (continuity.userRequest)
580
+ this.previousUserText = continuity.userRequest;
581
+ if (continuity.assistantResponse)
582
+ this.previousAssistantText = continuity.assistantResponse;
583
+ this.previousAffectedFiles.clear();
584
+ for (const filePath of continuity.affectedFiles)
585
+ this.previousAffectedFiles.add(filePath);
586
+ this.latestTokenUsage = null;
587
+ }
588
+ async maintainContext(stage, evidence, options = {}) {
589
+ const policy = readCodexContextMaintenancePolicy();
590
+ const beforeState = this.contextState();
591
+ const initialDecision = decideCodexContextMaintenance(beforeState, policy);
592
+ if ((!initialDecision.compact && !options.force) || !this.currentSessionId) {
593
+ return this.maintenanceResult({ stage, action: "none", reasons: initialDecision.reasons, beforeState });
594
+ }
595
+ const previousThreadId = this.currentSessionId;
596
+ this.notifyContextStatus("正在整理会话上下文", contextStatusDetail(beforeState));
597
+ this.maintenanceActive = true;
598
+ let compactFailed = false;
599
+ let compactError = "";
600
+ let afterState = beforeState;
601
+ let postCompactDecision = initialDecision;
602
+ let rotated = false;
603
+ let rolloverError = "";
604
+ try {
605
+ const previousUsageAtMs = beforeState.usage?.observedAtMs ?? 0;
606
+ await this.compactThread(previousThreadId, policy.compactTimeoutMs);
607
+ await this.waitForTokenUsageAfter(previousUsageAtMs, 2_000);
608
+ }
609
+ catch (error) {
610
+ compactFailed = true;
611
+ compactError = error instanceof Error ? error.message : String(error);
612
+ }
613
+ try {
614
+ afterState = this.contextState();
615
+ postCompactDecision = decideCodexContextMaintenance(afterState, policy, {
616
+ compactAttempted: true,
617
+ compactFailed
618
+ });
619
+ if (postCompactDecision.rollover && options.allowRollover !== false) {
620
+ this.notifyContextStatus("正在创建轻量会话", postCompactDecision.reasons.join(", "));
621
+ try {
622
+ await this.rotateThread(previousThreadId, evidence);
623
+ rotated = true;
624
+ }
625
+ catch (error) {
626
+ rolloverError = error instanceof Error ? error.message : String(error);
627
+ this.notifyContextStatus("轻量会话创建失败", rolloverError);
628
+ }
629
+ }
630
+ else if (!compactFailed) {
631
+ this.notifyContextStatus("会话上下文已压缩", contextStatusDetail(afterState));
632
+ }
633
+ }
634
+ finally {
635
+ this.maintenanceActive = false;
636
+ }
637
+ return this.maintenanceResult({
638
+ action: rotated ? "rollover" : rolloverError ? "rollover_failed" : compactFailed ? "compact_failed" : "compact",
639
+ stage,
640
+ reasons: postCompactDecision.reasons,
641
+ beforeState,
642
+ afterState,
643
+ compactError: [compactError, rolloverError].filter(Boolean).join("; "),
644
+ previousThreadId,
645
+ rolloverRecommended: postCompactDecision.rollover && options.allowRollover === false
646
+ });
647
+ }
648
+ compactThread(threadId, timeoutMs) {
649
+ if (this.pendingCompaction)
650
+ return Promise.reject(new Error("Codex context compaction is already running"));
651
+ return new Promise((resolve, reject) => {
652
+ const timer = setTimeout(() => {
653
+ if (this.pendingCompaction?.timer === timer)
654
+ this.pendingCompaction = null;
655
+ reject(new Error("Codex thread compaction timed out"));
656
+ }, timeoutMs);
657
+ timer.unref();
658
+ this.pendingCompaction = { resolve, reject, timer };
659
+ void this.requireRpc().request("thread/compact/start", { threadId }, timeoutMs).catch((error) => {
660
+ if (this.pendingCompaction?.timer !== timer)
661
+ return;
662
+ this.pendingCompaction = null;
663
+ clearTimeout(timer);
664
+ reject(error instanceof Error ? error : new Error(String(error)));
665
+ });
666
+ });
667
+ }
668
+ async rotateThread(previousThreadId, evidence) {
669
+ const continuityInstructions = buildCodexContinuityInstructions({
670
+ previousThreadId,
671
+ cwd: this.currentCwd,
672
+ userRequest: evidence.userRequest,
673
+ assistantResponse: evidence.assistantResponse,
674
+ affectedFiles: evidence.affectedFiles
675
+ });
676
+ const created = await this.requireRpc().request("thread/start", {
677
+ cwd: this.currentCwd,
678
+ approvalsReviewer: "user",
679
+ ...this.codexConfig(),
680
+ developerInstructions: continuityInstructions,
681
+ ephemeral: false,
682
+ threadSource: "aca-codex-acp-rollover"
683
+ }, 60_000);
684
+ const nextThreadId = threadIdFromResponse(created);
685
+ if (!nextThreadId)
686
+ throw new Error("Codex rollover thread/start did not return thread id");
687
+ this.sessionAliases.set(previousThreadId, nextThreadId);
688
+ this.currentSessionId = nextThreadId;
689
+ this.captureThreadMetadata(created);
690
+ }
691
+ async waitForTokenUsageAfter(observedAtMs, timeoutMs) {
692
+ const deadline = Date.now() + timeoutMs;
693
+ while (Date.now() < deadline) {
694
+ if ((this.latestTokenUsage?.observedAtMs ?? 0) > observedAtMs)
695
+ return;
696
+ await delay(50);
697
+ }
698
+ }
699
+ resolveSessionAlias(sessionId) {
700
+ if (!sessionId)
701
+ return null;
702
+ let current = sessionId;
703
+ const visited = new Set();
704
+ while (this.sessionAliases.has(current) && !visited.has(current)) {
705
+ visited.add(current);
706
+ current = this.sessionAliases.get(current) ?? current;
707
+ }
708
+ return current;
709
+ }
710
+ contextState() {
711
+ return {
712
+ usage: this.latestTokenUsage,
713
+ rolloutBytes: rolloutFileSize(this.currentRolloutPath),
714
+ turnCount: this.threadTurnCount
715
+ };
716
+ }
717
+ maintenanceResult(input) {
718
+ return {
719
+ stage: input.stage,
720
+ action: input.action,
721
+ reasons: input.reasons,
722
+ previousThreadId: input.previousThreadId ?? null,
723
+ sessionId: this.currentSessionId,
724
+ before: input.beforeState,
725
+ after: input.afterState ?? null,
726
+ compactError: input.compactError || null,
727
+ rolloverRecommended: input.rolloverRecommended === true
728
+ };
729
+ }
730
+ notifyContextStatus(label, detail) {
731
+ this.notify("session/update", {
732
+ sessionId: this.currentSessionId,
733
+ update: {
734
+ sessionUpdate: "aca_context_maintenance",
735
+ label,
736
+ detail
737
+ }
738
+ });
739
+ }
740
+ write(message) {
741
+ process.stdout.write(`${JSON.stringify(message)}\n`);
742
+ }
743
+ }
744
+ function acpPromptToCodexInput(prompt) {
745
+ return prompt.map((block) => {
746
+ const record = objectOrEmpty(block);
747
+ if (record.type === "image") {
748
+ const uri = stringField(record, "uri");
749
+ if (uri)
750
+ return { type: "localImage", path: uri };
751
+ const data = stringField(record, "data");
752
+ const mimeType = stringField(record, "mimeType") || "image/png";
753
+ return { type: "image", url: `data:${mimeType};base64,${data}` };
754
+ }
755
+ return { type: "text", text: stringField(record, "text"), text_elements: [] };
756
+ });
757
+ }
758
+ function acpPromptText(prompt) {
759
+ return prompt.map((block) => stringField(objectOrEmpty(block), "text")).filter(Boolean).join("\n\n");
760
+ }
761
+ function turnConfigFromConfig(config) {
762
+ const result = {};
763
+ if (typeof config.model === "string" && config.model)
764
+ result.model = config.model;
765
+ if (typeof config.approvalPolicy === "string" && config.approvalPolicy)
766
+ result.approvalPolicy = config.approvalPolicy;
767
+ if (typeof config.sandbox === "string" && config.sandbox)
768
+ result.sandbox = config.sandbox;
769
+ const effort = config.reasoningEffort ?? config.model_reasoning_effort ?? config.effort;
770
+ if (typeof effort === "string" && effort)
771
+ result.effort = effort;
772
+ return result;
773
+ }
774
+ function threadIdFromResponse(value) {
775
+ const thread = objectField(value, "thread");
776
+ return stringField(thread, "id") || null;
777
+ }
778
+ function objectField(value, key) {
779
+ const record = objectOrEmpty(value);
780
+ return objectOrEmpty(record[key]);
781
+ }
782
+ function objectOrEmpty(value) {
783
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
784
+ }
785
+ function textFromUnknown(value) {
786
+ if (typeof value === "string")
787
+ return value;
788
+ if (Array.isArray(value))
789
+ return value.map(textFromUnknown).filter(Boolean).join("");
790
+ if (!value || typeof value !== "object")
791
+ return "";
792
+ const record = objectOrEmpty(value);
793
+ for (const key of ["text", "message", "content", "output_text"]) {
794
+ const text = textFromUnknown(record[key]);
795
+ if (text)
796
+ return text;
797
+ }
798
+ return "";
799
+ }
800
+ function collectAffectedPaths(value, sink) {
801
+ if (Array.isArray(value)) {
802
+ for (const item of value)
803
+ collectAffectedPaths(item, sink);
804
+ return;
805
+ }
806
+ const record = objectOrEmpty(value);
807
+ const type = stringField(record, "type").toLowerCase();
808
+ const path = stringField(record, "path");
809
+ if (path && ["diff", "resource_link", "content"].includes(type))
810
+ sink.add(path);
811
+ if (Array.isArray(record.locations)) {
812
+ for (const location of record.locations) {
813
+ const locationPath = stringField(objectOrEmpty(location), "path");
814
+ if (locationPath)
815
+ sink.add(locationPath);
816
+ }
817
+ }
818
+ for (const nested of [record.content, record.update, record.rawInput, record.rawOutput]) {
819
+ if (nested && nested !== value)
820
+ collectAffectedPaths(nested, sink);
821
+ }
822
+ }
823
+ function continuityFromTurns(value) {
824
+ if (!Array.isArray(value) || value.length === 0)
825
+ return { userRequest: "", assistantResponse: "", affectedFiles: [] };
826
+ const latestTurn = value.at(-1);
827
+ const affectedFiles = new Set();
828
+ collectAffectedPaths(latestTurn, affectedFiles);
829
+ let userRequest = "";
830
+ let assistantResponse = "";
831
+ const visit = (item) => {
832
+ if (Array.isArray(item)) {
833
+ for (const child of item)
834
+ visit(child);
835
+ return;
836
+ }
837
+ if (!item || typeof item !== "object")
838
+ return;
839
+ const record = objectOrEmpty(item);
840
+ const type = stringField(record, "type").toLowerCase();
841
+ if (type === "usermessage")
842
+ userRequest = textFromUnknown(record) || userRequest;
843
+ if (type === "agentmessage")
844
+ assistantResponse = textFromUnknown(record) || assistantResponse;
845
+ for (const key of ["items", "content", "input"]) {
846
+ if (record[key])
847
+ visit(record[key]);
848
+ }
849
+ };
850
+ visit(latestTurn);
851
+ return { userRequest, assistantResponse, affectedFiles: [...affectedFiles] };
852
+ }
853
+ function contextUsageFromMaintenance(maintenance) {
854
+ const action = String(maintenance.action || "none");
855
+ const after = objectOrEmpty(maintenance.after);
856
+ const before = objectOrEmpty(maintenance.before);
857
+ const afterUsage = objectOrEmpty(after.usage);
858
+ const beforeUsage = objectOrEmpty(before.usage);
859
+ const usage = Object.keys(afterUsage).length > 0 ? afterUsage : beforeUsage;
860
+ const contextWindow = numberField(usage, "contextWindow");
861
+ if (action === "rollover") {
862
+ return {
863
+ usedTokens: 0,
864
+ inputTokens: 0,
865
+ contextWindow,
866
+ ratio: 0,
867
+ observedAtMs: Date.now(),
868
+ state: "new_thread",
869
+ maintenanceAction: action
870
+ };
871
+ }
872
+ if (Object.keys(usage).length === 0)
873
+ return null;
874
+ const totalTokens = numberField(usage, "totalTokens");
875
+ const inputTokens = numberField(usage, "inputTokens");
876
+ return {
877
+ usedTokens: totalTokens || inputTokens,
878
+ inputTokens,
879
+ contextWindow,
880
+ ratio: contextWindow > 0 ? (totalTokens || inputTokens) / contextWindow : 0,
881
+ observedAtMs: numberField(usage, "observedAtMs") || Date.now(),
882
+ state: action === "compact" ? "compacted" : "active",
883
+ maintenanceAction: action
884
+ };
885
+ }
886
+ function contextUsageFromTokenSnapshot(usage) {
887
+ if (!usage)
888
+ return null;
889
+ return {
890
+ usedTokens: usage.totalTokens || usage.inputTokens,
891
+ inputTokens: usage.inputTokens,
892
+ contextWindow: usage.contextWindow,
893
+ ratio: usage.contextWindow > 0 ? (usage.totalTokens || usage.inputTokens) / usage.contextWindow : 0,
894
+ observedAtMs: usage.observedAtMs,
895
+ state: "active",
896
+ maintenanceAction: null
897
+ };
898
+ }
899
+ function contextStatusDetail(state) {
900
+ const tokens = state.usage ? `${state.usage.inputTokens} tokens` : "token unknown";
901
+ const rollout = state.rolloutBytes > 0 ? `${Math.round(state.rolloutBytes / 1024 / 1024)}MB rollout` : "rollout unknown";
902
+ return `${tokens}, ${rollout}, ${state.turnCount} turns`;
903
+ }
904
+ function numberField(value, key) {
905
+ const item = objectOrEmpty(value)[key];
906
+ return typeof item === "number" && Number.isFinite(item) ? item : 0;
907
+ }
908
+ function stringField(value, key) {
909
+ const record = objectOrEmpty(value);
910
+ const item = record[key];
911
+ return typeof item === "string" ? item : "";
912
+ }
913
+ function isCodexUserInputRequest(method) {
914
+ const normalized = method.toLowerCase();
915
+ return normalized.includes("elicitation")
916
+ || normalized.includes("requestuserinput")
917
+ || normalized.includes("request_user_input")
918
+ || normalized.includes("input/request");
919
+ }
920
+ function readPromptTimeoutMs() {
921
+ const parsed = Number.parseInt(process.env.ACA_ACP_PROMPT_TIMEOUT_MS ?? "", 10);
922
+ return Number.isInteger(parsed) && parsed >= 30_000 ? parsed : 6 * 60 * 60 * 1000;
923
+ }
924
+ function waitForClose(child, timeoutMs) {
925
+ if (child.exitCode !== null || child.signalCode !== null)
926
+ return Promise.resolve(true);
927
+ return new Promise((resolve) => {
928
+ const timer = setTimeout(() => resolve(false), timeoutMs);
929
+ timer.unref();
930
+ child.once("close", () => {
931
+ clearTimeout(timer);
932
+ resolve(true);
933
+ });
934
+ });
935
+ }
936
+ function delay(ms) {
937
+ return new Promise((resolve) => setTimeout(resolve, ms));
938
+ }
939
+ export function runCodexAcpAdapter() {
940
+ new CodexAcpAdapter();
941
+ }
942
+ const entry = process.argv[1] || "";
943
+ // 单文件 Worker 由 worker-single-cli 显式调用;这里仅保留直接运行 adapter.js/ts
944
+ // 时的入口,避免同一个 SEA 进程创建两个 Adapter 实例。
945
+ if (/(?:^|[\\/])adapter\.(?:js|ts)$/.test(entry)) {
946
+ runCodexAcpAdapter();
947
+ }