awb-agent-manager 1.6.205 → 1.6.207

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.
@@ -1,11 +1,11 @@
1
1
  import { AgentSessionStore } from './agent-session-store.js';
2
- import { type AwbConfig } from './rest.js';
2
+ import { type AgentSessionConfigOptionPatch, type AwbConfig } from './rest.js';
3
3
  import type { AcpMcpServer } from './runtime/acp/acp-types.js';
4
4
  export interface AgentSessionRequest {
5
5
  manager_id: string;
6
6
  workspace_id?: string;
7
7
  cli: string;
8
- op: 'list' | 'history' | 'open' | 'prompt' | 'permission' | 'cancel' | 'set_mode' | 'close';
8
+ op: 'list' | 'history' | 'open' | 'prompt' | 'permission' | 'elicitation' | 'cancel' | 'set_mode' | 'set_config_option' | 'close';
9
9
  request_id?: string;
10
10
  session_id?: string | null;
11
11
  cwd?: string;
@@ -15,6 +15,11 @@ export interface AgentSessionRequest {
15
15
  permission_request_id?: string;
16
16
  option_id?: string | null;
17
17
  mode_id?: string;
18
+ config_id?: string;
19
+ config_value?: string | boolean;
20
+ elicitation_id?: string;
21
+ elicitation_action?: 'accept' | 'decline' | 'cancel';
22
+ elicitation_content?: Record<string, unknown> | null;
18
23
  credential_id?: string | null;
19
24
  driver_user_id: string;
20
25
  issued_at: string;
@@ -45,7 +50,19 @@ export interface SessionCredential {
45
50
  }
46
51
  export declare const SESSION_CLI_CREDENTIAL_PREFIX: Record<string, string>;
47
52
  export declare function redactSecrets(text: string): string;
53
+ type CommandPatch = {
54
+ name: string;
55
+ description: string;
56
+ input_hint?: string;
57
+ };
48
58
  export declare const ACP_SESSION_CLIS: readonly ["claude", "codex", "hermes"];
59
+ export declare function parseConfigOptions(raw: unknown): AgentSessionConfigOptionPatch[];
60
+ export declare function parseCommands(raw: unknown): CommandPatch[];
61
+ export declare function parsePlanEntries(raw: unknown): Array<{
62
+ content: string;
63
+ priority: string;
64
+ status: string;
65
+ }> | null;
49
66
  export declare function findOnPath(name: string): Promise<string | null>;
50
67
  export declare function resolveAcpCommandForCli(cli: string): Promise<ResolvedAcpCommand>;
51
68
  export declare function detectAcpSessionClis(env?: NodeJS.ProcessEnv): Promise<string[]>;
@@ -63,3 +80,4 @@ export declare class AgentSessionRunner {
63
80
  handle(request: AgentSessionRequest): Promise<void>;
64
81
  stopAll(reason?: string): Promise<void>;
65
82
  }
83
+ export {};
@@ -68,6 +68,89 @@ function boundedPayload(payload) {
68
68
  return bounded;
69
69
  return { truncated: true, preview: serialized.slice(0, 4_000) };
70
70
  }
71
+ export function parseConfigOptions(raw) {
72
+ if (!Array.isArray(raw))
73
+ return [];
74
+ const out = [];
75
+ for (const entry of raw) {
76
+ if (!entry || typeof entry !== 'object')
77
+ continue;
78
+ const o = entry;
79
+ const configId = typeof o.configId === 'string' ? o.configId : typeof o.config_id === 'string' ? o.config_id : '';
80
+ if (!configId)
81
+ continue;
82
+ const type = typeof o.type === 'string' ? o.type : (typeof o.currentValue === 'boolean' ? 'boolean' : 'select');
83
+ const options = [];
84
+ const pushOption = (v, group) => {
85
+ if (!v || typeof v !== 'object')
86
+ return;
87
+ const opt = v;
88
+ const value = typeof opt.value === 'string' ? opt.value : typeof opt.id === 'string' ? opt.id : '';
89
+ if (!value)
90
+ return;
91
+ options.push({
92
+ value,
93
+ name: typeof opt.name === 'string' && opt.name ? opt.name : value,
94
+ ...(typeof opt.description === 'string' && opt.description ? { description: opt.description } : {}),
95
+ ...(group ? { group } : {}),
96
+ });
97
+ };
98
+ if (Array.isArray(o.options)) {
99
+ for (const v of o.options) {
100
+ const g = v;
101
+ if (g && typeof g === 'object' && Array.isArray(g.options)) {
102
+ const label = typeof g.name === 'string' ? g.name : typeof g.group === 'string' ? g.group : '';
103
+ for (const inner of g.options)
104
+ pushOption(inner, label || undefined);
105
+ }
106
+ else {
107
+ pushOption(v);
108
+ }
109
+ }
110
+ }
111
+ const current = o.currentValue ?? o.current_value;
112
+ out.push({
113
+ config_id: configId,
114
+ name: typeof o.name === 'string' && o.name ? o.name : configId,
115
+ ...(typeof o.description === 'string' && o.description ? { description: o.description } : {}),
116
+ category: typeof o.category === 'string' && o.category ? o.category : 'unknown',
117
+ type,
118
+ current_value: typeof current === 'boolean' ? current : typeof current === 'string' ? current : null,
119
+ options,
120
+ });
121
+ }
122
+ return out;
123
+ }
124
+ export function parseCommands(raw) {
125
+ if (!Array.isArray(raw))
126
+ return [];
127
+ const out = [];
128
+ for (const entry of raw) {
129
+ if (!entry || typeof entry !== 'object')
130
+ continue;
131
+ const c = entry;
132
+ const name = typeof c.name === 'string' ? c.name.trim().replace(/^\//, '') : '';
133
+ if (!name)
134
+ continue;
135
+ const input = c.input && typeof c.input === 'object' ? c.input : null;
136
+ const hint = input && typeof input.hint === 'string' ? input.hint : '';
137
+ out.push({ name, description: typeof c.description === 'string' ? c.description : '', ...(hint ? { input_hint: hint } : {}) });
138
+ }
139
+ return out;
140
+ }
141
+ export function parsePlanEntries(raw) {
142
+ if (!Array.isArray(raw))
143
+ return null;
144
+ const entries = raw
145
+ .filter((e) => !!e && typeof e === 'object')
146
+ .map((e) => ({
147
+ content: typeof e.content === 'string' ? e.content.slice(0, 2_000) : '',
148
+ priority: typeof e.priority === 'string' ? e.priority : 'medium',
149
+ status: typeof e.status === 'string' ? e.status : 'pending',
150
+ }))
151
+ .filter((e) => e.content);
152
+ return entries;
153
+ }
71
154
  export async function findOnPath(name) {
72
155
  const candidates = process.platform === 'win32' ? [`${name}.cmd`, `${name}.exe`, name] : [name];
73
156
  for (const dir of (process.env.PATH || '').split(delimiter)) {
@@ -100,7 +183,7 @@ export async function resolveAcpCommandForCli(cli) {
100
183
  }
101
184
  case 'codex': {
102
185
  const found = await findOnPath('codex-acp');
103
- return found ? { command: found, args: [] } : { command: 'npx', args: ['--yes', '@zed-industries/codex-acp'] };
186
+ return found ? { command: found, args: [] } : { command: 'npx', args: ['--yes', '@agentclientprotocol/codex-acp'] };
104
187
  }
105
188
  case 'hermes': {
106
189
  const resolved = await resolveHermesAcpCommand();
@@ -126,6 +209,7 @@ export class AgentSessionRunner {
126
209
  #store;
127
210
  #live = new Map();
128
211
  #opening = new Map();
212
+ #exited = [];
129
213
  constructor(config, options) {
130
214
  this.#config = config;
131
215
  this.#store = options.store ?? new AgentSessionStore();
@@ -151,7 +235,7 @@ export class AgentSessionRunner {
151
235
  return Array.from(this.#live.values()).map((live) => ({
152
236
  cli: live.cli,
153
237
  session_id: live.sessionId,
154
- busy: live.turn !== null || live.pendingPermissions.size > 0,
238
+ busy: live.turn !== null || live.pendingPermissions.size > 0 || live.pendingElicitations.size > 0,
155
239
  pid: live.client.process.pid ?? null,
156
240
  }));
157
241
  }
@@ -185,6 +269,16 @@ export class AgentSessionRunner {
185
269
  case 'permission':
186
270
  this.#resolvePermission(cli, sessionId, request.permission_request_id || '', request.option_id ?? null);
187
271
  return;
272
+ case 'elicitation':
273
+ this.#resolveElicitation(cli, sessionId, request.elicitation_id || '', request.elicitation_action || 'cancel', request.elicitation_content ?? null);
274
+ return;
275
+ case 'set_config_option': {
276
+ const live = this.#live.get(this.#key(cli, sessionId));
277
+ if (!live || !request.config_id || request.config_value === undefined)
278
+ return;
279
+ await this.#setConfigOption(live, request.config_id, request.config_value);
280
+ return;
281
+ }
188
282
  case 'cancel': {
189
283
  const live = this.#live.get(this.#key(cli, sessionId));
190
284
  if (live)
@@ -197,6 +291,7 @@ export class AgentSessionRunner {
197
291
  if (!live || !modeId)
198
292
  return;
199
293
  await live.client.request('session/set_mode', { sessionId: live.sessionId, modeId }, { timeoutMs: this.#options.requestTimeoutMs });
294
+ live.currentMode = modeId;
200
295
  this.#enqueue(live, [{ type: 'system', payload: { text: `Mode set to ${modeId}.` } }], { current_mode: modeId, reason: 'mode' });
201
296
  return;
202
297
  }
@@ -245,11 +340,17 @@ export class AgentSessionRunner {
245
340
  await postAgentSessionRpcResponse(this.#config, managerId, requestId, { ok: false, error: 'Session not found on this Runtime Host.', code: 'not_found' });
246
341
  return;
247
342
  }
343
+ const pending = live
344
+ ? [...Array.from(live.pendingPermissions.values()), ...Array.from(live.pendingElicitations.values())].map((p) => p.event)
345
+ : [];
346
+ const events = pending.length
347
+ ? [...history.events, ...pending.map((e, i) => ({ ...e, seq: history.events.length + i + 1 }))]
348
+ : history.events;
248
349
  await postAgentSessionRpcResponse(this.#config, managerId, requestId, {
249
350
  ok: true,
250
351
  result: {
251
352
  session: history.session ?? (live ? this.#summaryOf(live) : null),
252
- events: history.events,
353
+ events,
253
354
  truncated: history.truncated,
254
355
  live: live ? this.#stateOf(live) : null,
255
356
  },
@@ -274,6 +375,10 @@ export class AgentSessionRunner {
274
375
  async stopAll(reason = 'manager_shutdown') {
275
376
  const keys = Array.from(this.#live.values()).map((l) => [l.cli, l.sessionId]);
276
377
  await Promise.all(keys.map(([cli, id]) => this.#closeLive(cli, id, 'idle', reason).catch(() => undefined)));
378
+ const drains = this.#exited.splice(0).map((l) => l.postChain);
379
+ if (drains.length) {
380
+ await Promise.race([Promise.all(drains), new Promise((resolve) => setTimeout(resolve, 3_000).unref?.())]);
381
+ }
277
382
  }
278
383
  async #ensureLive(cli, sessionId, cwd, title, request) {
279
384
  if (sessionId) {
@@ -316,25 +421,34 @@ export class AgentSessionRunner {
316
421
  const auth = await this.#prepareAuth(cli, cwd, request);
317
422
  log(`${tag} spawning ACP adapter cmd=${command} ${args.join(' ')} cwd=${cwd} auth=${auth.label}`);
318
423
  let live = null;
424
+ const env = this.#buildEnv(cli, requestedSessionId || 'new', auth);
319
425
  const client = await AcpClient.spawn({
320
426
  command,
321
427
  args,
322
428
  cwd,
323
- env: this.#buildEnv(cli, requestedSessionId || 'new', auth),
429
+ env,
324
430
  requestTimeoutMs: this.#options.requestTimeoutMs,
325
431
  onEvent: (event) => { if (live)
326
432
  this.#onEvent(live, event); },
327
433
  onPermissionRequest: (permission) => (live ? this.#onPermission(live, permission) : Promise.resolve({ outcome: 'cancelled' })),
434
+ onElicitation: (elicitation) => (live ? this.#onElicitation(live, elicitation) : Promise.resolve({ action: 'cancel' })),
328
435
  onStderr: (line) => log(`${tag} stderr: ${redactSecrets(line)}`),
329
436
  spawnOptions: { detached: process.platform !== 'win32' },
330
437
  });
331
438
  try {
332
439
  const initialized = await client.initialize({
333
440
  clientInfo: { name: 'awb-agent-session', version: this.#options.clientVersion || '1' },
334
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
441
+ clientCapabilities: {
442
+ fs: { readTextFile: false, writeTextFile: false },
443
+ terminal: false,
444
+ elicitation: { form: {}, url: {} },
445
+ session: { configOptions: { boolean: {} } },
446
+ plan: {},
447
+ },
335
448
  });
336
449
  const caps = (initialized?.agentCapabilities ?? {});
337
450
  const loadSupported = caps.loadSession === true;
451
+ const authMethods = Array.isArray(initialized?.authMethods) ? initialized.authMethods : [];
338
452
  const sessionIdForMcp = requestedSessionId || 'new';
339
453
  const mcpServers = this.#options.mcpServers ? this.#options.mcpServers(sessionIdForMcp) : this.#defaultMcpServers(sessionIdForMcp);
340
454
  live = {
@@ -346,6 +460,11 @@ export class AgentSessionRunner {
346
460
  loadSupported,
347
461
  loading: false,
348
462
  pendingPermissions: new Map(),
463
+ pendingElicitations: new Map(),
464
+ configOptions: [],
465
+ availableCommands: [],
466
+ currentMode: null,
467
+ availableModes: [],
349
468
  turn: null,
350
469
  textBuffer: '',
351
470
  reasoningBuffer: '',
@@ -359,6 +478,7 @@ export class AgentSessionRunner {
359
478
  exited: false,
360
479
  };
361
480
  let modes;
481
+ let configOptions;
362
482
  let resumed = false;
363
483
  if (requestedSessionId) {
364
484
  if (!loadSupported) {
@@ -366,8 +486,9 @@ export class AgentSessionRunner {
366
486
  }
367
487
  live.loading = true;
368
488
  try {
369
- const loaded = await client.loadSession({ sessionId: requestedSessionId, cwd, mcpServers });
489
+ const loaded = await this.#withAuthRetry(client, cli, authMethods, env, () => client.loadSession({ sessionId: requestedSessionId, cwd, mcpServers }));
370
490
  modes = loaded?.modes;
491
+ configOptions = loaded?.configOptions;
371
492
  resumed = true;
372
493
  }
373
494
  finally {
@@ -375,9 +496,10 @@ export class AgentSessionRunner {
375
496
  }
376
497
  }
377
498
  else {
378
- const created = await client.newSession({ cwd, mcpServers });
499
+ const created = await this.#withAuthRetry(client, cli, authMethods, env, () => client.newSession({ cwd, mcpServers }));
379
500
  live.sessionId = created.sessionId;
380
501
  modes = created.modes;
502
+ configOptions = created.configOptions;
381
503
  await this.#store.recordAwbSession({ cli, session_id: live.sessionId, cwd, title }).catch(() => undefined);
382
504
  }
383
505
  if (!live.sessionId)
@@ -386,6 +508,9 @@ export class AgentSessionRunner {
386
508
  this.#live.set(key, live);
387
509
  client.process.once('exit', (code, signal) => this.#onProcessExit(key, code, signal));
388
510
  const modeInfo = this.#parseModes(modes);
511
+ live.currentMode = modeInfo.current;
512
+ live.availableModes = modeInfo.available;
513
+ live.configOptions = parseConfigOptions(configOptions);
389
514
  this.#enqueue(live, [{
390
515
  type: 'system',
391
516
  payload: {
@@ -398,6 +523,8 @@ export class AgentSessionRunner {
398
523
  ...(title ? { title } : {}),
399
524
  current_mode: modeInfo.current,
400
525
  available_modes: modeInfo.available,
526
+ config_options: live.configOptions,
527
+ available_commands: live.availableCommands,
401
528
  resume_supported: loadSupported,
402
529
  last_error: null,
403
530
  reason: resumed ? 'resumed' : 'opened',
@@ -422,8 +549,52 @@ export class AgentSessionRunner {
422
549
  env.AWB_MANAGER_ID = this.#options.getManagerId();
423
550
  env.AWB_SESSION_CLI = cli;
424
551
  env.AWB_SESSION_ID = sessionId;
552
+ if (cli === 'codex' && env.NO_BROWSER === undefined)
553
+ env.NO_BROWSER = '1';
425
554
  return env;
426
555
  }
556
+ async #withAuthRetry(client, cli, authMethods, env, run) {
557
+ try {
558
+ return await run();
559
+ }
560
+ catch (err) {
561
+ const rpcCode = err?.rpcCode ?? err?.code;
562
+ const message = String(err?.message ?? '');
563
+ const authRequired = rpcCode === -32000 || /auth(entication)? required|not (logged|signed) in|unauthenticated/i.test(message);
564
+ if (!authRequired)
565
+ throw err;
566
+ const methods = authMethods
567
+ .filter((m) => !!m && typeof m === 'object')
568
+ .map((m) => ({ id: String(m.id ?? ''), name: String(m.name ?? m.id ?? ''), type: String(m.type ?? '') }))
569
+ .filter((m) => m.id);
570
+ const hasApiKey = !!(env.CODEX_API_KEY || env.OPENAI_API_KEY || env.ANTHROPIC_API_KEY || env.CLAUDE_CODE_OAUTH_TOKEN);
571
+ const apiKeyMethod = methods.find((m) => /api[-_]?key|token/i.test(m.id) || /api[-_]?key|token/i.test(m.name));
572
+ if (hasApiKey && apiKeyMethod) {
573
+ log(`[agent-session ${cli}] authentication required — trying ACP auth method ${apiKeyMethod.id}`);
574
+ await client.authenticate(apiKeyMethod.id, { timeoutMs: this.#options.requestTimeoutMs });
575
+ return await run();
576
+ }
577
+ const listed = methods.length ? ` Available methods: ${methods.map((m) => m.name || m.id).join(', ')}.` : '';
578
+ throw Object.assign(new Error(`Authentication required for ${cli} on this Runtime Host — run \`${cli} login\` there, or bind a credential in CLI settings.${listed}`), { code: 'auth_required' });
579
+ }
580
+ }
581
+ async #setConfigOption(live, configId, value) {
582
+ const before = live.configOptions.find((o) => o.config_id === configId);
583
+ const response = await live.client.setConfigOption(typeof value === 'boolean'
584
+ ? { sessionId: live.sessionId, configId, type: 'boolean', value }
585
+ : { sessionId: live.sessionId, configId, type: 'id', value }, { timeoutMs: this.#options.requestTimeoutMs });
586
+ if (Array.isArray(response?.configOptions))
587
+ live.configOptions = parseConfigOptions(response.configOptions);
588
+ else if (before) {
589
+ before.current_value = value;
590
+ }
591
+ const after = live.configOptions.find((o) => o.config_id === configId);
592
+ const label = after?.name || before?.name || configId;
593
+ const chosen = typeof value === 'boolean'
594
+ ? (value ? 'on' : 'off')
595
+ : (after?.options.find((o) => o.value === value)?.name || String(value));
596
+ this.#enqueue(live, [{ type: 'system', payload: { text: `${label} set to ${chosen}.` } }], { config_options: live.configOptions, reason: 'config_option' });
597
+ }
427
598
  async #prepareAuth(cli, cwd, request) {
428
599
  const none = { label: 'operator-login', env: {}, stripEnvKeys: [], cliHome: null };
429
600
  const credentialId = request.credential_id || '';
@@ -511,6 +682,8 @@ export class AgentSessionRunner {
511
682
  return 'idle';
512
683
  if (live.pendingPermissions.size > 0)
513
684
  return 'awaiting_permission';
685
+ if (live.pendingElicitations.size > 0)
686
+ return 'awaiting_input';
514
687
  if (live.turn)
515
688
  return 'busy';
516
689
  return 'ready';
@@ -526,6 +699,10 @@ export class AgentSessionRunner {
526
699
  title: live.title,
527
700
  status: this.#statusOf(live),
528
701
  resume_supported: live.loadSupported,
702
+ current_mode: live.currentMode,
703
+ available_modes: live.availableModes,
704
+ config_options: live.configOptions,
705
+ available_commands: live.availableCommands,
529
706
  };
530
707
  }
531
708
  async #runPrompt(live, turnId, text) {
@@ -622,10 +799,53 @@ export class AgentSessionRunner {
622
799
  case 'diagnostic': {
623
800
  const data = (event.data ?? {});
624
801
  const kind = String(data.sessionUpdate ?? data.session_update ?? '');
625
- if (event.method === 'session/update' && kind === 'current_mode_update') {
626
- const modeId = String(data.currentModeId ?? data.current_mode_id ?? '');
627
- if (modeId)
628
- this.#enqueue(live, [], { current_mode: modeId, reason: 'mode' });
802
+ if (event.method === 'session/update') {
803
+ switch (kind) {
804
+ case 'current_mode_update': {
805
+ const modeId = String(data.currentModeId ?? data.current_mode_id ?? '');
806
+ if (modeId) {
807
+ live.currentMode = modeId;
808
+ this.#enqueue(live, [], { current_mode: modeId, reason: 'mode' });
809
+ }
810
+ return;
811
+ }
812
+ case 'config_option_update':
813
+ live.configOptions = parseConfigOptions(data.configOptions ?? data.config_options);
814
+ this.#enqueue(live, [], { config_options: live.configOptions, reason: 'config_option' });
815
+ return;
816
+ case 'available_commands_update':
817
+ live.availableCommands = parseCommands(data.availableCommands ?? data.available_commands);
818
+ this.#enqueue(live, [], { available_commands: live.availableCommands, reason: 'commands' });
819
+ return;
820
+ case 'plan':
821
+ case 'plan_update': {
822
+ const entries = parsePlanEntries(kind === 'plan' ? data.entries : data.plan?.entries ?? data.entries);
823
+ if (entries) {
824
+ this.#flushBuffers(live, turnId);
825
+ this.#enqueue(live, [{ type: 'plan', payload: { entries }, turn_id: turnId }]);
826
+ }
827
+ return;
828
+ }
829
+ case 'plan_removed':
830
+ return;
831
+ case 'session_info_update': {
832
+ const title = typeof data.title === 'string' ? data.title.trim().slice(0, 200) : '';
833
+ if (title && title !== live.title) {
834
+ live.title = title;
835
+ this.#enqueue(live, [], { title, reason: 'title' });
836
+ void this.#store.touchAwbSession(live.cli, live.sessionId, { title }).catch(() => undefined);
837
+ }
838
+ return;
839
+ }
840
+ default:
841
+ break;
842
+ }
843
+ }
844
+ if (event.method === 'elicitation/complete') {
845
+ const elicitationId = String(data.elicitationId ?? data.elicitation_id ?? '');
846
+ if (elicitationId) {
847
+ this.#enqueue(live, [{ type: 'elicitation_decision', payload: { elicitation_id: elicitationId, action: 'accept', decided_by: 'agent' }, turn_id: turnId }]);
848
+ }
629
849
  return;
630
850
  }
631
851
  log(`[agent-session ${live.cli} ${live.sessionId.slice(0, 8)}] diagnostic ${event.method}${kind ? ` (${kind})` : ''}`);
@@ -642,12 +862,16 @@ export class AgentSessionRunner {
642
862
  this.#flushBuffers(live, turnId);
643
863
  const requestId = randomUUID();
644
864
  const options = (permission.options ?? []).map((o) => ({ option_id: o.optionId, name: o.name, kind: o.kind }));
645
- this.#enqueue(live, [{
865
+ const meta = (permission._meta?.permission ?? null);
866
+ const title = permission.title || permission.toolCall?.title || (typeof meta?.title === 'string' ? meta.title : '');
867
+ const description = permission.description || (typeof meta?.description === 'string' ? meta.description : '');
868
+ const [requestEvent] = this.#enqueue(live, [{
646
869
  type: 'permission_request',
647
870
  payload: {
648
871
  request_id: requestId,
649
- tool_call_id: permission.toolCall?.toolCallId ?? '',
650
- title: permission.toolCall?.title ?? '',
872
+ tool_call_id: permission.toolCall?.toolCallId ?? permission.subject?.toolCallId ?? '',
873
+ title,
874
+ ...(description ? { description } : {}),
651
875
  kind: permission.toolCall?.kind ?? '',
652
876
  options,
653
877
  raw_input: boundedValue(permission.toolCall?.rawInput ?? permission.toolCall?.raw_input),
@@ -665,9 +889,88 @@ export class AgentSessionRunner {
665
889
  resolve({ outcome: 'cancelled' });
666
890
  }, this.#options.permissionTimeoutMs);
667
891
  timer.unref?.();
668
- live.pendingPermissions.set(requestId, { resolve, timer });
892
+ live.pendingPermissions.set(requestId, { resolve, timer, event: requestEvent });
669
893
  });
670
894
  }
895
+ #cancelPendingPermissions(live) {
896
+ const events = [];
897
+ for (const [requestId, pending] of live.pendingPermissions) {
898
+ clearTimeout(pending.timer);
899
+ live.pendingPermissions.delete(requestId);
900
+ events.push({
901
+ type: 'permission_decision',
902
+ payload: { request_id: requestId, outcome: 'cancelled', option_id: null, decided_by: 'system' },
903
+ turn_id: live.turn?.turnId,
904
+ });
905
+ pending.resolve({ outcome: 'cancelled' });
906
+ }
907
+ for (const [elicitationId, pending] of live.pendingElicitations) {
908
+ clearTimeout(pending.timer);
909
+ live.pendingElicitations.delete(elicitationId);
910
+ events.push({
911
+ type: 'elicitation_decision',
912
+ payload: { elicitation_id: elicitationId, action: 'cancel', decided_by: 'system' },
913
+ turn_id: live.turn?.turnId,
914
+ });
915
+ pending.resolve({ action: 'cancel' });
916
+ }
917
+ if (events.length)
918
+ this.#enqueue(live, events);
919
+ }
920
+ async #onElicitation(live, request) {
921
+ if (live.loading)
922
+ return { action: 'cancel' };
923
+ const turnId = live.turn?.turnId;
924
+ this.#flushBuffers(live, turnId);
925
+ const mode = request.mode === 'url' ? 'url' : 'form';
926
+ const elicitationId = (mode === 'url' && typeof request.elicitationId === 'string' && request.elicitationId) ? request.elicitationId : randomUUID();
927
+ const payload = {
928
+ elicitation_id: elicitationId,
929
+ mode,
930
+ message: typeof request.message === 'string' ? request.message.slice(0, 8_000) : '',
931
+ tool_call_id: typeof request.toolCallId === 'string' ? request.toolCallId : '',
932
+ };
933
+ if (mode === 'form')
934
+ payload.schema = boundedValue(request.requestedSchema ?? {});
935
+ if (mode === 'url')
936
+ payload.url = typeof request.url === 'string' ? request.url.slice(0, 2_048) : '';
937
+ if (mode === 'url') {
938
+ this.#enqueue(live, [{ type: 'elicitation_request', payload, turn_id: turnId }]);
939
+ return { action: 'accept' };
940
+ }
941
+ const [requestEvent] = this.#enqueue(live, [{ type: 'elicitation_request', payload, turn_id: turnId }], { status: 'awaiting_input', reason: 'elicitation' });
942
+ return new Promise((resolve) => {
943
+ const timer = setTimeout(() => {
944
+ live.pendingElicitations.delete(elicitationId);
945
+ this.#enqueue(live, [{
946
+ type: 'elicitation_decision',
947
+ payload: { elicitation_id: elicitationId, action: 'cancel', decided_by: 'timeout' },
948
+ turn_id: live.turn?.turnId,
949
+ }], { status: this.#statusOf(live) === 'awaiting_input' ? 'busy' : this.#statusOf(live), reason: 'elicitation_timeout' });
950
+ resolve({ action: 'cancel' });
951
+ }, this.#options.permissionTimeoutMs);
952
+ timer.unref?.();
953
+ live.pendingElicitations.set(elicitationId, { resolve, timer, event: requestEvent });
954
+ });
955
+ }
956
+ #resolveElicitation(cli, sessionId, elicitationId, action, content) {
957
+ const live = this.#live.get(this.#key(cli, sessionId));
958
+ if (!live)
959
+ return;
960
+ const pending = live.pendingElicitations.get(elicitationId);
961
+ if (!pending) {
962
+ log(`[agent-session ${cli} ${sessionId.slice(0, 8)}] elicitation ${elicitationId.slice(0, 8)} not pending (late or duplicate answer)`);
963
+ return;
964
+ }
965
+ clearTimeout(pending.timer);
966
+ live.pendingElicitations.delete(elicitationId);
967
+ this.#enqueue(live, [{
968
+ type: 'elicitation_decision',
969
+ payload: { elicitation_id: elicitationId, action, ...(action === 'accept' ? { content: boundedValue(content ?? {}) } : {}), decided_by: 'user' },
970
+ turn_id: live.turn?.turnId,
971
+ }], { status: live.pendingPermissions.size > 0 ? 'awaiting_permission' : 'busy', reason: `elicitation_${action}` });
972
+ pending.resolve(action === 'accept' ? { action: 'accept', content: content ?? {} } : { action });
973
+ }
671
974
  #resolvePermission(cli, sessionId, requestId, optionId) {
672
975
  const live = this.#live.get(this.#key(cli, sessionId));
673
976
  if (!live)
@@ -724,6 +1027,7 @@ export class AgentSessionRunner {
724
1027
  ? postAgentSessionEvents(this.#config, ref, stamped, state ?? null)
725
1028
  : Promise.resolve({ ok: true, status: 204 })))
726
1029
  .then(() => undefined, () => undefined);
1030
+ return stamped;
727
1031
  }
728
1032
  #touch(live) {
729
1033
  this.#clearIdle(live);
@@ -733,7 +1037,7 @@ export class AgentSessionRunner {
733
1037
  if (ms <= 0)
734
1038
  return;
735
1039
  live.idleTimer = setTimeout(() => {
736
- if (live.turn || live.pendingPermissions.size > 0) {
1040
+ if (live.turn || live.pendingPermissions.size > 0 || live.pendingElicitations.size > 0) {
737
1041
  this.#touch(live);
738
1042
  return;
739
1043
  }
@@ -752,19 +1056,20 @@ export class AgentSessionRunner {
752
1056
  if (!live)
753
1057
  return;
754
1058
  live.exited = true;
755
- for (const [id, pending] of live.pendingPermissions) {
756
- clearTimeout(pending.timer);
757
- pending.resolve({ outcome: 'cancelled' });
758
- live.pendingPermissions.delete(id);
759
- }
760
1059
  this.#clearIdle(live);
761
- if (live.closing)
1060
+ if (live.closing) {
1061
+ this.#cancelPendingPermissions(live);
762
1062
  return;
1063
+ }
763
1064
  this.#live.delete(key);
764
1065
  const detail = `Agent process exited (${signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`}).`;
765
1066
  log(`[agent-session ${live.cli} ${live.sessionId.slice(0, 8)}] ${detail}`);
766
1067
  this.#flushBuffers(live, live.turn?.turnId);
767
- this.#enqueue(live, [{ type: 'system', payload: { text: `${detail} The next prompt reopens the session.` } }], live.turn ? undefined : { status: 'idle', reason: 'process_exit' });
1068
+ this.#cancelPendingPermissions(live);
1069
+ this.#enqueue(live, [{ type: 'system', payload: { text: `${detail} The next prompt reopens the session.` } }], { status: 'idle', reason: 'process_exit' });
1070
+ this.#exited.push(live);
1071
+ if (this.#exited.length > 50)
1072
+ this.#exited.splice(0, this.#exited.length - 50);
768
1073
  }
769
1074
  async #closeLive(cli, sessionId, finalStatus, reason = finalStatus) {
770
1075
  const key = this.#key(cli, sessionId);
@@ -773,11 +1078,7 @@ export class AgentSessionRunner {
773
1078
  return;
774
1079
  live.closing = true;
775
1080
  this.#clearIdle(live);
776
- for (const [id, pending] of live.pendingPermissions) {
777
- clearTimeout(pending.timer);
778
- pending.resolve({ outcome: 'cancelled' });
779
- live.pendingPermissions.delete(id);
780
- }
1081
+ this.#cancelPendingPermissions(live);
781
1082
  if (live.turn)
782
1083
  await live.client.cancel(live.sessionId).catch(() => undefined);
783
1084
  this.#flushBuffers(live, live.turn?.turnId);