remote-codex 0.11.45 → 0.11.49

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 (47) hide show
  1. package/README.md +80 -10
  2. package/apps/relay-server/dist/index.js +1 -1
  3. package/apps/supervisor-api/dist/index.js +8370 -1501
  4. package/apps/supervisor-web/dist/assets/c-BIGW1oBm.js +1 -0
  5. package/apps/supervisor-web/dist/assets/{core-DODVy7wn.js → core-B0prQzhr.js} +1 -1
  6. package/apps/supervisor-web/dist/assets/cpp-DIPi6g--.js +1 -0
  7. package/apps/supervisor-web/dist/assets/csharp-DSvCPggb.js +1 -0
  8. package/apps/supervisor-web/dist/assets/go-C27-OAKa.js +1 -0
  9. package/apps/supervisor-web/dist/assets/index-DZI1aSXo.js +22 -0
  10. package/apps/supervisor-web/dist/assets/index-Dy8PgXgw.css +1 -0
  11. package/apps/supervisor-web/dist/assets/java-CylS5w8V.js +1 -0
  12. package/apps/supervisor-web/dist/assets/{markdown-vendor-RZk8L7-L.js → markdown-vendor-BG6PurxI.js} +33 -33
  13. package/apps/supervisor-web/dist/assets/ruby-B6AkvBWc.js +1 -0
  14. package/apps/supervisor-web/dist/assets/rust-B1yitclQ.js +1 -0
  15. package/apps/supervisor-web/dist/assets/{shellscript-CEILq0vU.js → shellscript-DfDnw5Jg.js} +1 -1
  16. package/apps/supervisor-web/dist/assets/thread-ui-gcslNXur.js +3968 -0
  17. package/apps/supervisor-web/dist/assets/xml-sdJ4AIDG.js +1 -0
  18. package/apps/supervisor-web/dist/index.html +4 -4
  19. package/docs/windows-device-manager.zh.md +143 -0
  20. package/docs/windows-device-setup.zh.md +263 -0
  21. package/docs/windows-one-click-installer-research.zh.md +481 -0
  22. package/docs/windows.md +6 -0
  23. package/package.json +7 -2
  24. package/packages/acp/src/agent-catalog.test.ts +57 -0
  25. package/packages/acp/src/agent-catalog.ts +361 -0
  26. package/packages/acp/src/catalog-runtime.test.ts +99 -0
  27. package/packages/acp/src/catalog-runtime.ts +507 -0
  28. package/packages/acp/src/codex-environment.test.ts +58 -0
  29. package/packages/acp/src/codex-environment.ts +33 -0
  30. package/packages/acp/src/index.ts +5 -0
  31. package/packages/acp/src/item-mapper.test.ts +137 -0
  32. package/packages/acp/src/item-mapper.ts +473 -0
  33. package/packages/acp/src/runtimeAdapter.test.ts +79 -0
  34. package/packages/acp/src/runtimeAdapter.ts +1193 -0
  35. package/packages/acp/src/terminal-service.test.ts +31 -0
  36. package/packages/acp/src/terminal-service.ts +137 -0
  37. package/packages/agent-runtime/src/runtime-errors.ts +1 -0
  38. package/packages/agent-runtime/src/types.ts +8 -0
  39. package/packages/db/migrations/0030_thread_agent_id.sql +9 -0
  40. package/packages/db/src/repositories.ts +3 -0
  41. package/packages/db/src/schema.ts +1 -0
  42. package/packages/shared/src/agent-providers.ts +10 -1
  43. package/packages/shared/src/index.ts +24 -0
  44. package/scripts/windows/build-device-manager.ps1 +64 -0
  45. package/apps/supervisor-web/dist/assets/index-BO9S3vTX.css +0 -1
  46. package/apps/supervisor-web/dist/assets/index-GqVDOqbI.js +0 -22
  47. package/apps/supervisor-web/dist/assets/thread-ui-BWC_ljvN.js +0 -3714
@@ -0,0 +1,1193 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { Readable, Writable } from 'node:stream';
6
+ import type { ChildProcess } from 'node:child_process';
7
+
8
+ import * as acp from '@agentclientprotocol/sdk';
9
+
10
+ import {
11
+ AgentRuntimeError,
12
+ type AgentActionRequestResponseInput,
13
+ type AgentHistoryItem,
14
+ type AgentModel,
15
+ type AgentPendingProviderRequest,
16
+ type AgentProviderCapabilities,
17
+ type AgentProviderRequest,
18
+ type AgentProviderRequestMapping,
19
+ type AgentRuntime,
20
+ type AgentRuntimeEvent,
21
+ type AgentRuntimeManagementSchema,
22
+ type AgentRuntimeStatus,
23
+ type AgentSessionDetail,
24
+ type AgentSessionSummary,
25
+ type AgentTurn,
26
+ type InterruptAgentTurnInput,
27
+ type ResumeAgentSessionInput,
28
+ type StartAgentSessionInput,
29
+ type StartAgentSessionResult,
30
+ type StartAgentTurnInput,
31
+ } from '../../agent-runtime/src/index';
32
+ import {
33
+ parseCommandLine,
34
+ spawnProcess,
35
+ } from '../../process-runtime/src/index';
36
+ import type { AgentBackendInstallationDto } from '../../shared/src/index';
37
+ import {
38
+ AcpTurnItemMapper,
39
+ type AcpMappedItemUpdate,
40
+ } from './item-mapper';
41
+ import { AcpTerminalService } from './terminal-service';
42
+
43
+ interface AcpRuntimeOptions {
44
+ command: string;
45
+ startupTimeoutMs?: number;
46
+ env?: NodeJS.ProcessEnv;
47
+ clientInfo?: {
48
+ name: string;
49
+ title?: string;
50
+ version?: string;
51
+ };
52
+ }
53
+
54
+ interface AcpSessionState {
55
+ providerSessionId: string;
56
+ cwd: string;
57
+ title: string | null;
58
+ createdAt: string;
59
+ updatedAt: string;
60
+ model: string | null;
61
+ reasoningEffort: string | null;
62
+ sandboxMode: string | null;
63
+ status: AgentSessionSummary['status'];
64
+ turns: AgentTurn[];
65
+ activeMapper: AcpTurnItemMapper | null;
66
+ modes: acp.SessionModeState | null;
67
+ configOptions: acp.SessionConfigOption[];
68
+ availableCommands: acp.AvailableCommand[];
69
+ }
70
+
71
+ interface PendingPermission {
72
+ params: acp.RequestPermissionRequest;
73
+ resolve: (response: acp.RequestPermissionResponse) => void;
74
+ timer: NodeJS.Timeout;
75
+ }
76
+
77
+ const acpCapabilities: AgentProviderCapabilities = {
78
+ sessions: {
79
+ list: true,
80
+ read: true,
81
+ resume: true,
82
+ importLocal: false,
83
+ },
84
+ turns: {
85
+ start: true,
86
+ streamInput: false,
87
+ steer: false,
88
+ interrupt: true,
89
+ compact: false,
90
+ },
91
+ branching: {
92
+ fork: false,
93
+ hardRollback: false,
94
+ resumeAt: false,
95
+ rewindFiles: false,
96
+ },
97
+ controls: {
98
+ planMode: true,
99
+ permissionRequests: true,
100
+ sandboxMode: true,
101
+ performanceMode: false,
102
+ goals: false,
103
+ },
104
+ management: {
105
+ models: false,
106
+ mcpStatus: false,
107
+ skills: false,
108
+ hooks: false,
109
+ hookTrust: false,
110
+ hostConfigFiles: false,
111
+ providerSettings: false,
112
+ },
113
+ usage: {
114
+ contextWindow: true,
115
+ tokenUsage: true,
116
+ costUsd: false,
117
+ },
118
+ };
119
+
120
+ const acpManagementSchema: AgentRuntimeManagementSchema = {
121
+ hostConfigFiles: [],
122
+ toolboxItems: [],
123
+ hookCommandTemplates: [],
124
+ providerConfigFormat: 'none',
125
+ mcpConfigFormat: 'none',
126
+ configArchives: false,
127
+ buildRestart: false,
128
+ };
129
+
130
+ function errorMessage(error: unknown) {
131
+ return error instanceof Error ? error.message : String(error);
132
+ }
133
+
134
+ function cloneCapabilities(): AgentProviderCapabilities {
135
+ return structuredClone(acpCapabilities);
136
+ }
137
+
138
+ function isRecord(value: unknown): value is Record<string, unknown> {
139
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
140
+ }
141
+
142
+ function allSelectOptions(option: acp.SessionConfigOption) {
143
+ if (option.type !== 'select') {
144
+ return [];
145
+ }
146
+ return option.options.flatMap((entry) =>
147
+ 'options' in entry ? entry.options : [entry],
148
+ );
149
+ }
150
+
151
+ function configOptionByCategory(
152
+ options: acp.SessionConfigOption[],
153
+ category: 'model' | 'thought_level',
154
+ ) {
155
+ const hints = category === 'model'
156
+ ? ['model']
157
+ : ['thought', 'reasoning', 'effort'];
158
+ return options.find((option) =>
159
+ option.category === category ||
160
+ hints.some((hint) => option.id.toLowerCase().includes(hint)),
161
+ ) ?? null;
162
+ }
163
+
164
+ function normalizeAcpEffort(value: string | null | undefined) {
165
+ const normalized = value?.trim().toLowerCase().replace(/[\s-]+/g, '_');
166
+ switch (normalized) {
167
+ case 'none':
168
+ case 'minimal':
169
+ case 'low':
170
+ case 'medium':
171
+ case 'high':
172
+ case 'max':
173
+ case 'ultra':
174
+ return normalized;
175
+ case 'xhigh':
176
+ case 'extra_high':
177
+ return 'xhigh';
178
+ default:
179
+ return null;
180
+ }
181
+ }
182
+
183
+ function reasoningOptions(configOptions: acp.SessionConfigOption[]) {
184
+ const option = configOptionByCategory(configOptions, 'thought_level');
185
+ if (!option || option.type !== 'select') {
186
+ return {
187
+ efforts: [] as AgentModel['supportedReasoningEfforts'],
188
+ defaultEffort: null,
189
+ };
190
+ }
191
+ const efforts = allSelectOptions(option).flatMap((entry) => {
192
+ const effort = normalizeAcpEffort(entry.value);
193
+ return effort
194
+ ? [{ reasoningEffort: effort, description: entry.description ?? '' }]
195
+ : [];
196
+ });
197
+ return {
198
+ efforts: efforts.filter(
199
+ (entry, index) =>
200
+ efforts.findIndex((candidate) => candidate.reasoningEffort === entry.reasoningEffort) === index,
201
+ ),
202
+ defaultEffort: normalizeAcpEffort(option.currentValue),
203
+ };
204
+ }
205
+
206
+ function permissionOptions(params: acp.RequestPermissionRequest) {
207
+ return params.options.map((option) => ({
208
+ id: option.optionId,
209
+ name: option.name,
210
+ kind: option.kind,
211
+ }));
212
+ }
213
+
214
+ function selectedPermission(optionId: string): acp.RequestPermissionResponse {
215
+ return {
216
+ outcome: {
217
+ outcome: 'selected',
218
+ optionId,
219
+ },
220
+ };
221
+ }
222
+
223
+ function cancelledPermission(): acp.RequestPermissionResponse {
224
+ return { outcome: { outcome: 'cancelled' } };
225
+ }
226
+
227
+ function promptUsagePayload(usage: acp.Usage) {
228
+ const normalized = {
229
+ totalTokens: usage.totalTokens,
230
+ inputTokens: usage.inputTokens,
231
+ cachedInputTokens: usage.cachedReadTokens ?? 0,
232
+ cacheWriteInputTokens: usage.cachedWriteTokens ?? 0,
233
+ outputTokens: usage.outputTokens,
234
+ reasoningOutputTokens: usage.thoughtTokens ?? 0,
235
+ };
236
+ return {
237
+ total: normalized,
238
+ last: normalized,
239
+ };
240
+ }
241
+
242
+ function sessionDetail(state: AcpSessionState): AgentSessionDetail {
243
+ return {
244
+ provider: 'acp',
245
+ providerSessionId: state.providerSessionId,
246
+ cwd: state.cwd,
247
+ title: state.title,
248
+ preview: state.turns.at(-1)?.items.findLast((item) => item.kind === 'agentMessage')?.text ?? null,
249
+ createdAt: state.createdAt,
250
+ updatedAt: state.updatedAt,
251
+ status: state.status,
252
+ turns: state.turns,
253
+ totalTurnCount: state.turns.length,
254
+ };
255
+ }
256
+
257
+ function sessionSummaryFromInfo(info: acp.SessionInfo): AgentSessionSummary {
258
+ return {
259
+ provider: 'acp',
260
+ providerSessionId: info.sessionId,
261
+ cwd: info.cwd,
262
+ title: info.title ?? null,
263
+ preview: null,
264
+ createdAt: null,
265
+ updatedAt: info.updatedAt ?? null,
266
+ status: 'not_loaded',
267
+ rawSession: info,
268
+ };
269
+ }
270
+
271
+ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
272
+ readonly provider = 'acp' as const;
273
+ readonly displayName = 'ACP Agent';
274
+ readonly description = 'Generic Agent Client Protocol runtime over stdio.';
275
+ readonly capabilities = cloneCapabilities();
276
+ readonly managementSchema = acpManagementSchema;
277
+ readonly installation: AgentBackendInstallationDto = {
278
+ packageName: null,
279
+ installed: true,
280
+ installedVersion: null,
281
+ latestVersion: null,
282
+ installCommand: null,
283
+ updateCommand: null,
284
+ busy: false,
285
+ lastError: null,
286
+ };
287
+
288
+ private readonly sessions = new Map<string, AcpSessionState>();
289
+ private readonly knownSessions = new Map<string, AgentSessionSummary>();
290
+ private readonly pendingPermissions = new Map<number, PendingPermission>();
291
+ private readonly terminalService: AcpTerminalService;
292
+ private child: ChildProcess | null = null;
293
+ private connection: acp.ClientConnection | null = null;
294
+ private context: acp.ClientContext | null = null;
295
+ private initializeResponse: acp.InitializeResponse | null = null;
296
+ private startupPromise: Promise<void> | null = null;
297
+ private stopping = false;
298
+ private permissionSequence = 0;
299
+ private status: AgentRuntimeStatus = {
300
+ state: 'stopped',
301
+ transport: 'stdio',
302
+ lastStartedAt: null,
303
+ lastError: null,
304
+ restartCount: 0,
305
+ };
306
+
307
+ constructor(private readonly options: AcpRuntimeOptions) {
308
+ super();
309
+ this.terminalService = new AcpTerminalService(
310
+ (sessionId) => this.sessions.get(sessionId)?.cwd ?? null,
311
+ );
312
+ }
313
+
314
+ getStatus() {
315
+ return { ...this.status };
316
+ }
317
+
318
+ async start() {
319
+ if (this.status.state === 'ready') {
320
+ return;
321
+ }
322
+ if (this.startupPromise) {
323
+ return this.startupPromise;
324
+ }
325
+ this.startupPromise = this.startConnection();
326
+ try {
327
+ await this.startupPromise;
328
+ } finally {
329
+ this.startupPromise = null;
330
+ }
331
+ }
332
+
333
+ private async startConnection() {
334
+ this.stopping = false;
335
+ this.status = {
336
+ ...this.status,
337
+ state: 'starting',
338
+ lastError: null,
339
+ restartCount: this.status.lastStartedAt ? this.status.restartCount + 1 : 0,
340
+ };
341
+ this.emit('status', this.getStatus());
342
+
343
+ try {
344
+ const parsed = parseCommandLine(this.options.command);
345
+ const child = spawnProcess({
346
+ command: parsed.command,
347
+ args: parsed.args,
348
+ env: { ...process.env, ...this.options.env },
349
+ stdio: ['pipe', 'pipe', 'pipe'],
350
+ });
351
+ this.child = child;
352
+ child.stderr?.on('data', (chunk: Buffer | string) => {
353
+ this.emit('stderr', Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk);
354
+ });
355
+ child.on('error', (error) => this.markFailed(error));
356
+ child.on('close', (code, signal) => {
357
+ if (!this.stopping && this.status.state !== 'failed') {
358
+ this.markFailed(new Error(
359
+ `ACP agent exited unexpectedly (code ${code ?? 'null'}, signal ${signal ?? 'none'}).`,
360
+ ));
361
+ }
362
+ });
363
+ if (!child.stdin || !child.stdout) {
364
+ throw new Error('ACP agent did not expose stdio pipes.');
365
+ }
366
+
367
+ const stream = acp.ndJsonStream(
368
+ Writable.toWeb(child.stdin) as WritableStream<Uint8Array>,
369
+ Readable.toWeb(child.stdout) as ReadableStream<Uint8Array>,
370
+ );
371
+ const app = acp
372
+ .client({ name: this.options.clientInfo?.name ?? 'remote-codex-supervisor' })
373
+ .onRequest(acp.methods.client.session.requestPermission, (request) =>
374
+ this.requestPermission(request.params))
375
+ .onNotification(acp.methods.client.session.update, (notification) =>
376
+ this.handleSessionUpdate(notification.params))
377
+ .onRequest(acp.methods.client.fs.readTextFile, (request) =>
378
+ this.readTextFile(request.params))
379
+ .onRequest(acp.methods.client.fs.writeTextFile, (request) =>
380
+ this.writeTextFile(request.params))
381
+ .onRequest(acp.methods.client.terminal.create, (request) =>
382
+ this.terminalService.create(request.params))
383
+ .onRequest(acp.methods.client.terminal.output, (request) =>
384
+ this.terminalService.output(request.params))
385
+ .onRequest(acp.methods.client.terminal.waitForExit, (request) =>
386
+ this.terminalService.waitForExit(request.params))
387
+ .onRequest(acp.methods.client.terminal.kill, (request) =>
388
+ this.terminalService.kill(request.params))
389
+ .onRequest(acp.methods.client.terminal.release, (request) =>
390
+ this.terminalService.release(request.params));
391
+ this.connection = app.connect(stream);
392
+ this.context = this.connection.agent;
393
+ const initialize = this.context.request(acp.methods.agent.initialize, {
394
+ protocolVersion: acp.PROTOCOL_VERSION,
395
+ clientCapabilities: {
396
+ fs: { readTextFile: true, writeTextFile: true },
397
+ terminal: true,
398
+ session: {
399
+ compaction: {},
400
+ configOptions: { boolean: {} },
401
+ },
402
+ plan: {},
403
+ },
404
+ clientInfo: {
405
+ name: this.options.clientInfo?.name ?? 'remote-codex-supervisor',
406
+ version: this.options.clientInfo?.version ?? '0.1.0',
407
+ ...(this.options.clientInfo?.title ? { title: this.options.clientInfo.title } : {}),
408
+ },
409
+ }) as Promise<acp.InitializeResponse>;
410
+ const initializeResponse = await this.withStartupTimeout(initialize);
411
+ this.initializeResponse = initializeResponse;
412
+ this.applyAgentCapabilities(initializeResponse.agentCapabilities);
413
+ this.status = {
414
+ ...this.status,
415
+ state: 'ready',
416
+ lastStartedAt: new Date().toISOString(),
417
+ lastError: null,
418
+ };
419
+ this.installation.installed = true;
420
+ this.installation.installedVersion = initializeResponse.agentInfo
421
+ ? [initializeResponse.agentInfo.name, initializeResponse.agentInfo.version]
422
+ .filter(Boolean)
423
+ .join(' ')
424
+ : 'ACP';
425
+ this.installation.lastError = null;
426
+ this.emit('status', this.getStatus());
427
+ } catch (error) {
428
+ this.markFailed(error);
429
+ this.connection?.close(error);
430
+ this.child?.kill('SIGTERM');
431
+ this.connection = null;
432
+ this.context = null;
433
+ this.child = null;
434
+ throw new AgentRuntimeError(
435
+ `Unable to start ACP agent: ${errorMessage(error)}`,
436
+ 'acp',
437
+ 'provider_unavailable',
438
+ { command: this.options.command },
439
+ error,
440
+ );
441
+ }
442
+ }
443
+
444
+ async stop() {
445
+ this.stopping = true;
446
+ this.terminalService.stop();
447
+ for (const [id, permission] of this.pendingPermissions) {
448
+ clearTimeout(permission.timer);
449
+ permission.resolve(cancelledPermission());
450
+ this.pendingPermissions.delete(id);
451
+ }
452
+ this.connection?.close();
453
+ this.child?.kill('SIGTERM');
454
+ this.connection = null;
455
+ this.context = null;
456
+ this.initializeResponse = null;
457
+ this.child = null;
458
+ this.sessions.clear();
459
+ this.status = {
460
+ ...this.status,
461
+ state: 'stopped',
462
+ lastError: null,
463
+ };
464
+ this.emit('status', this.getStatus());
465
+ }
466
+
467
+ async listModels(): Promise<AgentModel[]> {
468
+ return [{
469
+ id: 'default',
470
+ model: 'default',
471
+ displayName: 'Agent default',
472
+ description: 'Use the model configured by the ACP agent.',
473
+ isDefault: true,
474
+ hidden: false,
475
+ supportedReasoningEfforts: [],
476
+ defaultReasoningEffort: null,
477
+ }];
478
+ }
479
+
480
+ async inspectModelOptions(cwd: string): Promise<AgentModel[]> {
481
+ const context = await this.requireContext();
482
+ const response = await context.request(acp.methods.agent.session.new, {
483
+ cwd: path.resolve(cwd),
484
+ mcpServers: [],
485
+ _meta: { yoloMode: false, remoteCodexCapabilityProbe: true },
486
+ });
487
+ let configOptions = response.configOptions ?? [];
488
+ try {
489
+ const modelOption = configOptionByCategory(configOptions, 'model');
490
+ if (!modelOption || modelOption.type !== 'select') {
491
+ const reasoning = reasoningOptions(configOptions);
492
+ return [{
493
+ id: 'default',
494
+ model: 'default',
495
+ displayName: 'Agent default',
496
+ description: 'Use the model configured by the ACP agent.',
497
+ isDefault: true,
498
+ hidden: false,
499
+ supportedReasoningEfforts: reasoning.efforts,
500
+ defaultReasoningEffort: reasoning.defaultEffort,
501
+ selectionKind: 'model',
502
+ }];
503
+ }
504
+
505
+ const models: AgentModel[] = [];
506
+ for (const option of allSelectOptions(modelOption)) {
507
+ if (option.value !== modelOption.currentValue) {
508
+ try {
509
+ const updated = await context.request(acp.methods.agent.session.setConfigOption, {
510
+ sessionId: response.sessionId,
511
+ configId: modelOption.id,
512
+ value: option.value,
513
+ });
514
+ configOptions = updated.configOptions;
515
+ } catch {
516
+ configOptions = response.configOptions ?? [];
517
+ }
518
+ }
519
+ const reasoning = reasoningOptions(configOptions);
520
+ models.push({
521
+ id: option.value,
522
+ model: option.value,
523
+ displayName: option.name || option.value,
524
+ description: option.description ?? '',
525
+ isDefault: option.value === modelOption.currentValue,
526
+ hidden: false,
527
+ supportedReasoningEfforts: reasoning.efforts,
528
+ defaultReasoningEffort: reasoning.defaultEffort,
529
+ selectionKind: 'model',
530
+ });
531
+ }
532
+ return models;
533
+ } finally {
534
+ if (this.initializeResponse?.agentCapabilities?.sessionCapabilities?.close) {
535
+ await context.request(acp.methods.agent.session.close, {
536
+ sessionId: response.sessionId,
537
+ }).catch(() => undefined);
538
+ }
539
+ }
540
+ }
541
+
542
+ async listSessions(): Promise<AgentSessionSummary[]> {
543
+ const context = await this.requireContext();
544
+ const canList = Boolean(this.initializeResponse?.agentCapabilities?.sessionCapabilities?.list);
545
+ if (!canList) {
546
+ return [...this.sessions.values()].map((state) => sessionDetail(state));
547
+ }
548
+
549
+ const sessions: AgentSessionSummary[] = [];
550
+ let cursor: string | null | undefined;
551
+ do {
552
+ const response = await context.request(acp.methods.agent.session.list, {
553
+ ...(cursor ? { cursor } : {}),
554
+ });
555
+ for (const info of response.sessions) {
556
+ const loaded = this.sessions.get(info.sessionId);
557
+ if (loaded) {
558
+ loaded.title = info.title ?? loaded.title;
559
+ loaded.updatedAt = info.updatedAt ?? loaded.updatedAt;
560
+ }
561
+ const summary = loaded
562
+ ? sessionDetail(loaded)
563
+ : sessionSummaryFromInfo(info);
564
+ sessions.push(summary);
565
+ this.knownSessions.set(summary.providerSessionId, summary);
566
+ }
567
+ cursor = response.nextCursor;
568
+ } while (cursor);
569
+ return sessions;
570
+ }
571
+
572
+ async listLoadedSessions() {
573
+ return [...this.sessions.keys()];
574
+ }
575
+
576
+ async readSession(
577
+ providerSessionId: string,
578
+ ): Promise<AgentSessionDetail> {
579
+ const state = this.sessions.get(providerSessionId);
580
+ if (state) {
581
+ return sessionDetail(state);
582
+ }
583
+ throw new AgentRuntimeError(
584
+ 'ACP session history is owned by the Remote Codex supervisor and is not materialized in this runtime process.',
585
+ 'acp',
586
+ 'request_failed',
587
+ { historyUnavailable: true, providerSessionId },
588
+ );
589
+ }
590
+
591
+ async startSession(input: StartAgentSessionInput): Promise<StartAgentSessionResult> {
592
+ const context = await this.requireContext();
593
+ const response = await context.request(acp.methods.agent.session.new, {
594
+ cwd: path.resolve(input.cwd),
595
+ mcpServers: [],
596
+ _meta: {
597
+ yoloMode: input.approvalMode === 'yolo',
598
+ },
599
+ });
600
+ const now = new Date().toISOString();
601
+ const state: AcpSessionState = {
602
+ providerSessionId: response.sessionId,
603
+ cwd: path.resolve(input.cwd),
604
+ title: null,
605
+ createdAt: now,
606
+ updatedAt: now,
607
+ model: input.model === 'default' ? null : input.model,
608
+ reasoningEffort: input.reasoningEffort ?? null,
609
+ sandboxMode: input.sandboxMode ?? null,
610
+ status: 'idle',
611
+ turns: [],
612
+ activeMapper: null,
613
+ modes: response.modes ?? null,
614
+ configOptions: response.configOptions ?? [],
615
+ availableCommands: [],
616
+ };
617
+ this.sessions.set(state.providerSessionId, state);
618
+ this.knownSessions.set(state.providerSessionId, sessionDetail(state));
619
+ await this.applySessionSettings(state, input.model, input.reasoningEffort, input.sandboxMode);
620
+ const session = sessionDetail(state);
621
+ return {
622
+ provider: 'acp',
623
+ agentId: null,
624
+ providerSessionId: state.providerSessionId,
625
+ model: state.model,
626
+ reasoningEffort: state.reasoningEffort,
627
+ sandboxMode: state.sandboxMode,
628
+ session,
629
+ rawSession: response,
630
+ };
631
+ }
632
+
633
+ async resumeSession(input: ResumeAgentSessionInput): Promise<StartAgentSessionResult> {
634
+ const existing = this.sessions.get(input.providerSessionId);
635
+ const state = existing ?? await this.restoreSession(input.providerSessionId);
636
+ await this.applySessionSettings(state, input.model, undefined, input.sandboxMode);
637
+ state.status = 'idle';
638
+ state.updatedAt = new Date().toISOString();
639
+ const session = sessionDetail(state);
640
+ return {
641
+ provider: 'acp',
642
+ providerSessionId: state.providerSessionId,
643
+ model: state.model,
644
+ reasoningEffort: state.reasoningEffort,
645
+ sandboxMode: state.sandboxMode,
646
+ session,
647
+ };
648
+ }
649
+
650
+ async startTurn(input: StartAgentTurnInput): Promise<AgentTurn> {
651
+ const state = this.sessions.get(input.providerSessionId);
652
+ if (!state) {
653
+ throw new AgentRuntimeError(
654
+ `ACP session is not loaded: ${input.providerSessionId}`,
655
+ 'acp',
656
+ 'request_failed',
657
+ );
658
+ }
659
+ if (state.activeMapper) {
660
+ throw new AgentRuntimeError('ACP session already has an active turn.', 'acp', 'request_failed');
661
+ }
662
+ await this.applySessionSettings(
663
+ state,
664
+ input.model,
665
+ input.reasoningEffort,
666
+ input.sandboxMode,
667
+ input.collaborationMode,
668
+ );
669
+
670
+ const turnId = input.displayTurnId ?? randomUUID();
671
+ const startedAt = new Date().toISOString();
672
+ const initialItems: AgentHistoryItem[] = input.hidden
673
+ ? []
674
+ : [{
675
+ id: `${turnId}:user`,
676
+ kind: 'userMessage',
677
+ text: input.displayPrompt ?? input.prompt,
678
+ createdAt: startedAt,
679
+ }];
680
+ const mapper = new AcpTurnItemMapper(turnId, initialItems);
681
+ const startedTurn = { ...mapper.turn(), startedAt };
682
+ state.activeMapper = mapper;
683
+ state.status = 'running';
684
+ state.updatedAt = startedAt;
685
+ state.turns.push(startedTurn);
686
+ this.emitRuntimeEvent({
687
+ type: 'turn.started',
688
+ provider: 'acp',
689
+ providerSessionId: state.providerSessionId,
690
+ turn: startedTurn,
691
+ });
692
+
693
+ const prompt = input.developerInstructions?.trim()
694
+ ? `${input.developerInstructions.trim()}\n\n${input.prompt}`
695
+ : input.prompt;
696
+ const context = await this.requireContext();
697
+ void context.request(acp.methods.agent.session.prompt, {
698
+ sessionId: state.providerSessionId,
699
+ prompt: [{ type: 'text', text: prompt }],
700
+ }).then(
701
+ (response) => this.completePrompt(state, mapper, response),
702
+ (error) => this.failPrompt(state, mapper, error),
703
+ );
704
+ return startedTurn;
705
+ }
706
+
707
+ async interruptTurn(input: InterruptAgentTurnInput): Promise<AgentTurn | null> {
708
+ const state = this.sessions.get(input.providerSessionId);
709
+ if (!state?.activeMapper || state.activeMapper.turnId !== input.providerTurnId) {
710
+ return null;
711
+ }
712
+ const context = await this.requireContext();
713
+ await context.notify(acp.methods.agent.session.cancel, {
714
+ sessionId: input.providerSessionId,
715
+ });
716
+ return state.activeMapper.turn('interrupted');
717
+ }
718
+
719
+ mapProviderRequest(
720
+ request: AgentProviderRequest,
721
+ options: { approvalMode: 'yolo' | 'guarded' },
722
+ ): AgentProviderRequestMapping | null {
723
+ if (request.method !== acp.methods.client.session.requestPermission || !isRecord(request.params)) {
724
+ return null;
725
+ }
726
+ const params = request.params as acp.RequestPermissionRequest;
727
+ const choices = permissionOptions(params);
728
+ const allow = choices.find((option) => option.kind === 'allow_always')
729
+ ?? choices.find((option) => option.kind === 'allow_once');
730
+ if (options.approvalMode === 'yolo' && allow) {
731
+ return {
732
+ providerRequestId: request.id,
733
+ providerSessionId: params.sessionId,
734
+ autoApprovedResult: selectedPermission(allow.id),
735
+ pendingRequest: null,
736
+ };
737
+ }
738
+
739
+ const turnId = this.sessions.get(params.sessionId)?.activeMapper?.turnId ?? null;
740
+ return {
741
+ providerRequestId: request.id,
742
+ providerSessionId: params.sessionId,
743
+ autoApprovedResult: null,
744
+ pendingRequest: {
745
+ providerRequestId: request.id,
746
+ responseKind: 'acpPermission',
747
+ responsePayload: { options: choices },
748
+ request: {
749
+ id: `acp-permission:${request.id}`,
750
+ kind: 'requestUserInput',
751
+ title: 'Permission required',
752
+ description: params.toolCall.title ?? null,
753
+ turnId,
754
+ itemId: params.toolCall.toolCallId,
755
+ createdAt: new Date().toISOString(),
756
+ questions: [{
757
+ id: 'permission',
758
+ header: 'Permission',
759
+ question: params.toolCall.title ?? 'Allow this tool call?',
760
+ isOther: false,
761
+ isSecret: false,
762
+ options: choices.map((choice) => ({
763
+ label: choice.name,
764
+ description: choice.kind.replaceAll('_', ' '),
765
+ })),
766
+ }],
767
+ },
768
+ },
769
+ };
770
+ }
771
+
772
+ buildProviderRequestResponse(
773
+ pending: AgentPendingProviderRequest,
774
+ input: AgentActionRequestResponseInput,
775
+ ) {
776
+ const options = Array.isArray(pending.responsePayload?.options)
777
+ ? pending.responsePayload.options.filter(isRecord)
778
+ : [];
779
+ const answer = Object.values(input.answers).flatMap((entry) => entry.answers)[0] ?? '';
780
+ const selected = options.find((option) => option.name === answer || option.id === answer);
781
+ return selected && typeof selected.id === 'string'
782
+ ? selectedPermission(selected.id)
783
+ : cancelledPermission();
784
+ }
785
+
786
+ respondToProviderRequest(id: string | number, result: unknown) {
787
+ const numericId = Number(id);
788
+ const pending = this.pendingPermissions.get(numericId);
789
+ if (!pending) {
790
+ return;
791
+ }
792
+ clearTimeout(pending.timer);
793
+ this.pendingPermissions.delete(numericId);
794
+ pending.resolve(result as acp.RequestPermissionResponse);
795
+ }
796
+
797
+ private async restoreSession(providerSessionId: string) {
798
+ const context = await this.requireContext();
799
+ let summary = this.knownSessions.get(providerSessionId) ?? null;
800
+ if (!summary) {
801
+ summary = (await this.listSessions()).find(
802
+ (candidate) => candidate.providerSessionId === providerSessionId,
803
+ ) ?? null;
804
+ }
805
+ if (!summary?.cwd) {
806
+ throw new AgentRuntimeError(
807
+ `ACP session working directory is unavailable: ${providerSessionId}`,
808
+ 'acp',
809
+ 'request_failed',
810
+ );
811
+ }
812
+
813
+ const now = new Date().toISOString();
814
+ const state: AcpSessionState = {
815
+ providerSessionId,
816
+ cwd: summary.cwd,
817
+ title: summary.title,
818
+ createdAt: summary.createdAt ?? now,
819
+ updatedAt: summary.updatedAt ?? now,
820
+ model: null,
821
+ reasoningEffort: null,
822
+ sandboxMode: null,
823
+ status: 'not_loaded',
824
+ turns: [],
825
+ activeMapper: null,
826
+ modes: null,
827
+ configOptions: [],
828
+ availableCommands: [],
829
+ };
830
+ this.sessions.set(providerSessionId, state);
831
+ try {
832
+ const capabilities = this.initializeResponse?.agentCapabilities;
833
+ if (capabilities?.loadSession) {
834
+ const response = await context.request(acp.methods.agent.session.load, {
835
+ sessionId: providerSessionId,
836
+ cwd: summary.cwd,
837
+ mcpServers: [],
838
+ });
839
+ state.modes = response.modes ?? null;
840
+ state.configOptions = response.configOptions ?? [];
841
+ } else if (capabilities?.sessionCapabilities?.resume) {
842
+ const response = await context.request(acp.methods.agent.session.resume, {
843
+ sessionId: providerSessionId,
844
+ cwd: summary.cwd,
845
+ mcpServers: [],
846
+ });
847
+ state.modes = response.modes ?? null;
848
+ state.configOptions = response.configOptions ?? [];
849
+ } else {
850
+ throw new Error('ACP agent does not support session/load or session/resume.');
851
+ }
852
+ state.status = 'idle';
853
+ return state;
854
+ } catch (error) {
855
+ this.sessions.delete(providerSessionId);
856
+ throw new AgentRuntimeError(
857
+ `Unable to restore ACP session: ${errorMessage(error)}`,
858
+ 'acp',
859
+ 'request_failed',
860
+ { providerSessionId },
861
+ error,
862
+ );
863
+ }
864
+ }
865
+
866
+ private async applySessionSettings(
867
+ state: AcpSessionState,
868
+ model?: string | null,
869
+ reasoningEffort?: string | null,
870
+ sandboxMode?: string | null,
871
+ collaborationMode?: 'default' | 'plan' | null,
872
+ ) {
873
+ if (model && model !== 'default') {
874
+ await this.setConfigOption(state, 'model', model);
875
+ state.model = model;
876
+ }
877
+ if (reasoningEffort) {
878
+ await this.setConfigOption(state, 'thought_level', reasoningEffort);
879
+ state.reasoningEffort = reasoningEffort;
880
+ }
881
+ if (sandboxMode !== undefined) {
882
+ state.sandboxMode = sandboxMode;
883
+ }
884
+
885
+ const modes = state.modes?.availableModes ?? [];
886
+ if (modes.length === 0) {
887
+ return;
888
+ }
889
+ const preferred = collaborationMode === 'plan'
890
+ ? ['plan', 'architect', 'ask']
891
+ : sandboxMode === 'read-only'
892
+ ? ['read-only', 'readonly', 'ask']
893
+ : sandboxMode === 'danger-full-access'
894
+ ? ['agent-full-access', 'full-access', 'yolo']
895
+ : ['agent', 'code', 'build'];
896
+ const mode = preferred
897
+ .map((id) => modes.find((candidate) => candidate.id.toLowerCase() === id))
898
+ .find(Boolean);
899
+ if (mode && mode.id !== state.modes?.currentModeId) {
900
+ const context = await this.requireContext();
901
+ await context.request(acp.methods.agent.session.setMode, {
902
+ sessionId: state.providerSessionId,
903
+ modeId: mode.id,
904
+ });
905
+ state.modes = { ...state.modes!, currentModeId: mode.id };
906
+ }
907
+ }
908
+
909
+ private async setConfigOption(
910
+ state: AcpSessionState,
911
+ category: 'model' | 'thought_level',
912
+ value: string,
913
+ ) {
914
+ const option = state.configOptions.find((candidate) =>
915
+ candidate.category === category || candidate.id.toLowerCase().includes(category === 'model' ? 'model' : 'thought'),
916
+ );
917
+ if ((!option || option.type !== 'select') && category === 'model') {
918
+ const context = await this.requireContext();
919
+ await context.request('session/set_model', {
920
+ sessionId: state.providerSessionId,
921
+ modelId: value,
922
+ });
923
+ return;
924
+ }
925
+ if (!option || option.type !== 'select') {
926
+ return;
927
+ }
928
+ const selected = allSelectOptions(option).find((candidate) =>
929
+ candidate.value === value || candidate.name.toLowerCase() === value.toLowerCase(),
930
+ );
931
+ if (!selected || selected.value === option.currentValue) {
932
+ return;
933
+ }
934
+ const context = await this.requireContext();
935
+ const response = await context.request(acp.methods.agent.session.setConfigOption, {
936
+ sessionId: state.providerSessionId,
937
+ configId: option.id,
938
+ value: selected.value,
939
+ });
940
+ state.configOptions = response.configOptions;
941
+ }
942
+
943
+ private handleSessionUpdate(notification: acp.SessionNotification) {
944
+ const state = this.sessions.get(notification.sessionId);
945
+ if (!state) {
946
+ return;
947
+ }
948
+ state.updatedAt = new Date().toISOString();
949
+ const update = notification.update;
950
+ if (update.sessionUpdate === 'config_option_update') {
951
+ state.configOptions = update.configOptions;
952
+ } else if (update.sessionUpdate === 'current_mode_update' && state.modes) {
953
+ state.modes = { ...state.modes, currentModeId: update.currentModeId };
954
+ } else if (update.sessionUpdate === 'available_commands_update') {
955
+ state.availableCommands = update.availableCommands;
956
+ } else if (update.sessionUpdate === 'session_info_update') {
957
+ if (update.title !== undefined) {
958
+ state.title = update.title;
959
+ if (update.title) {
960
+ this.emitRuntimeEvent({
961
+ type: 'session.title.updated',
962
+ provider: 'acp',
963
+ providerSessionId: state.providerSessionId,
964
+ title: update.title,
965
+ });
966
+ }
967
+ }
968
+ }
969
+
970
+ const mapper = state.activeMapper;
971
+ if (!mapper) {
972
+ return;
973
+ }
974
+ const mapped = mapper.apply(update);
975
+ for (const itemUpdate of mapped.itemUpdates) {
976
+ this.emitItemUpdate(state, mapper.turnId, itemUpdate);
977
+ }
978
+ for (const delta of mapped.outputDeltas) {
979
+ this.emitRuntimeEvent({
980
+ type: 'output.delta',
981
+ provider: 'acp',
982
+ providerSessionId: state.providerSessionId,
983
+ providerTurnId: mapper.turnId,
984
+ itemId: delta.itemId,
985
+ delta: delta.delta,
986
+ });
987
+ }
988
+ if (mapped.planUpdate) {
989
+ this.emitRuntimeEvent({
990
+ type: 'plan.updated',
991
+ provider: 'acp',
992
+ providerSessionId: state.providerSessionId,
993
+ providerTurnId: mapper.turnId,
994
+ explanation: mapped.planUpdate.explanation,
995
+ plan: mapped.planUpdate.plan,
996
+ });
997
+ }
998
+ if (mapped.usage) {
999
+ this.emitRuntimeEvent({
1000
+ type: 'usage.updated',
1001
+ provider: 'acp',
1002
+ providerSessionId: state.providerSessionId,
1003
+ providerTurnId: mapper.turnId,
1004
+ usage: {
1005
+ last: { totalTokens: mapped.usage.used },
1006
+ modelContextWindow: mapped.usage.size,
1007
+ ...(mapped.usage.cost ? { cost: mapped.usage.cost } : {}),
1008
+ },
1009
+ });
1010
+ }
1011
+ }
1012
+
1013
+ private completePrompt(
1014
+ state: AcpSessionState,
1015
+ mapper: AcpTurnItemMapper,
1016
+ response: acp.PromptResponse,
1017
+ ) {
1018
+ if (state.activeMapper !== mapper) {
1019
+ return;
1020
+ }
1021
+ if (response.usage) {
1022
+ this.emitRuntimeEvent({
1023
+ type: 'usage.updated',
1024
+ provider: 'acp',
1025
+ providerSessionId: state.providerSessionId,
1026
+ providerTurnId: mapper.turnId,
1027
+ usage: promptUsagePayload(response.usage),
1028
+ });
1029
+ }
1030
+ const status = response.stopReason === 'cancelled' ? 'interrupted' : 'completed';
1031
+ const completed = mapper.complete(status);
1032
+ for (const itemUpdate of completed.updates) {
1033
+ this.emitItemUpdate(state, mapper.turnId, itemUpdate);
1034
+ }
1035
+ const turn = {
1036
+ ...completed.turn,
1037
+ startedAt: state.turns.find((candidate) => candidate.providerTurnId === mapper.turnId)?.startedAt ?? null,
1038
+ rawTurn: response,
1039
+ };
1040
+ this.replaceTurn(state, turn);
1041
+ state.activeMapper = null;
1042
+ state.status = status === 'interrupted' ? 'interrupted' : 'idle';
1043
+ state.updatedAt = new Date().toISOString();
1044
+ this.emitRuntimeEvent({
1045
+ type: 'turn.completed',
1046
+ provider: 'acp',
1047
+ providerSessionId: state.providerSessionId,
1048
+ turn,
1049
+ });
1050
+ }
1051
+
1052
+ private failPrompt(state: AcpSessionState, mapper: AcpTurnItemMapper, error: unknown) {
1053
+ if (state.activeMapper !== mapper) {
1054
+ return;
1055
+ }
1056
+ const message = errorMessage(error);
1057
+ const completed = mapper.complete('failed', message);
1058
+ for (const itemUpdate of completed.updates) {
1059
+ this.emitItemUpdate(state, mapper.turnId, itemUpdate);
1060
+ }
1061
+ const turn = {
1062
+ ...completed.turn,
1063
+ startedAt: state.turns.find((candidate) => candidate.providerTurnId === mapper.turnId)?.startedAt ?? null,
1064
+ rawTurn: error,
1065
+ };
1066
+ this.replaceTurn(state, turn);
1067
+ state.activeMapper = null;
1068
+ state.status = 'failed';
1069
+ state.updatedAt = new Date().toISOString();
1070
+ this.emitRuntimeEvent({
1071
+ type: 'turn.completed',
1072
+ provider: 'acp',
1073
+ providerSessionId: state.providerSessionId,
1074
+ turn,
1075
+ });
1076
+ }
1077
+
1078
+ private replaceTurn(state: AcpSessionState, turn: AgentTurn) {
1079
+ const index = state.turns.findIndex((candidate) => candidate.providerTurnId === turn.providerTurnId);
1080
+ if (index >= 0) {
1081
+ state.turns[index] = turn;
1082
+ } else {
1083
+ state.turns.push(turn);
1084
+ }
1085
+ }
1086
+
1087
+ private emitItemUpdate(
1088
+ state: AcpSessionState,
1089
+ providerTurnId: string,
1090
+ update: AcpMappedItemUpdate,
1091
+ ) {
1092
+ this.emitRuntimeEvent({
1093
+ type: update.completed ? 'item.completed' : 'item.started',
1094
+ provider: 'acp',
1095
+ providerSessionId: state.providerSessionId,
1096
+ providerTurnId,
1097
+ item: update.item,
1098
+ });
1099
+ }
1100
+
1101
+ private requestPermission(params: acp.RequestPermissionRequest) {
1102
+ const id = ++this.permissionSequence;
1103
+ return new Promise<acp.RequestPermissionResponse>((resolve) => {
1104
+ const timer = setTimeout(() => {
1105
+ this.pendingPermissions.delete(id);
1106
+ resolve(cancelledPermission());
1107
+ }, 5 * 60 * 1000);
1108
+ timer.unref();
1109
+ this.pendingPermissions.set(id, { params, resolve, timer });
1110
+ this.emit('provider-request', {
1111
+ provider: 'acp',
1112
+ id,
1113
+ method: acp.methods.client.session.requestPermission,
1114
+ params,
1115
+ rawRequest: params,
1116
+ } satisfies AgentProviderRequest);
1117
+ });
1118
+ }
1119
+
1120
+ private async readTextFile(params: acp.ReadTextFileRequest): Promise<acp.ReadTextFileResponse> {
1121
+ if (!path.isAbsolute(params.path)) {
1122
+ throw new Error('ACP file paths must be absolute.');
1123
+ }
1124
+ const content = await fs.readFile(params.path, 'utf8');
1125
+ if (params.line === undefined && params.limit === undefined) {
1126
+ return { content };
1127
+ }
1128
+ const lines = content.replace(/\r\n/g, '\n').split('\n');
1129
+ const start = Math.max(0, (params.line ?? 1) - 1);
1130
+ const end = params.limit == null ? undefined : start + Math.max(0, params.limit);
1131
+ return { content: lines.slice(start, end).join('\n') };
1132
+ }
1133
+
1134
+ private async writeTextFile(params: acp.WriteTextFileRequest) {
1135
+ if (!path.isAbsolute(params.path)) {
1136
+ throw new Error('ACP file paths must be absolute.');
1137
+ }
1138
+ await fs.mkdir(path.dirname(params.path), { recursive: true });
1139
+ await fs.writeFile(params.path, params.content, 'utf8');
1140
+ return {};
1141
+ }
1142
+
1143
+ private async requireContext() {
1144
+ await this.start();
1145
+ if (!this.context || this.status.state !== 'ready') {
1146
+ throw new AgentRuntimeError('ACP agent is not connected.', 'acp', 'client_closed');
1147
+ }
1148
+ return this.context;
1149
+ }
1150
+
1151
+ private applyAgentCapabilities(capabilities: acp.AgentCapabilities | null | undefined) {
1152
+ this.capabilities.sessions.list = Boolean(capabilities?.sessionCapabilities?.list);
1153
+ // Imported ACP sessions have no supervisor-owned item history yet. Keep
1154
+ // import disabled until replayed updates can be assigned stable turn ids.
1155
+ this.capabilities.sessions.importLocal = false;
1156
+ this.capabilities.branching.fork = false;
1157
+ }
1158
+
1159
+ private withStartupTimeout<T>(promise: Promise<T>) {
1160
+ const timeoutMs = this.options.startupTimeoutMs ?? 10_000;
1161
+ return new Promise<T>((resolve, reject) => {
1162
+ const timer = setTimeout(
1163
+ () => reject(new Error(`ACP initialize timed out after ${timeoutMs}ms.`)),
1164
+ timeoutMs,
1165
+ );
1166
+ promise.then(
1167
+ (value) => {
1168
+ clearTimeout(timer);
1169
+ resolve(value);
1170
+ },
1171
+ (error) => {
1172
+ clearTimeout(timer);
1173
+ reject(error);
1174
+ },
1175
+ );
1176
+ });
1177
+ }
1178
+
1179
+ private markFailed(error: unknown) {
1180
+ const message = errorMessage(error);
1181
+ this.status = {
1182
+ ...this.status,
1183
+ state: 'failed',
1184
+ lastError: message,
1185
+ };
1186
+ this.installation.lastError = message;
1187
+ this.emit('status', this.getStatus());
1188
+ }
1189
+
1190
+ private emitRuntimeEvent(event: AgentRuntimeEvent) {
1191
+ this.emit('event', event);
1192
+ }
1193
+ }