awb-agent-manager 1.6.199 → 1.6.201
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.
- package/dist/lib/agent-session-runner.d.ts +34 -29
- package/dist/lib/agent-session-runner.js +385 -240
- package/dist/lib/agent-session-runner.js.map +1 -1
- package/dist/lib/agent-session-store.d.ts +61 -0
- package/dist/lib/agent-session-store.js +649 -0
- package/dist/lib/agent-session-store.js.map +1 -0
- package/dist/lib/event-dispatcher.js +5 -10
- package/dist/lib/event-dispatcher.js.map +1 -1
- package/dist/lib/instance-heartbeat.d.ts +1 -0
- package/dist/lib/instance-heartbeat.js +1 -0
- package/dist/lib/instance-heartbeat.js.map +1 -1
- package/dist/lib/rest.d.ts +25 -5
- package/dist/lib/rest.js +57 -21
- package/dist/lib/rest.js.map +1 -1
- package/dist/main.js +6 -1
- package/dist/main.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,37 @@
|
|
|
1
|
-
import { access, constants as fsConstants } from 'node:fs/promises';
|
|
1
|
+
import { access, constants as fsConstants, lstat, mkdir, symlink } from 'node:fs/promises';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { delimiter,
|
|
4
|
-
import {
|
|
3
|
+
import { delimiter, join } from 'node:path';
|
|
4
|
+
import { AgentSessionStore, resolveClaudeHome, resolveCodexHome } from './agent-session-store.js';
|
|
5
|
+
import { AGENT_MANAGER_HOME } from './constants.js';
|
|
6
|
+
import { normalizeCredentialFields } from './credential-fields.js';
|
|
5
7
|
import { log } from './logging.js';
|
|
6
8
|
import { terminateDetachedProcessTree } from './process-tree.js';
|
|
7
|
-
import {
|
|
9
|
+
import { fetchSessionCredential, postAgentSessionEvents, postAgentSessionRpcResponse, } from './rest.js';
|
|
10
|
+
import { createRuntimeCliAdapter } from './runtime/runtime-registry.js';
|
|
8
11
|
import { AcpClient } from './runtime/acp/acp-client.js';
|
|
9
12
|
import { resolveHermesAcpCommand } from './runtime/hermes/hermes-command.js';
|
|
13
|
+
export const SESSION_CLI_CREDENTIAL_PREFIX = {
|
|
14
|
+
claude: 'claude_',
|
|
15
|
+
codex: 'codex_',
|
|
16
|
+
};
|
|
17
|
+
const SESSION_REQUIRED_CREDENTIAL_FIELDS = {
|
|
18
|
+
claude_subscription: ['credentials_json'],
|
|
19
|
+
claude_api_key: ['api_key'],
|
|
20
|
+
claude_oauth_token: ['oauth_token'],
|
|
21
|
+
codex_subscription: ['auth_json'],
|
|
22
|
+
codex_api_key: ['api_key'],
|
|
23
|
+
};
|
|
24
|
+
export function redactSecrets(text) {
|
|
25
|
+
return String(text ?? '')
|
|
26
|
+
.replace(/Bearer\s+[^\s"'`]+/gi, 'Bearer <redacted>')
|
|
27
|
+
.replace(/sk-ant-[A-Za-z0-9_-]{8,}/g, 'sk-ant-<redacted>')
|
|
28
|
+
.replace(/sk-[A-Za-z0-9_-]{16,}/g, 'sk-<redacted>')
|
|
29
|
+
.replace(/(api[_-]?key|oauth[_-]?token|access[_-]?token|refresh[_-]?token)(["']?\s*[:=]\s*["']?)[^\s"',}]+/gi, '$1$2<redacted>');
|
|
30
|
+
}
|
|
31
|
+
const SESSION_STORE_SUBDIR = {
|
|
32
|
+
claude: 'projects',
|
|
33
|
+
codex: 'sessions',
|
|
34
|
+
};
|
|
10
35
|
const DEFAULT_IDLE_MINUTES = 30;
|
|
11
36
|
const DEFAULT_PERMISSION_TIMEOUT_MS = 15 * 60_000;
|
|
12
37
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
@@ -15,6 +40,7 @@ const DEFAULT_FLUSH_INTERVAL_MS = 150;
|
|
|
15
40
|
const MAX_TOOL_TEXT_CHARS = 16_000;
|
|
16
41
|
const MAX_PAYLOAD_CHARS = 200_000;
|
|
17
42
|
const ALLOW_KINDS = new Set(['allow_once', 'allow_always', 'allow_session']);
|
|
43
|
+
export const ACP_SESSION_CLIS = ['claude', 'codex', 'hermes'];
|
|
18
44
|
function truncateText(value, max) {
|
|
19
45
|
return value.length > max ? `${value.slice(0, max)}\n…[truncated ${value.length - max} chars]` : value;
|
|
20
46
|
}
|
|
@@ -42,7 +68,7 @@ function boundedPayload(payload) {
|
|
|
42
68
|
return bounded;
|
|
43
69
|
return { truncated: true, preview: serialized.slice(0, 4_000) };
|
|
44
70
|
}
|
|
45
|
-
async function findOnPath(name) {
|
|
71
|
+
export async function findOnPath(name) {
|
|
46
72
|
const candidates = process.platform === 'win32' ? [`${name}.cmd`, `${name}.exe`, name] : [name];
|
|
47
73
|
for (const dir of (process.env.PATH || '').split(delimiter)) {
|
|
48
74
|
if (!dir)
|
|
@@ -63,45 +89,48 @@ function parseCommandLine(line) {
|
|
|
63
89
|
const parts = line.trim().split(/\s+/).filter(Boolean);
|
|
64
90
|
return { command: parts[0] || '', args: parts.slice(1) };
|
|
65
91
|
}
|
|
66
|
-
export async function
|
|
67
|
-
const
|
|
68
|
-
if (typeof extra.acp_command === 'string' && extra.acp_command.trim()) {
|
|
69
|
-
const args = Array.isArray(extra.acp_args) ? extra.acp_args.map((a) => String(a)) : [];
|
|
70
|
-
return { command: extra.acp_command.trim(), args };
|
|
71
|
-
}
|
|
72
|
-
const envOverride = process.env[`AWB_ACP_COMMAND_${runtime.toUpperCase()}`]?.trim();
|
|
92
|
+
export async function resolveAcpCommandForCli(cli) {
|
|
93
|
+
const envOverride = process.env[`AWB_ACP_COMMAND_${cli.toUpperCase()}`]?.trim();
|
|
73
94
|
if (envOverride)
|
|
74
95
|
return parseCommandLine(envOverride);
|
|
75
|
-
switch (
|
|
76
|
-
case 'claude':
|
|
77
|
-
case 'deepseek': {
|
|
96
|
+
switch (cli) {
|
|
97
|
+
case 'claude': {
|
|
78
98
|
const found = await findOnPath('claude-agent-acp');
|
|
79
|
-
return found
|
|
80
|
-
? { command: found, args: [] }
|
|
81
|
-
: { command: 'npx', args: ['--yes', '@agentclientprotocol/claude-agent-acp'] };
|
|
99
|
+
return found ? { command: found, args: [] } : { command: 'npx', args: ['--yes', '@agentclientprotocol/claude-agent-acp'] };
|
|
82
100
|
}
|
|
83
101
|
case 'codex': {
|
|
84
102
|
const found = await findOnPath('codex-acp');
|
|
85
|
-
return found
|
|
86
|
-
? { command: found, args: [] }
|
|
87
|
-
: { command: 'npx', args: ['--yes', '@zed-industries/codex-acp'] };
|
|
103
|
+
return found ? { command: found, args: [] } : { command: 'npx', args: ['--yes', '@zed-industries/codex-acp'] };
|
|
88
104
|
}
|
|
89
105
|
case 'hermes': {
|
|
90
106
|
const resolved = await resolveHermesAcpCommand();
|
|
91
107
|
return { command: resolved.command, args: [...resolved.argsPrefix] };
|
|
92
108
|
}
|
|
93
109
|
default:
|
|
94
|
-
throw new Error(`No ACP adapter is known for
|
|
110
|
+
throw new Error(`No ACP adapter is known for CLI "${cli}".`);
|
|
95
111
|
}
|
|
96
112
|
}
|
|
113
|
+
export async function detectAcpSessionClis(env = process.env) {
|
|
114
|
+
const out = [];
|
|
115
|
+
if (env.AWB_ACP_COMMAND_CLAUDE || await findOnPath('claude-agent-acp') || await findOnPath('claude'))
|
|
116
|
+
out.push('claude');
|
|
117
|
+
if (env.AWB_ACP_COMMAND_CODEX || await findOnPath('codex-acp') || await findOnPath('codex'))
|
|
118
|
+
out.push('codex');
|
|
119
|
+
if (env.AWB_ACP_COMMAND_HERMES || env.HERMES_ACP_COMMAND || await findOnPath('hermes-acp') || await findOnPath('hermes'))
|
|
120
|
+
out.push('hermes');
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
97
123
|
export class AgentSessionRunner {
|
|
98
124
|
#config;
|
|
99
125
|
#options;
|
|
126
|
+
#store;
|
|
100
127
|
#live = new Map();
|
|
101
128
|
#opening = new Map();
|
|
102
|
-
constructor(config, options
|
|
129
|
+
constructor(config, options) {
|
|
103
130
|
this.#config = config;
|
|
131
|
+
this.#store = options.store ?? new AgentSessionStore();
|
|
104
132
|
this.#options = {
|
|
133
|
+
getManagerId: options.getManagerId,
|
|
105
134
|
idleMinutes: options.idleMinutes ?? DEFAULT_IDLE_MINUTES,
|
|
106
135
|
permissionTimeoutMs: options.permissionTimeoutMs ?? DEFAULT_PERMISSION_TIMEOUT_MS,
|
|
107
136
|
requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
@@ -110,13 +139,18 @@ export class AgentSessionRunner {
|
|
|
110
139
|
commandResolver: options.commandResolver,
|
|
111
140
|
baseEnv: options.baseEnv,
|
|
112
141
|
clientVersion: options.clientVersion,
|
|
142
|
+
mcpServers: options.mcpServers,
|
|
143
|
+
sessionHomesDir: options.sessionHomesDir ?? join(AGENT_MANAGER_HOME, 'session-homes'),
|
|
144
|
+
credentialFetcher: options.credentialFetcher,
|
|
113
145
|
};
|
|
114
146
|
}
|
|
147
|
+
get store() {
|
|
148
|
+
return this.#store;
|
|
149
|
+
}
|
|
115
150
|
_snapshot() {
|
|
116
151
|
return Array.from(this.#live.values()).map((live) => ({
|
|
152
|
+
cli: live.cli,
|
|
117
153
|
session_id: live.sessionId,
|
|
118
|
-
agent_id: live.agentId,
|
|
119
|
-
runtime: live.runtime,
|
|
120
154
|
busy: live.turn !== null || live.pendingPermissions.size > 0,
|
|
121
155
|
pid: live.client.process.pid ?? null,
|
|
122
156
|
}));
|
|
@@ -124,194 +158,247 @@ export class AgentSessionRunner {
|
|
|
124
158
|
countInFlight() {
|
|
125
159
|
return this._snapshot().filter((s) => s.busy).length;
|
|
126
160
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
});
|
|
140
|
-
}
|
|
161
|
+
#ref(cli, sessionId) {
|
|
162
|
+
return { manager_id: this.#options.getManagerId(), cli, session_id: sessionId };
|
|
163
|
+
}
|
|
164
|
+
#key(cli, sessionId) {
|
|
165
|
+
return `${cli}:${sessionId}`;
|
|
166
|
+
}
|
|
167
|
+
async handle(request) {
|
|
168
|
+
const cli = String(request.cli || '').toLowerCase();
|
|
169
|
+
const sessionId = request.session_id || '';
|
|
170
|
+
const tag = `[agent-session ${cli}${sessionId ? ` ${sessionId.slice(0, 8)}` : ''}]`;
|
|
171
|
+
if (request.request_id) {
|
|
172
|
+
await this.#handleRpc(request, cli, sessionId, tag);
|
|
141
173
|
return;
|
|
142
174
|
}
|
|
143
|
-
const
|
|
144
|
-
const liveBefore = this.#live.get(sid);
|
|
175
|
+
const liveBefore = sessionId ? this.#live.get(this.#key(cli, sessionId)) : undefined;
|
|
145
176
|
try {
|
|
146
177
|
switch (request.op) {
|
|
147
|
-
case 'open':
|
|
148
|
-
await this.#ensureLive(request, ctx, rest);
|
|
149
|
-
return;
|
|
150
178
|
case 'prompt': {
|
|
151
|
-
|
|
179
|
+
if (!sessionId)
|
|
180
|
+
return;
|
|
181
|
+
const live = await this.#ensureLive(cli, sessionId, request.cwd || '', request.title || '', request);
|
|
152
182
|
await this.#runPrompt(live, request.turn_id || randomUUID(), request.text || '');
|
|
153
183
|
return;
|
|
154
184
|
}
|
|
155
185
|
case 'permission':
|
|
156
|
-
this.#resolvePermission(
|
|
186
|
+
this.#resolvePermission(cli, sessionId, request.permission_request_id || '', request.option_id ?? null);
|
|
157
187
|
return;
|
|
158
188
|
case 'cancel': {
|
|
159
|
-
const live = this.#live.get(
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
await live.client.cancel(live.nativeSessionId).catch(() => undefined);
|
|
189
|
+
const live = this.#live.get(this.#key(cli, sessionId));
|
|
190
|
+
if (live)
|
|
191
|
+
await live.client.cancel(live.sessionId).catch(() => undefined);
|
|
163
192
|
return;
|
|
164
193
|
}
|
|
165
194
|
case 'set_mode': {
|
|
166
|
-
const live =
|
|
195
|
+
const live = this.#live.get(this.#key(cli, sessionId));
|
|
167
196
|
const modeId = request.mode_id || '';
|
|
168
|
-
if (!modeId)
|
|
197
|
+
if (!live || !modeId)
|
|
169
198
|
return;
|
|
170
|
-
await live.client.request('session/set_mode', { sessionId: live.
|
|
199
|
+
await live.client.request('session/set_mode', { sessionId: live.sessionId, modeId }, { timeoutMs: this.#options.requestTimeoutMs });
|
|
171
200
|
this.#enqueue(live, [{ type: 'system', payload: { text: `Mode set to ${modeId}.` } }], { current_mode: modeId, reason: 'mode' });
|
|
172
201
|
return;
|
|
173
202
|
}
|
|
174
203
|
case 'close':
|
|
175
|
-
await this.#closeLive(
|
|
204
|
+
await this.#closeLive(cli, sessionId, 'closed');
|
|
176
205
|
return;
|
|
177
206
|
default:
|
|
178
207
|
log(`${tag} unknown op ${String(request.op)}`);
|
|
179
208
|
}
|
|
180
|
-
const settled = this.#live.get(
|
|
209
|
+
const settled = this.#live.get(this.#key(cli, sessionId)) ?? liveBefore;
|
|
181
210
|
if (settled)
|
|
182
211
|
await settled.postChain;
|
|
183
212
|
}
|
|
184
213
|
catch (err) {
|
|
185
|
-
const message = err?.message ?? String(err);
|
|
214
|
+
const message = redactSecrets(err?.message ?? String(err));
|
|
186
215
|
log(`${tag} ${request.op} failed: ${message}`);
|
|
187
|
-
const live = this.#live.get(
|
|
216
|
+
const live = this.#live.get(this.#key(cli, sessionId));
|
|
217
|
+
const failure = { type: 'error', payload: { message, code: err?.code ?? undefined }, turn_id: request.turn_id };
|
|
218
|
+
const state = { status: live?.turn ? 'busy' : 'error', last_error: message, reason: `${request.op}_failed` };
|
|
188
219
|
if (live) {
|
|
189
|
-
this.#enqueue(live, [
|
|
190
|
-
|
|
191
|
-
last_error: message,
|
|
192
|
-
reason: `${request.op}_failed`,
|
|
193
|
-
});
|
|
220
|
+
this.#enqueue(live, [failure], state);
|
|
221
|
+
await live.postChain;
|
|
194
222
|
}
|
|
195
|
-
else {
|
|
196
|
-
await postAgentSessionEvents(
|
|
197
|
-
status: 'error',
|
|
198
|
-
last_error: message,
|
|
199
|
-
reason: `${request.op}_failed`,
|
|
200
|
-
});
|
|
223
|
+
else if (sessionId) {
|
|
224
|
+
await postAgentSessionEvents(this.#config, this.#ref(cli, sessionId), [{ ...failure, seq: 0, id: `${sessionId}:err:${Date.now()}`, created_at: new Date().toISOString() }], state);
|
|
201
225
|
}
|
|
202
226
|
}
|
|
203
227
|
}
|
|
228
|
+
async #handleRpc(request, cli, sessionId, tag) {
|
|
229
|
+
const requestId = request.request_id;
|
|
230
|
+
const managerId = this.#options.getManagerId();
|
|
231
|
+
try {
|
|
232
|
+
switch (request.op) {
|
|
233
|
+
case 'list': {
|
|
234
|
+
const sessions = await this.#store.listSessions(cli);
|
|
235
|
+
const withLive = sessions.map((s) => ({ ...s, live_status: this.#live.get(this.#key(cli, s.session_id)) ? this.#statusOf(this.#live.get(this.#key(cli, s.session_id))) : undefined }));
|
|
236
|
+
await postAgentSessionRpcResponse(this.#config, managerId, requestId, { ok: true, result: { sessions: withLive } });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
case 'history': {
|
|
240
|
+
if (!sessionId)
|
|
241
|
+
throw Object.assign(new Error('session_id is required'), { code: 'not_found' });
|
|
242
|
+
const history = await this.#store.readHistory(cli, sessionId);
|
|
243
|
+
const live = this.#live.get(this.#key(cli, sessionId));
|
|
244
|
+
if (!history.session && !live) {
|
|
245
|
+
await postAgentSessionRpcResponse(this.#config, managerId, requestId, { ok: false, error: 'Session not found on this Runtime Host.', code: 'not_found' });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
await postAgentSessionRpcResponse(this.#config, managerId, requestId, {
|
|
249
|
+
ok: true,
|
|
250
|
+
result: {
|
|
251
|
+
session: history.session ?? (live ? this.#summaryOf(live) : null),
|
|
252
|
+
events: history.events,
|
|
253
|
+
truncated: history.truncated,
|
|
254
|
+
live: live ? this.#stateOf(live) : null,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
case 'open': {
|
|
260
|
+
const live = await this.#ensureLive(cli, sessionId, request.cwd || '', request.title || '', request);
|
|
261
|
+
await postAgentSessionRpcResponse(this.#config, managerId, requestId, { ok: true, result: this.#stateOf(live) });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
default:
|
|
265
|
+
await postAgentSessionRpcResponse(this.#config, managerId, requestId, { ok: false, error: `Unknown RPC op ${String(request.op)}`, code: 'bad_request' });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
const message = redactSecrets(err?.message ?? String(err));
|
|
270
|
+
log(`${tag} ${request.op} rpc failed: ${message}`);
|
|
271
|
+
await postAgentSessionRpcResponse(this.#config, managerId, requestId, { ok: false, error: message, code: err?.code ?? 'manager_error' });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
204
274
|
async stopAll(reason = 'manager_shutdown') {
|
|
205
|
-
const
|
|
206
|
-
await Promise.all(
|
|
207
|
-
}
|
|
208
|
-
async #ensureLive(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
275
|
+
const keys = Array.from(this.#live.values()).map((l) => [l.cli, l.sessionId]);
|
|
276
|
+
await Promise.all(keys.map(([cli, id]) => this.#closeLive(cli, id, 'idle', reason).catch(() => undefined)));
|
|
277
|
+
}
|
|
278
|
+
async #ensureLive(cli, sessionId, cwd, title, request) {
|
|
279
|
+
if (sessionId) {
|
|
280
|
+
const existing = this.#live.get(this.#key(cli, sessionId));
|
|
281
|
+
if (existing && !existing.exited && !existing.closing) {
|
|
282
|
+
const wanted = request.credential_id || '';
|
|
283
|
+
if (existing.credentialId === wanted || existing.turn)
|
|
284
|
+
return existing;
|
|
285
|
+
log(`[agent-session ${cli} ${sessionId.slice(0, 8)}] credential binding changed (${existing.credentialId || 'operator-login'} → ${wanted || 'operator-login'}); reopening`);
|
|
286
|
+
await this.#closeLive(cli, sessionId, 'idle', 'credential_changed');
|
|
287
|
+
}
|
|
288
|
+
const inFlight = this.#opening.get(this.#key(cli, sessionId));
|
|
289
|
+
if (inFlight)
|
|
290
|
+
return inFlight;
|
|
291
|
+
}
|
|
292
|
+
const opening = this.#open(cli, sessionId, cwd, title, request);
|
|
293
|
+
if (sessionId) {
|
|
294
|
+
this.#opening.set(this.#key(cli, sessionId), opening);
|
|
295
|
+
opening.finally(() => this.#opening.delete(this.#key(cli, sessionId))).catch(() => undefined);
|
|
296
|
+
}
|
|
218
297
|
return opening;
|
|
219
298
|
}
|
|
220
|
-
async #open(
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
299
|
+
async #open(cli, requestedSessionId, requestedCwd, title, request) {
|
|
300
|
+
const tag = `[agent-session ${cli}${requestedSessionId ? ` ${requestedSessionId.slice(0, 8)}` : ' new'}]`;
|
|
301
|
+
let cwd = requestedCwd.trim();
|
|
302
|
+
if (requestedSessionId && !cwd) {
|
|
303
|
+
const history = await this.#store.readHistory(cli, requestedSessionId).catch(() => null);
|
|
304
|
+
cwd = history?.session?.cwd || '';
|
|
305
|
+
}
|
|
225
306
|
if (!cwd)
|
|
226
|
-
throw new Error('No working directory:
|
|
307
|
+
throw new Error('No working directory: pass cwd for a new session.');
|
|
227
308
|
try {
|
|
228
309
|
await access(cwd, fsConstants.R_OK);
|
|
229
310
|
}
|
|
230
311
|
catch {
|
|
231
312
|
throw new Error(`Working directory does not exist on this Runtime Host: ${cwd}`);
|
|
232
313
|
}
|
|
233
|
-
const resolver = this.#options.commandResolver ??
|
|
234
|
-
const { command, args } = await resolver(
|
|
235
|
-
const
|
|
236
|
-
log(`${tag} spawning ACP adapter
|
|
314
|
+
const resolver = this.#options.commandResolver ?? resolveAcpCommandForCli;
|
|
315
|
+
const { command, args } = await resolver(cli);
|
|
316
|
+
const auth = await this.#prepareAuth(cli, cwd, request);
|
|
317
|
+
log(`${tag} spawning ACP adapter cmd=${command} ${args.join(' ')} cwd=${cwd} auth=${auth.label}`);
|
|
318
|
+
let live = null;
|
|
237
319
|
const client = await AcpClient.spawn({
|
|
238
320
|
command,
|
|
239
321
|
args,
|
|
240
322
|
cwd,
|
|
241
|
-
env,
|
|
323
|
+
env: this.#buildEnv(cli, requestedSessionId || 'new', auth),
|
|
242
324
|
requestTimeoutMs: this.#options.requestTimeoutMs,
|
|
243
|
-
onEvent: (event) =>
|
|
244
|
-
|
|
245
|
-
|
|
325
|
+
onEvent: (event) => { if (live)
|
|
326
|
+
this.#onEvent(live, event); },
|
|
327
|
+
onPermissionRequest: (permission) => (live ? this.#onPermission(live, permission) : Promise.resolve({ outcome: 'cancelled' })),
|
|
328
|
+
onStderr: (line) => log(`${tag} stderr: ${redactSecrets(line)}`),
|
|
246
329
|
spawnOptions: { detached: process.platform !== 'win32' },
|
|
247
330
|
});
|
|
248
|
-
const live = {
|
|
249
|
-
sessionId: sid,
|
|
250
|
-
agentId: ctx.agent_id,
|
|
251
|
-
workspaceId: request.workspace_id,
|
|
252
|
-
runtime,
|
|
253
|
-
cwd,
|
|
254
|
-
client,
|
|
255
|
-
nativeSessionId: '',
|
|
256
|
-
loadSupported: false,
|
|
257
|
-
permissionPolicy: request.permission_policy || 'ask',
|
|
258
|
-
rest,
|
|
259
|
-
pendingPermissions: new Map(),
|
|
260
|
-
turn: null,
|
|
261
|
-
textBuffer: '',
|
|
262
|
-
reasoningBuffer: '',
|
|
263
|
-
flushTimer: null,
|
|
264
|
-
idleTimer: null,
|
|
265
|
-
postChain: Promise.resolve(),
|
|
266
|
-
closing: false,
|
|
267
|
-
exited: false,
|
|
268
|
-
};
|
|
269
|
-
this.#live.set(sid, live);
|
|
270
|
-
client.process.once('exit', (code, signal) => this.#onProcessExit(sid, code, signal));
|
|
271
331
|
try {
|
|
272
332
|
const initialized = await client.initialize({
|
|
273
333
|
clientInfo: { name: 'awb-agent-session', version: this.#options.clientVersion || '1' },
|
|
274
334
|
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
|
|
275
335
|
});
|
|
276
336
|
const caps = (initialized?.agentCapabilities ?? {});
|
|
277
|
-
|
|
278
|
-
const
|
|
337
|
+
const loadSupported = caps.loadSession === true;
|
|
338
|
+
const sessionIdForMcp = requestedSessionId || 'new';
|
|
339
|
+
const mcpServers = this.#options.mcpServers ? this.#options.mcpServers(sessionIdForMcp) : this.#defaultMcpServers(sessionIdForMcp);
|
|
340
|
+
live = {
|
|
341
|
+
cli,
|
|
342
|
+
sessionId: requestedSessionId,
|
|
343
|
+
cwd,
|
|
344
|
+
title,
|
|
345
|
+
client,
|
|
346
|
+
loadSupported,
|
|
347
|
+
loading: false,
|
|
348
|
+
pendingPermissions: new Map(),
|
|
349
|
+
turn: null,
|
|
350
|
+
textBuffer: '',
|
|
351
|
+
reasoningBuffer: '',
|
|
352
|
+
flushTimer: null,
|
|
353
|
+
idleTimer: null,
|
|
354
|
+
postChain: Promise.resolve(),
|
|
355
|
+
seq: 0,
|
|
356
|
+
nonce: randomUUID().slice(0, 8),
|
|
357
|
+
credentialId: request.credential_id || '',
|
|
358
|
+
closing: false,
|
|
359
|
+
exited: false,
|
|
360
|
+
};
|
|
361
|
+
let modes;
|
|
279
362
|
let resumed = false;
|
|
280
|
-
|
|
281
|
-
|
|
363
|
+
if (requestedSessionId) {
|
|
364
|
+
if (!loadSupported) {
|
|
365
|
+
throw Object.assign(new Error(`The ${cli} ACP adapter cannot resume existing sessions (no loadSession capability).`), { code: 'resume_unsupported' });
|
|
366
|
+
}
|
|
367
|
+
live.loading = true;
|
|
282
368
|
try {
|
|
283
|
-
const loaded = await client.loadSession({ sessionId:
|
|
284
|
-
live.nativeSessionId = request.native_session_id;
|
|
369
|
+
const loaded = await client.loadSession({ sessionId: requestedSessionId, cwd, mcpServers });
|
|
285
370
|
modes = loaded?.modes;
|
|
286
371
|
resumed = true;
|
|
287
372
|
}
|
|
288
|
-
|
|
289
|
-
|
|
373
|
+
finally {
|
|
374
|
+
live.loading = false;
|
|
290
375
|
}
|
|
291
376
|
}
|
|
292
|
-
|
|
377
|
+
else {
|
|
293
378
|
const created = await client.newSession({ cwd, mcpServers });
|
|
294
|
-
live.
|
|
379
|
+
live.sessionId = created.sessionId;
|
|
295
380
|
modes = created.modes;
|
|
381
|
+
await this.#store.recordAwbSession({ cli, session_id: live.sessionId, cwd, title }).catch(() => undefined);
|
|
296
382
|
}
|
|
383
|
+
if (!live.sessionId)
|
|
384
|
+
throw new Error('ACP adapter returned no session id.');
|
|
385
|
+
const key = this.#key(cli, live.sessionId);
|
|
386
|
+
this.#live.set(key, live);
|
|
387
|
+
client.process.once('exit', (code, signal) => this.#onProcessExit(key, code, signal));
|
|
297
388
|
const modeInfo = this.#parseModes(modes);
|
|
298
389
|
this.#enqueue(live, [{
|
|
299
390
|
type: 'system',
|
|
300
391
|
payload: {
|
|
301
|
-
text: resumed
|
|
302
|
-
|
|
303
|
-
: `Session opened (${runtime}, ${cwd}).`,
|
|
304
|
-
runtime,
|
|
305
|
-
cwd,
|
|
306
|
-
resumed,
|
|
307
|
-
command: `${command} ${args.join(' ')}`.trim(),
|
|
392
|
+
text: resumed ? `Session resumed (${cli}, ${cwd}).` : `Session opened (${cli}, ${cwd}).`,
|
|
393
|
+
cli, cwd, resumed, command: `${command} ${args.join(' ')}`.trim(),
|
|
308
394
|
},
|
|
309
395
|
}], {
|
|
310
396
|
status: 'ready',
|
|
311
|
-
|
|
312
|
-
|
|
397
|
+
cwd,
|
|
398
|
+
...(title ? { title } : {}),
|
|
313
399
|
current_mode: modeInfo.current,
|
|
314
400
|
available_modes: modeInfo.available,
|
|
401
|
+
resume_supported: loadSupported,
|
|
315
402
|
last_error: null,
|
|
316
403
|
reason: resumed ? 'resumed' : 'opened',
|
|
317
404
|
});
|
|
@@ -319,43 +406,86 @@ export class AgentSessionRunner {
|
|
|
319
406
|
return live;
|
|
320
407
|
}
|
|
321
408
|
catch (err) {
|
|
322
|
-
|
|
323
|
-
|
|
409
|
+
const child = client.process;
|
|
410
|
+
client.close();
|
|
411
|
+
if (child?.pid)
|
|
412
|
+
await terminateDetachedProcessTree(child.pid, 250, { child }).catch(() => undefined);
|
|
324
413
|
throw err;
|
|
325
414
|
}
|
|
326
415
|
}
|
|
327
|
-
#buildEnv(
|
|
416
|
+
#buildEnv(cli, sessionId, auth) {
|
|
328
417
|
const env = { ...(this.#options.baseEnv ?? process.env) };
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
else if (ctx.cli_home_dir) {
|
|
334
|
-
try {
|
|
335
|
-
const adapter = createAdapter(runtime === 'deepseek' ? 'deepseek' : runtime);
|
|
336
|
-
const configDirEnv = adapter.configDirEnv();
|
|
337
|
-
if (configDirEnv)
|
|
338
|
-
env[configDirEnv] = ctx.cli_home_dir;
|
|
339
|
-
}
|
|
340
|
-
catch {
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
Object.assign(env, ctx.extra_env ?? {});
|
|
344
|
-
env.AWB_AGENT_ID = ctx.agent_id;
|
|
345
|
-
env.AWB_SESSION_ID = sessionId;
|
|
346
|
-
env.AWB_API_KEY = ctx.api_key;
|
|
418
|
+
for (const key of auth.stripEnvKeys)
|
|
419
|
+
delete env[key];
|
|
420
|
+
Object.assign(env, auth.env);
|
|
347
421
|
env.AWB_URL = this.#config.url;
|
|
422
|
+
env.AWB_MANAGER_ID = this.#options.getManagerId();
|
|
423
|
+
env.AWB_SESSION_CLI = cli;
|
|
424
|
+
env.AWB_SESSION_ID = sessionId;
|
|
348
425
|
return env;
|
|
349
426
|
}
|
|
350
|
-
#
|
|
427
|
+
async #prepareAuth(cli, cwd, request) {
|
|
428
|
+
const none = { label: 'operator-login', env: {}, stripEnvKeys: [], cliHome: null };
|
|
429
|
+
const credentialId = request.credential_id || '';
|
|
430
|
+
if (!credentialId)
|
|
431
|
+
return none;
|
|
432
|
+
const prefix = SESSION_CLI_CREDENTIAL_PREFIX[cli];
|
|
433
|
+
if (!prefix)
|
|
434
|
+
throw Object.assign(new Error(`${cli} sessions cannot use an AWB credential.`), { code: 'credential_unsupported' });
|
|
435
|
+
const fetcher = this.#options.credentialFetcher
|
|
436
|
+
?? ((id, ws) => fetchSessionCredential(this.#config, this.#options.getManagerId(), id, ws));
|
|
437
|
+
const fetched = await fetcher(credentialId, request.workspace_id || '');
|
|
438
|
+
if (!fetched)
|
|
439
|
+
throw Object.assign(new Error('The credential assigned in CLI settings could not be fetched from AWB.'), { code: 'credential_unavailable' });
|
|
440
|
+
if (!fetched.provider.startsWith(prefix)) {
|
|
441
|
+
throw Object.assign(new Error(`CLI settings credential provider ${fetched.provider} does not match ${cli}.`), { code: 'credential_provider_mismatch' });
|
|
442
|
+
}
|
|
443
|
+
const { fields, repaired } = normalizeCredentialFields(fetched.fields);
|
|
444
|
+
if (repaired.length)
|
|
445
|
+
log(`[agent-session ${cli}] credential ${fetched.credential_id.slice(0, 8)} whitespace repaired in: ${repaired.join(', ')}`);
|
|
446
|
+
const missing = (SESSION_REQUIRED_CREDENTIAL_FIELDS[fetched.provider] ?? []).filter((key) => !fields[key]);
|
|
447
|
+
if (missing.length) {
|
|
448
|
+
throw Object.assign(new Error(`CLI settings credential ${fetched.provider} is missing ${missing.join(', ')} — re-save it in Settings → Credentials.`), { code: 'credential_incomplete' });
|
|
449
|
+
}
|
|
450
|
+
const credential = { ...fetched, fields };
|
|
451
|
+
const adapter = createRuntimeCliAdapter(cli);
|
|
452
|
+
const cliHome = join(this.#options.sessionHomesDir, cli, credential.credential_id);
|
|
453
|
+
await mkdir(cliHome, { recursive: true, mode: 0o700 });
|
|
454
|
+
await this.#linkSessionStore(cli, cliHome);
|
|
455
|
+
const prep = await adapter.prepareCliHome(cliHome, credential, null);
|
|
456
|
+
const env = { ...(prep.extraEnv ?? {}) };
|
|
457
|
+
const configDirEnv = adapter.configDirEnv();
|
|
458
|
+
if (configDirEnv)
|
|
459
|
+
env[configDirEnv] = cliHome;
|
|
460
|
+
const stripEnvKeys = adapter.authEnvKeys().filter((key) => !(key in env));
|
|
461
|
+
await adapter.ensureWorkspaceTrust(cliHome, cwd).catch((err) => log(`[agent-session ${cli}] trust seed failed: ${err?.message ?? err}`));
|
|
462
|
+
return { label: `credential:${credential.provider}`, env, stripEnvKeys, cliHome };
|
|
463
|
+
}
|
|
464
|
+
async #linkSessionStore(cli, cliHome) {
|
|
465
|
+
const subdir = SESSION_STORE_SUBDIR[cli];
|
|
466
|
+
if (!subdir)
|
|
467
|
+
return;
|
|
468
|
+
const operatorHome = cli === 'claude' ? resolveClaudeHome(this.#options.baseEnv ?? process.env) : resolveCodexHome(this.#options.baseEnv ?? process.env);
|
|
469
|
+
const target = join(operatorHome, subdir);
|
|
470
|
+
const linkPath = join(cliHome, subdir);
|
|
471
|
+
await mkdir(target, { recursive: true });
|
|
472
|
+
try {
|
|
473
|
+
const existing = await lstat(linkPath);
|
|
474
|
+
if (existing.isSymbolicLink() || existing.isDirectory())
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
}
|
|
479
|
+
await symlink(target, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
|
|
480
|
+
}
|
|
481
|
+
#defaultMcpServers(sessionId) {
|
|
351
482
|
return [{
|
|
352
483
|
type: 'http',
|
|
353
484
|
name: 'awb',
|
|
354
485
|
url: `${this.#config.url.replace(/\/$/, '')}/mcp`,
|
|
355
486
|
headers: [
|
|
356
|
-
{ name: 'Authorization', value: `Bearer ${
|
|
487
|
+
{ name: 'Authorization', value: `Bearer ${this.#config.apiKey}` },
|
|
357
488
|
{ name: 'X-AWB-Client-Type', value: 'agent-session' },
|
|
358
|
-
{ name: 'X-AWB-Agent-Id', value: ctx.agent_id },
|
|
359
489
|
{ name: 'X-AWB-Session-Id', value: sessionId },
|
|
360
490
|
],
|
|
361
491
|
}];
|
|
@@ -376,6 +506,28 @@ export class AgentSessionRunner {
|
|
|
376
506
|
}));
|
|
377
507
|
return { current, available };
|
|
378
508
|
}
|
|
509
|
+
#statusOf(live) {
|
|
510
|
+
if (live.exited || live.closing)
|
|
511
|
+
return 'idle';
|
|
512
|
+
if (live.pendingPermissions.size > 0)
|
|
513
|
+
return 'awaiting_permission';
|
|
514
|
+
if (live.turn)
|
|
515
|
+
return 'busy';
|
|
516
|
+
return 'ready';
|
|
517
|
+
}
|
|
518
|
+
#summaryOf(live) {
|
|
519
|
+
const now = new Date().toISOString();
|
|
520
|
+
return { cli: live.cli, session_id: live.sessionId, cwd: live.cwd, title: live.title, created_at: now, updated_at: now, source: 'awb' };
|
|
521
|
+
}
|
|
522
|
+
#stateOf(live) {
|
|
523
|
+
return {
|
|
524
|
+
session_id: live.sessionId,
|
|
525
|
+
cwd: live.cwd,
|
|
526
|
+
title: live.title,
|
|
527
|
+
status: this.#statusOf(live),
|
|
528
|
+
resume_supported: live.loadSupported,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
379
531
|
async #runPrompt(live, turnId, text) {
|
|
380
532
|
if (live.turn) {
|
|
381
533
|
this.#enqueue(live, [{ type: 'error', payload: { message: 'A turn is already in progress.', code: 'turn_in_progress' }, turn_id: turnId }]);
|
|
@@ -383,9 +535,13 @@ export class AgentSessionRunner {
|
|
|
383
535
|
}
|
|
384
536
|
live.turn = { turnId, startedAt: Date.now() };
|
|
385
537
|
this.#clearIdle(live);
|
|
386
|
-
|
|
538
|
+
if (!live.title) {
|
|
539
|
+
live.title = text.trim().replace(/\s+/g, ' ').slice(0, 80);
|
|
540
|
+
await this.#store.touchAwbSession(live.cli, live.sessionId, { title: live.title }).catch(() => undefined);
|
|
541
|
+
}
|
|
542
|
+
this.#enqueue(live, [{ type: 'turn', payload: { phase: 'started' }, turn_id: turnId }], { status: 'busy', title: live.title, reason: 'turn_started' });
|
|
387
543
|
try {
|
|
388
|
-
const response = await live.client.prompt({ sessionId: live.
|
|
544
|
+
const response = await live.client.prompt({ sessionId: live.sessionId, prompt: [{ type: 'text', text }] }, { timeoutMs: this.#options.promptTimeoutMs });
|
|
389
545
|
this.#flushBuffers(live, turnId);
|
|
390
546
|
const events = [];
|
|
391
547
|
if (response?.usage) {
|
|
@@ -403,23 +559,24 @@ export class AgentSessionRunner {
|
|
|
403
559
|
}
|
|
404
560
|
events.push({ type: 'turn', payload: { phase: 'finished', stop_reason: response?.stopReason || 'end_turn' }, turn_id: turnId });
|
|
405
561
|
this.#enqueue(live, events, { status: 'ready', last_error: null, reason: 'turn_finished' });
|
|
562
|
+
await this.#store.touchAwbSession(live.cli, live.sessionId).catch(() => undefined);
|
|
406
563
|
}
|
|
407
564
|
catch (err) {
|
|
408
565
|
this.#flushBuffers(live, turnId);
|
|
409
|
-
const message = err?.message ?? String(err);
|
|
566
|
+
const message = redactSecrets(err?.message ?? String(err));
|
|
410
567
|
this.#enqueue(live, [
|
|
411
568
|
{ type: 'error', payload: { message, code: err?.code ?? undefined }, turn_id: turnId },
|
|
412
569
|
{ type: 'turn', payload: { phase: 'finished', stop_reason: 'error' }, turn_id: turnId },
|
|
413
|
-
], { status: live.exited ? '
|
|
570
|
+
], { status: live.exited ? 'idle' : 'error', last_error: message, reason: 'turn_failed' });
|
|
414
571
|
}
|
|
415
572
|
finally {
|
|
416
573
|
live.turn = null;
|
|
417
574
|
this.#touch(live);
|
|
575
|
+
await live.postChain;
|
|
418
576
|
}
|
|
419
577
|
}
|
|
420
|
-
#onEvent(
|
|
421
|
-
|
|
422
|
-
if (!live)
|
|
578
|
+
#onEvent(live, event) {
|
|
579
|
+
if (live.loading)
|
|
423
580
|
return;
|
|
424
581
|
const turnId = live.turn?.turnId;
|
|
425
582
|
switch (event.type) {
|
|
@@ -443,7 +600,7 @@ export class AgentSessionRunner {
|
|
|
443
600
|
return;
|
|
444
601
|
}
|
|
445
602
|
case 'tool_updated':
|
|
446
|
-
case 'tool_completed':
|
|
603
|
+
case 'tool_completed':
|
|
447
604
|
this.#flushBuffers(live, turnId);
|
|
448
605
|
this.#enqueue(live, [{
|
|
449
606
|
type: 'tool_update',
|
|
@@ -451,22 +608,14 @@ export class AgentSessionRunner {
|
|
|
451
608
|
turn_id: turnId,
|
|
452
609
|
}]);
|
|
453
610
|
return;
|
|
454
|
-
}
|
|
455
611
|
case 'child_finished':
|
|
456
612
|
this.#flushBuffers(live, turnId);
|
|
457
|
-
this.#enqueue(live, [{
|
|
458
|
-
type: 'tool_update',
|
|
459
|
-
payload: { tool_call_id: event.childRunId, status: event.status, output: boundedValue(event.output) },
|
|
460
|
-
turn_id: turnId,
|
|
461
|
-
}]);
|
|
613
|
+
this.#enqueue(live, [{ type: 'tool_update', payload: { tool_call_id: event.childRunId, status: event.status, output: boundedValue(event.output) }, turn_id: turnId }]);
|
|
462
614
|
return;
|
|
463
615
|
case 'usage':
|
|
464
616
|
this.#enqueue(live, [{
|
|
465
617
|
type: 'usage',
|
|
466
|
-
payload: {
|
|
467
|
-
input_tokens: event.inputTokens, output_tokens: event.outputTokens, total_tokens: event.totalTokens,
|
|
468
|
-
cached_read_tokens: event.cachedReadTokens, thought_tokens: event.thoughtTokens,
|
|
469
|
-
},
|
|
618
|
+
payload: { input_tokens: event.inputTokens, output_tokens: event.outputTokens, total_tokens: event.totalTokens, cached_read_tokens: event.cachedReadTokens, thought_tokens: event.thoughtTokens },
|
|
470
619
|
turn_id: turnId,
|
|
471
620
|
}]);
|
|
472
621
|
return;
|
|
@@ -479,43 +628,32 @@ export class AgentSessionRunner {
|
|
|
479
628
|
this.#enqueue(live, [], { current_mode: modeId, reason: 'mode' });
|
|
480
629
|
return;
|
|
481
630
|
}
|
|
482
|
-
log(`[agent-session ${sessionId.slice(0, 8)}] diagnostic ${event.method}${kind ? ` (${kind})` : ''}`);
|
|
631
|
+
log(`[agent-session ${live.cli} ${live.sessionId.slice(0, 8)}] diagnostic ${event.method}${kind ? ` (${kind})` : ''}`);
|
|
483
632
|
return;
|
|
484
633
|
}
|
|
485
634
|
default:
|
|
486
635
|
return;
|
|
487
636
|
}
|
|
488
637
|
}
|
|
489
|
-
async #onPermission(
|
|
490
|
-
|
|
491
|
-
if (!live)
|
|
638
|
+
async #onPermission(live, permission) {
|
|
639
|
+
if (live.loading)
|
|
492
640
|
return { outcome: 'cancelled' };
|
|
493
641
|
const turnId = live.turn?.turnId;
|
|
494
642
|
this.#flushBuffers(live, turnId);
|
|
495
643
|
const requestId = randomUUID();
|
|
496
644
|
const options = (permission.options ?? []).map((o) => ({ option_id: o.optionId, name: o.name, kind: o.kind }));
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (live.permissionPolicy === 'auto_allow') {
|
|
510
|
-
const pick = (permission.options ?? []).find((o) => ALLOW_KINDS.has(o.kind)) ?? permission.options?.[0];
|
|
511
|
-
this.#enqueue(live, [requestEvent, {
|
|
512
|
-
type: 'permission_decision',
|
|
513
|
-
payload: { request_id: requestId, outcome: pick ? 'selected' : 'cancelled', option_id: pick?.optionId ?? null, decided_by: 'policy' },
|
|
514
|
-
turn_id: turnId,
|
|
515
|
-
}]);
|
|
516
|
-
return pick ? { outcome: 'selected', optionId: pick.optionId } : { outcome: 'cancelled' };
|
|
517
|
-
}
|
|
518
|
-
this.#enqueue(live, [requestEvent], { status: 'awaiting_permission', reason: 'permission' });
|
|
645
|
+
this.#enqueue(live, [{
|
|
646
|
+
type: 'permission_request',
|
|
647
|
+
payload: {
|
|
648
|
+
request_id: requestId,
|
|
649
|
+
tool_call_id: permission.toolCall?.toolCallId ?? '',
|
|
650
|
+
title: permission.toolCall?.title ?? '',
|
|
651
|
+
kind: permission.toolCall?.kind ?? '',
|
|
652
|
+
options,
|
|
653
|
+
raw_input: boundedValue(permission.toolCall?.rawInput ?? permission.toolCall?.raw_input),
|
|
654
|
+
},
|
|
655
|
+
turn_id: turnId,
|
|
656
|
+
}], { status: 'awaiting_permission', reason: 'permission' });
|
|
519
657
|
return new Promise((resolve) => {
|
|
520
658
|
const timer = setTimeout(() => {
|
|
521
659
|
live.pendingPermissions.delete(requestId);
|
|
@@ -530,17 +668,23 @@ export class AgentSessionRunner {
|
|
|
530
668
|
live.pendingPermissions.set(requestId, { resolve, timer });
|
|
531
669
|
});
|
|
532
670
|
}
|
|
533
|
-
#resolvePermission(sessionId, requestId, optionId) {
|
|
534
|
-
const live = this.#live.get(sessionId);
|
|
671
|
+
#resolvePermission(cli, sessionId, requestId, optionId) {
|
|
672
|
+
const live = this.#live.get(this.#key(cli, sessionId));
|
|
535
673
|
if (!live)
|
|
536
674
|
return;
|
|
537
675
|
const pending = live.pendingPermissions.get(requestId);
|
|
538
676
|
if (!pending) {
|
|
539
|
-
log(`[agent-session ${sessionId.slice(0, 8)}] permission ${requestId.slice(0, 8)} not pending (late or duplicate decision)`);
|
|
677
|
+
log(`[agent-session ${cli} ${sessionId.slice(0, 8)}] permission ${requestId.slice(0, 8)} not pending (late or duplicate decision)`);
|
|
540
678
|
return;
|
|
541
679
|
}
|
|
542
680
|
clearTimeout(pending.timer);
|
|
543
681
|
live.pendingPermissions.delete(requestId);
|
|
682
|
+
const allowed = !!optionId && (ALLOW_KINDS.size > 0);
|
|
683
|
+
this.#enqueue(live, [{
|
|
684
|
+
type: 'permission_decision',
|
|
685
|
+
payload: { request_id: requestId, outcome: optionId ? 'selected' : 'cancelled', option_id: optionId, decided_by: 'user' },
|
|
686
|
+
turn_id: live.turn?.turnId,
|
|
687
|
+
}], { status: 'busy', reason: allowed ? 'permission_allowed' : 'permission_denied' });
|
|
544
688
|
pending.resolve(optionId ? { outcome: 'selected', optionId } : { outcome: 'cancelled' });
|
|
545
689
|
}
|
|
546
690
|
#scheduleFlush(live) {
|
|
@@ -568,10 +712,17 @@ export class AgentSessionRunner {
|
|
|
568
712
|
if (events.length)
|
|
569
713
|
this.#enqueue(live, events);
|
|
570
714
|
}
|
|
571
|
-
#enqueue(live, events,
|
|
572
|
-
const
|
|
715
|
+
#enqueue(live, events, state) {
|
|
716
|
+
const now = new Date().toISOString();
|
|
717
|
+
const stamped = events.map((e) => {
|
|
718
|
+
live.seq += 1;
|
|
719
|
+
return { ...e, payload: boundedPayload(e.payload), seq: live.seq, id: `${live.sessionId}:live:${live.nonce}:${live.seq}`, created_at: now };
|
|
720
|
+
});
|
|
721
|
+
const ref = this.#ref(live.cli, live.sessionId);
|
|
573
722
|
live.postChain = live.postChain
|
|
574
|
-
.then(() =>
|
|
723
|
+
.then(() => (stamped.length || state
|
|
724
|
+
? postAgentSessionEvents(this.#config, ref, stamped, state ?? null)
|
|
725
|
+
: Promise.resolve({ ok: true, status: 204 })))
|
|
575
726
|
.then(() => undefined, () => undefined);
|
|
576
727
|
}
|
|
577
728
|
#touch(live) {
|
|
@@ -586,7 +737,7 @@ export class AgentSessionRunner {
|
|
|
586
737
|
this.#touch(live);
|
|
587
738
|
return;
|
|
588
739
|
}
|
|
589
|
-
void this.#closeLive(live.sessionId, '
|
|
740
|
+
void this.#closeLive(live.cli, live.sessionId, 'idle', 'idle');
|
|
590
741
|
}, ms);
|
|
591
742
|
live.idleTimer.unref?.();
|
|
592
743
|
}
|
|
@@ -596,8 +747,8 @@ export class AgentSessionRunner {
|
|
|
596
747
|
live.idleTimer = null;
|
|
597
748
|
}
|
|
598
749
|
}
|
|
599
|
-
#onProcessExit(
|
|
600
|
-
const live = this.#live.get(
|
|
750
|
+
#onProcessExit(key, code, signal) {
|
|
751
|
+
const live = this.#live.get(key);
|
|
601
752
|
if (!live)
|
|
602
753
|
return;
|
|
603
754
|
live.exited = true;
|
|
@@ -609,19 +760,17 @@ export class AgentSessionRunner {
|
|
|
609
760
|
this.#clearIdle(live);
|
|
610
761
|
if (live.closing)
|
|
611
762
|
return;
|
|
612
|
-
this.#live.delete(
|
|
763
|
+
this.#live.delete(key);
|
|
613
764
|
const detail = `Agent process exited (${signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`}).`;
|
|
614
|
-
log(`[agent-session ${sessionId.slice(0, 8)}] ${detail}`);
|
|
765
|
+
log(`[agent-session ${live.cli} ${live.sessionId.slice(0, 8)}] ${detail}`);
|
|
615
766
|
this.#flushBuffers(live, live.turn?.turnId);
|
|
616
|
-
this.#enqueue(live, [{ type: 'system', payload: { text: `${detail} The next prompt reopens the session.` } }], live.turn ? undefined : { status: '
|
|
767
|
+
this.#enqueue(live, [{ type: 'system', payload: { text: `${detail} The next prompt reopens the session.` } }], live.turn ? undefined : { status: 'idle', reason: 'process_exit' });
|
|
617
768
|
}
|
|
618
|
-
async #closeLive(sessionId, finalStatus, reason = finalStatus) {
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
return;
|
|
769
|
+
async #closeLive(cli, sessionId, finalStatus, reason = finalStatus) {
|
|
770
|
+
const key = this.#key(cli, sessionId);
|
|
771
|
+
const live = this.#live.get(key);
|
|
772
|
+
if (!live)
|
|
623
773
|
return;
|
|
624
|
-
}
|
|
625
774
|
live.closing = true;
|
|
626
775
|
this.#clearIdle(live);
|
|
627
776
|
for (const [id, pending] of live.pendingPermissions) {
|
|
@@ -630,29 +779,25 @@ export class AgentSessionRunner {
|
|
|
630
779
|
live.pendingPermissions.delete(id);
|
|
631
780
|
}
|
|
632
781
|
if (live.turn)
|
|
633
|
-
await live.client.cancel(live.
|
|
782
|
+
await live.client.cancel(live.sessionId).catch(() => undefined);
|
|
634
783
|
this.#flushBuffers(live, live.turn?.turnId);
|
|
635
|
-
if (!live.exited
|
|
636
|
-
await live.client.closeSession(live.
|
|
637
|
-
|
|
638
|
-
this.#live.delete(sessionId);
|
|
639
|
-
await this.#killClient(live);
|
|
640
|
-
const text = finalStatus === 'closed'
|
|
641
|
-
? 'Agent process stopped.'
|
|
642
|
-
: reason === 'idle'
|
|
643
|
-
? `Idle for ${this.#options.idleMinutes} min — agent process stopped. The next prompt reopens the session.`
|
|
644
|
-
: `Agent process stopped (${reason}). The next prompt reopens the session.`;
|
|
645
|
-
this.#enqueue(live, [{ type: 'system', payload: { text } }], finalStatus === 'closed'
|
|
646
|
-
? { reason: 'process_stopped' }
|
|
647
|
-
: { status: 'suspended', reason });
|
|
648
|
-
await live.postChain;
|
|
649
|
-
}
|
|
650
|
-
async #killClient(live) {
|
|
784
|
+
if (!live.exited)
|
|
785
|
+
await live.client.closeSession(live.sessionId).catch(() => undefined);
|
|
786
|
+
this.#live.delete(key);
|
|
651
787
|
const child = live.client.process;
|
|
652
788
|
const pid = child?.pid;
|
|
653
789
|
live.client.close();
|
|
654
790
|
if (pid && child)
|
|
655
791
|
await terminateDetachedProcessTree(pid, 250, { child }).catch(() => undefined);
|
|
792
|
+
const text = finalStatus === 'closed'
|
|
793
|
+
? 'Agent process stopped.'
|
|
794
|
+
: reason === 'idle'
|
|
795
|
+
? `Idle for ${this.#options.idleMinutes} min — agent process stopped. The next prompt reopens the session.`
|
|
796
|
+
: reason === 'credential_changed'
|
|
797
|
+
? 'CLI settings changed — reopening the session with the new credential.'
|
|
798
|
+
: `Agent process stopped (${reason}). The next prompt reopens the session.`;
|
|
799
|
+
this.#enqueue(live, [{ type: 'system', payload: { text } }], { status: finalStatus, reason });
|
|
800
|
+
await live.postChain;
|
|
656
801
|
}
|
|
657
802
|
}
|
|
658
803
|
//# sourceMappingURL=agent-session-runner.js.map
|