@borgee/agents-host 0.2.2 → 0.2.26

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 (76) hide show
  1. package/README.md +184 -21
  2. package/dist/agents-host-supervisor.d.ts +7 -5
  3. package/dist/agents-host-supervisor.js +24 -4
  4. package/dist/agents-host.d.ts +89 -15
  5. package/dist/agents-host.js +2099 -141
  6. package/dist/chat/chat-control-plane.d.ts +13 -2
  7. package/dist/chat/sdk-chat-control-plane.d.ts +14 -3
  8. package/dist/chat/sdk-chat-control-plane.js +54 -2
  9. package/dist/cli-args.d.ts +46 -5
  10. package/dist/cli-args.js +313 -32
  11. package/dist/cli.d.ts +9 -0
  12. package/dist/cli.js +112 -5
  13. package/dist/compatibility-gates.d.ts +35 -0
  14. package/dist/compatibility-gates.js +127 -0
  15. package/dist/config.d.ts +1 -0
  16. package/dist/config.js +23 -5
  17. package/dist/connections-state-store.d.ts +81 -0
  18. package/dist/connections-state-store.js +228 -0
  19. package/dist/context/injection.d.ts +109 -0
  20. package/dist/context/injection.js +350 -0
  21. package/dist/context/prompt.d.ts +4 -1
  22. package/dist/context/prompt.js +170 -1
  23. package/dist/context/turn-preparation.d.ts +9 -0
  24. package/dist/context/turn-preparation.js +106 -0
  25. package/dist/debug.d.ts +44 -0
  26. package/dist/debug.js +135 -0
  27. package/dist/gateway/localhost-gateway.d.ts +52 -0
  28. package/dist/gateway/localhost-gateway.js +857 -0
  29. package/dist/index.js +7 -5
  30. package/dist/local-config.d.ts +4 -1
  31. package/dist/local-config.js +24 -7
  32. package/dist/managed-daemon-log.d.ts +34 -0
  33. package/dist/managed-daemon-log.js +261 -0
  34. package/dist/managed-daemon.d.ts +220 -0
  35. package/dist/managed-daemon.js +1601 -0
  36. package/dist/policy/authorization-audit.d.ts +63 -0
  37. package/dist/policy/authorization-audit.js +94 -0
  38. package/dist/policy/copilot-permission.d.ts +15 -0
  39. package/dist/policy/copilot-permission.js +193 -0
  40. package/dist/policy/gateway-authorization.d.ts +42 -0
  41. package/dist/policy/gateway-authorization.js +162 -0
  42. package/dist/providers/awaiting-user.d.ts +12 -0
  43. package/dist/providers/awaiting-user.js +151 -0
  44. package/dist/providers/claude/adapter.d.ts +3 -1
  45. package/dist/providers/claude/adapter.js +8 -12
  46. package/dist/providers/claude/cli-client.d.ts +12 -5
  47. package/dist/providers/claude/cli-client.js +184 -37
  48. package/dist/providers/claude/session-store.d.ts +1 -0
  49. package/dist/providers/codex/adapter.d.ts +11 -0
  50. package/dist/providers/codex/adapter.js +19 -0
  51. package/dist/providers/codex/cli-client.d.ts +103 -0
  52. package/dist/providers/codex/cli-client.js +1133 -0
  53. package/dist/providers/codex/project-doc.d.ts +3 -0
  54. package/dist/providers/codex/project-doc.js +66 -0
  55. package/dist/providers/codex/session-store.d.ts +38 -0
  56. package/dist/providers/codex/session-store.js +150 -0
  57. package/dist/providers/copilot/adapter.d.ts +3 -1
  58. package/dist/providers/copilot/adapter.js +8 -12
  59. package/dist/providers/copilot/cli-client.d.ts +20 -2
  60. package/dist/providers/copilot/cli-client.js +251 -71
  61. package/dist/providers/copilot/session-store.d.ts +1 -0
  62. package/dist/providers/create-provider.d.ts +11 -2
  63. package/dist/providers/create-provider.js +131 -12
  64. package/dist/run.d.ts +1 -0
  65. package/dist/run.js +5 -2
  66. package/dist/state-paths.d.ts +13 -1
  67. package/dist/state-paths.js +84 -3
  68. package/dist/task-thread-resolution.d.ts +10 -0
  69. package/dist/task-thread-resolution.js +48 -0
  70. package/dist/types.d.ts +174 -1
  71. package/dist/visible-mentions.d.ts +3 -0
  72. package/dist/visible-mentions.js +15 -0
  73. package/package.json +19 -17
  74. package/skills/borgee-agent/SKILL.md +33 -0
  75. package/skills/borgee-agent/borgee-agent.mjs +473 -0
  76. package/skills/borgee-agent/borgee-agent.py +409 -0
@@ -0,0 +1,151 @@
1
+ export const HOST_CONTROL_PREFIX = '[[BORGEE_CONTROL]] ';
2
+ export const AWAITING_USER_CONTROL_PREFIX = HOST_CONTROL_PREFIX;
3
+ function hasVisibleText(value) {
4
+ return value.trim().length > 0;
5
+ }
6
+ function stripTrailingCarriageReturn(value) {
7
+ return value.endsWith('\r') ? value.slice(0, -1) : value;
8
+ }
9
+ function matchControlLine(sourceText, trimTrailingWhitespace) {
10
+ const analyzedText = trimTrailingWhitespace
11
+ ? sourceText.replace(/[ \t\r\n]+$/u, '')
12
+ : sourceText;
13
+ const lastNewline = analyzedText.lastIndexOf('\n');
14
+ const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
15
+ const line = stripTrailingCarriageReturn(analyzedText.slice(lineStart));
16
+ const removalStart = lastNewline === -1 ? 0 : lastNewline;
17
+ if (line.startsWith(HOST_CONTROL_PREFIX)) {
18
+ return {
19
+ kind: 'footer',
20
+ analyzedText,
21
+ removalStart,
22
+ payload: line.slice(HOST_CONTROL_PREFIX.length),
23
+ };
24
+ }
25
+ if (line.length > 0 && HOST_CONTROL_PREFIX.startsWith(line)) {
26
+ return {
27
+ kind: 'plausible-prefix',
28
+ analyzedText,
29
+ removalStart,
30
+ };
31
+ }
32
+ return {
33
+ kind: 'none',
34
+ analyzedText,
35
+ removalStart,
36
+ };
37
+ }
38
+ function parseControlPayload(payload) {
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(payload);
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
47
+ return null;
48
+ }
49
+ const record = parsed;
50
+ if (typeof record.kind !== 'string') {
51
+ return null;
52
+ }
53
+ if (record.kind === 'continue-to-peer' || record.kind === 'conclude-locally') {
54
+ return Object.keys(record).length === 1
55
+ ? { kind: record.kind }
56
+ : null;
57
+ }
58
+ if (record.kind === 'start-protocol') {
59
+ return Object.keys(record).length === 2
60
+ && typeof record.rounds === 'number'
61
+ && Number.isInteger(record.rounds)
62
+ && record.rounds > 0
63
+ ? {
64
+ kind: 'start-protocol',
65
+ rounds: record.rounds,
66
+ }
67
+ : null;
68
+ }
69
+ if (record.kind === 'awaiting-user') {
70
+ const keys = Object.keys(record);
71
+ if (keys.some((key) => key !== 'kind' && key !== 'question' && key !== 'reason')) {
72
+ return null;
73
+ }
74
+ if (typeof record.question !== 'string' || record.question.trim().length === 0) {
75
+ return null;
76
+ }
77
+ if (record.reason !== undefined
78
+ && (typeof record.reason !== 'string' || record.reason.trim().length === 0)) {
79
+ return null;
80
+ }
81
+ return {
82
+ kind: 'awaiting-user',
83
+ question: record.question.trim(),
84
+ ...(typeof record.reason === 'string' ? { reason: record.reason.trim() } : {}),
85
+ };
86
+ }
87
+ return null;
88
+ }
89
+ export function parseProviderReply(text) {
90
+ const match = matchControlLine(text, true);
91
+ if (match.kind !== 'footer' || match.payload === undefined) {
92
+ return { text };
93
+ }
94
+ const control = parseControlPayload(match.payload);
95
+ if (!control) {
96
+ return {
97
+ text: stripTrailingCarriageReturn(match.analyzedText.slice(0, match.removalStart)),
98
+ controlMalformed: true,
99
+ };
100
+ }
101
+ return {
102
+ text: stripTrailingCarriageReturn(match.analyzedText.slice(0, match.removalStart)),
103
+ control,
104
+ ...(control.kind === 'awaiting-user'
105
+ ? {
106
+ awaitingUser: {
107
+ question: control.question,
108
+ ...(control.reason ? { reason: control.reason } : {}),
109
+ },
110
+ }
111
+ : {}),
112
+ };
113
+ }
114
+ export function sanitizeAwaitingUserProgressText(text) {
115
+ const match = matchControlLine(text, true);
116
+ if (match.kind !== 'none') {
117
+ return stripTrailingCarriageReturn(match.analyzedText.slice(0, match.removalStart));
118
+ }
119
+ const rawMatch = matchControlLine(text, false);
120
+ if (rawMatch.kind === 'none') {
121
+ return text;
122
+ }
123
+ return stripTrailingCarriageReturn(rawMatch.analyzedText.slice(0, rawMatch.removalStart));
124
+ }
125
+ export class AwaitingUserProgressSanitizer {
126
+ onProgress;
127
+ lastPublished = null;
128
+ constructor(onProgress) {
129
+ this.onProgress = onProgress;
130
+ }
131
+ publish(text) {
132
+ if (!this.onProgress) {
133
+ return;
134
+ }
135
+ const sanitizedText = sanitizeAwaitingUserProgressText(text);
136
+ if (!hasVisibleText(sanitizedText) || sanitizedText === this.lastPublished) {
137
+ return;
138
+ }
139
+ this.lastPublished = sanitizedText;
140
+ this.onProgress({ text: sanitizedText });
141
+ }
142
+ }
143
+ export function createAwaitingUserProgressHandler(onProgress) {
144
+ if (!onProgress) {
145
+ return undefined;
146
+ }
147
+ const sanitizer = new AwaitingUserProgressSanitizer(onProgress);
148
+ return (update) => {
149
+ sanitizer.publish(update.text);
150
+ };
151
+ }
@@ -1,9 +1,11 @@
1
1
  import type { ProviderAdapter } from '../provider-adapter.js';
2
2
  import type { ProviderGenerateOptions, ProviderInput, ProviderReply } from '../../types.js';
3
+ import { ProviderTurnPreparer } from '../../context/turn-preparation.js';
3
4
  import { ClaudeCliClient } from './cli-client.js';
4
5
  export declare class ClaudeProviderAdapter implements ProviderAdapter {
5
6
  private readonly cli;
6
- constructor(cli: ClaudeCliClient);
7
+ private readonly turnPreparer;
8
+ constructor(cli: ClaudeCliClient, turnPreparer: ProviderTurnPreparer);
7
9
  generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
8
10
  dispose(): Promise<void>;
9
11
  }
@@ -1,21 +1,17 @@
1
- import { buildPrompt } from '../../context/prompt.js';
1
+ import { createAwaitingUserProgressHandler, parseProviderReply } from '../awaiting-user.js';
2
2
  export class ClaudeProviderAdapter {
3
3
  cli;
4
- constructor(cli) {
4
+ turnPreparer;
5
+ constructor(cli, turnPreparer) {
5
6
  this.cli = cli;
7
+ this.turnPreparer = turnPreparer;
6
8
  }
7
9
  async generateReply(input, options) {
8
- const prompt = buildPrompt({
9
- agentName: input.agentName,
10
- provider: input.provider,
11
- channelId: input.channelId,
12
- incomingAuthorId: input.incomingAuthorId,
13
- incomingContent: input.incomingContent,
10
+ const preparedTurn = await this.turnPreparer.prepare(input);
11
+ const text = await this.cli.generateReply(preparedTurn, {
12
+ onProgress: createAwaitingUserProgressHandler(options?.onProgress),
14
13
  });
15
- const text = await this.cli.generateReply(input.channelId, prompt, {
16
- onProgress: options?.onProgress,
17
- });
18
- return { text };
14
+ return parseProviderReply(text);
19
15
  }
20
16
  async dispose() {
21
17
  await this.cli.dispose();
@@ -1,22 +1,26 @@
1
1
  import spawn from 'cross-spawn';
2
- import type { ProviderGenerateOptions } from '../../types.js';
2
+ import { type DebugLogger } from '../../debug.js';
3
+ import type { PreparedProviderTurnInput, ProviderGenerateOptions } from '../../types.js';
3
4
  import type { ClaudeChannelSessionStore } from './session-store.js';
4
5
  interface ClaudeCliRuntime {
5
6
  spawn: typeof spawn;
7
+ cwd: string;
6
8
  }
7
9
  /**
8
10
  * CLI client for the Claude Code CLI (`claude`) non-interactive mode, with
9
- * native per-channel session continuity.
11
+ * native provider-session continuity.
10
12
  *
11
13
  * Claude session memory remains entirely inside the Claude CLI. This client
12
- * only pins one native session id per Borgee channel and serializes turns per
13
- * channel so `--resume` is never called concurrently for the same session.
14
+ * only pins one native session id per routed provider session and serializes
15
+ * turns per route so `--resume` is never called concurrently for the same
16
+ * native session.
14
17
  */
15
18
  export declare class ClaudeCliClient {
16
19
  private readonly command;
17
20
  private readonly args;
18
21
  private readonly sessionStore?;
19
22
  private readonly resolveSessionStoreAgentId;
23
+ private readonly logger;
20
24
  private readonly runtime;
21
25
  private readonly channels;
22
26
  private readonly persistedSessions;
@@ -24,7 +28,8 @@ export declare class ClaudeCliClient {
24
28
  private stopped;
25
29
  private sessionStoreLoadPromise;
26
30
  private sessionStoreWriteQueue;
27
- constructor(command: string, args: string[], runtimeOverrides?: Partial<ClaudeCliRuntime>, sessionStore?: ClaudeChannelSessionStore | undefined, resolveSessionStoreAgentId?: () => string | undefined);
31
+ constructor(command: string, args: string[], runtimeOverrides?: Partial<ClaudeCliRuntime>, sessionStore?: ClaudeChannelSessionStore | undefined, resolveSessionStoreAgentId?: () => string | undefined, logger?: DebugLogger);
32
+ generateReply(turn: PreparedProviderTurnInput, options?: ProviderGenerateOptions): Promise<string>;
28
33
  generateReply(channelId: string, prompt: string, options?: ProviderGenerateOptions): Promise<string>;
29
34
  dispose(): Promise<void>;
30
35
  private getOrCreateChannelState;
@@ -37,6 +42,8 @@ export declare class ClaudeCliClient {
37
42
  private persistSession;
38
43
  private persistSessionBestEffort;
39
44
  private resetPersistedSession;
45
+ private resolveTurnCwd;
46
+ private resetSessionIfCwdChanged;
40
47
  private resetPersistedSessionBestEffort;
41
48
  private enqueueSessionStoreWrite;
42
49
  private run;
@@ -1,17 +1,75 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { resolve } from 'node:path';
2
3
  import spawn from 'cross-spawn';
4
+ import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
3
5
  const DEFAULT_RUNTIME = {
4
6
  spawn,
7
+ cwd: process.cwd(),
5
8
  };
6
9
  const STREAM_JSON_ARGS = ['--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
10
+ class ClaudeCliProcessError extends Error {
11
+ exitCode;
12
+ signal;
13
+ stderrBytes;
14
+ stderrLineCount;
15
+ staleResume;
16
+ constructor(exitCode, signal, stderrBytes, stderrLineCount, staleResume = false) {
17
+ super(formatClaudeCliFailureMessage(exitCode, signal, stderrBytes, stderrLineCount));
18
+ this.name = 'ClaudeCliProcessError';
19
+ if (exitCode !== null) {
20
+ this.exitCode = exitCode;
21
+ }
22
+ if (signal !== null) {
23
+ this.signal = signal;
24
+ }
25
+ this.stderrBytes = stderrBytes;
26
+ this.stderrLineCount = stderrLineCount;
27
+ this.staleResume = staleResume;
28
+ }
29
+ }
30
+ function formatClaudeCliFailureMessage(exitCode, signal, stderrBytes, stderrLineCount) {
31
+ const processOutcome = signal ? `signal ${signal}` : `code ${String(exitCode ?? 'unknown')}`;
32
+ const lineLabel = stderrLineCount === 1 ? 'line' : 'lines';
33
+ return `Claude CLI failed with ${processOutcome} (stderr: ${stderrBytes} bytes across ${stderrLineCount} ${lineLabel})`;
34
+ }
35
+ function matchesStaleResumeText(text) {
36
+ const normalized = text.toLowerCase();
37
+ return normalized.includes('session not found')
38
+ || normalized.includes('cannot resume')
39
+ || normalized.includes('no conversation found with session id');
40
+ }
7
41
  function isStaleResumeFailure(error) {
42
+ if (error instanceof ClaudeCliProcessError) {
43
+ return error.staleResume;
44
+ }
8
45
  const message = normalizeError(error).message.toLowerCase();
9
- return message.includes('session not found') || message.includes('cannot resume');
46
+ return matchesStaleResumeText(message);
10
47
  }
11
48
  function normalizeError(error) {
12
49
  return error instanceof Error ? error : new Error(String(error));
13
50
  }
14
- function createDeferredTurn(prompt, options) {
51
+ function parsePersistedSessionRecord(rawValue) {
52
+ const trimmed = rawValue.trim();
53
+ if (trimmed.startsWith('{')) {
54
+ const parsed = JSON.parse(trimmed);
55
+ if (typeof parsed.sessionId === 'string' && parsed.sessionId.trim().length > 0) {
56
+ return {
57
+ sessionId: parsed.sessionId,
58
+ cwd: typeof parsed.cwd === 'string' && parsed.cwd.trim().length > 0
59
+ ? parsed.cwd
60
+ : undefined,
61
+ };
62
+ }
63
+ }
64
+ return { sessionId: rawValue };
65
+ }
66
+ function serializePersistedSessionRecord(record) {
67
+ return JSON.stringify({
68
+ sessionId: record.sessionId,
69
+ ...(record.cwd ? { cwd: record.cwd } : {}),
70
+ });
71
+ }
72
+ function createDeferredTurn(channelId, prompt, sessionPersistence, promptContext, options) {
15
73
  let settled = false;
16
74
  let resolvePromise;
17
75
  let rejectPromise;
@@ -20,7 +78,10 @@ function createDeferredTurn(prompt, options) {
20
78
  rejectPromise = reject;
21
79
  });
22
80
  return {
81
+ channelId,
23
82
  prompt,
83
+ sessionPersistence,
84
+ promptContext,
24
85
  options,
25
86
  promise,
26
87
  resolve(value) {
@@ -46,6 +107,17 @@ function asString(value) {
46
107
  function hasVisibleText(value) {
47
108
  return typeof value === 'string' && value.trim().length > 0;
48
109
  }
110
+ function isRelativePathLike(value) {
111
+ return value.startsWith('./') || value.startsWith('../');
112
+ }
113
+ function resolveClaudeLaunch(command, args, baseCwd) {
114
+ const resolvedCommand = isRelativePathLike(command) ? resolve(baseCwd, command) : command;
115
+ const resolvedArgs = args.map((arg) => (isRelativePathLike(arg) ? resolve(baseCwd, arg) : arg));
116
+ return {
117
+ command: resolvedCommand,
118
+ args: resolvedArgs,
119
+ };
120
+ }
49
121
  function readTextBlocks(blocks) {
50
122
  if (!Array.isArray(blocks)) {
51
123
  return [];
@@ -150,19 +222,28 @@ class ClaudeStreamCollector {
150
222
  this.onProgress({ text });
151
223
  }
152
224
  }
225
+ function resolveSessionRouting(turn) {
226
+ return {
227
+ channelId: turn.channelId,
228
+ key: turn.providerSessionRouting?.key ?? turn.channelId,
229
+ persistence: turn.providerSessionRouting?.persistence ?? 'persistent',
230
+ };
231
+ }
153
232
  /**
154
233
  * CLI client for the Claude Code CLI (`claude`) non-interactive mode, with
155
- * native per-channel session continuity.
234
+ * native provider-session continuity.
156
235
  *
157
236
  * Claude session memory remains entirely inside the Claude CLI. This client
158
- * only pins one native session id per Borgee channel and serializes turns per
159
- * channel so `--resume` is never called concurrently for the same session.
237
+ * only pins one native session id per routed provider session and serializes
238
+ * turns per route so `--resume` is never called concurrently for the same
239
+ * native session.
160
240
  */
161
241
  export class ClaudeCliClient {
162
242
  command;
163
243
  args;
164
244
  sessionStore;
165
245
  resolveSessionStoreAgentId;
246
+ logger;
166
247
  runtime;
167
248
  channels = new Map();
168
249
  persistedSessions = new Map();
@@ -170,22 +251,33 @@ export class ClaudeCliClient {
170
251
  stopped = false;
171
252
  sessionStoreLoadPromise = null;
172
253
  sessionStoreWriteQueue = Promise.resolve();
173
- constructor(command, args, runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined) {
254
+ constructor(command, args, runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger()) {
174
255
  this.command = command;
175
256
  this.args = args;
176
257
  this.sessionStore = sessionStore;
177
258
  this.resolveSessionStoreAgentId = resolveSessionStoreAgentId;
259
+ this.logger = logger;
178
260
  this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
179
261
  }
180
- async generateReply(channelId, prompt, options) {
262
+ async generateReply(channelIdOrTurn, promptOrOptions, maybeOptions) {
181
263
  if (this.stopped) {
182
264
  throw new Error('Claude CLI backend stopped');
183
265
  }
184
- const state = this.getOrCreateChannelState(channelId);
185
- const turn = createDeferredTurn(prompt, options);
186
- state.queue.push(turn);
187
- this.processChannelQueue(channelId, state);
188
- return turn.promise;
266
+ const preparedTurn = typeof channelIdOrTurn === 'string'
267
+ ? {
268
+ channelId: channelIdOrTurn,
269
+ prompt: typeof promptOrOptions === 'string' ? promptOrOptions : '',
270
+ }
271
+ : channelIdOrTurn;
272
+ const options = typeof channelIdOrTurn === 'string'
273
+ ? maybeOptions
274
+ : promptOrOptions;
275
+ const sessionRouting = resolveSessionRouting(preparedTurn);
276
+ const state = this.getOrCreateChannelState(sessionRouting.key, sessionRouting.persistence);
277
+ const pendingTurn = createDeferredTurn(preparedTurn.channelId, preparedTurn.prompt, sessionRouting.persistence, preparedTurn.promptContext, options);
278
+ state.queue.push(pendingTurn);
279
+ this.processChannelQueue(sessionRouting.key, state);
280
+ return pendingTurn.promise;
189
281
  }
190
282
  async dispose() {
191
283
  if (this.stopped)
@@ -200,11 +292,14 @@ export class ClaudeCliClient {
200
292
  state.queue = [];
201
293
  state.activeChild?.kill('SIGTERM');
202
294
  }
295
+ await this.sessionStoreWriteQueue;
296
+ await this.sessionStore?.close?.();
203
297
  }
204
- getOrCreateChannelState(channelId) {
298
+ getOrCreateChannelState(channelId, sessionPersistence) {
205
299
  let state = this.channels.get(channelId);
206
300
  if (!state) {
207
301
  state = {
302
+ sessionPersistence,
208
303
  processing: false,
209
304
  queue: [],
210
305
  sessionHydrated: false,
@@ -212,6 +307,9 @@ export class ClaudeCliClient {
212
307
  };
213
308
  this.channels.set(channelId, state);
214
309
  }
310
+ else if (state.sessionPersistence !== sessionPersistence) {
311
+ throw new Error(`Claude session routing persistence changed for key "${channelId}"`);
312
+ }
215
313
  return state;
216
314
  }
217
315
  processChannelQueue(channelId, state) {
@@ -227,7 +325,7 @@ export class ClaudeCliClient {
227
325
  return;
228
326
  }
229
327
  try {
230
- if (await this.ensureSessionStoreLoaded()) {
328
+ if (state.sessionPersistence === 'persistent' && await this.ensureSessionStoreLoaded()) {
231
329
  this.hydrateChannelState(channelId, state);
232
330
  }
233
331
  }
@@ -254,6 +352,13 @@ export class ClaudeCliClient {
254
352
  }
255
353
  finally {
256
354
  state.processing = false;
355
+ if (state.sessionPersistence === 'ephemeral' && !this.stopped && state.queue.length === 0) {
356
+ state.sessionEstablished = false;
357
+ state.sessionHydrated = false;
358
+ state.sessionId = undefined;
359
+ state.sessionCwd = undefined;
360
+ this.channels.delete(channelId);
361
+ }
257
362
  if (state.queue.length > 0 && !this.stopped) {
258
363
  this.processChannelQueue(channelId, state);
259
364
  }
@@ -261,6 +366,7 @@ export class ClaudeCliClient {
261
366
  })();
262
367
  }
263
368
  async runTurn(channelId, state, turn) {
369
+ await this.resetSessionIfCwdChanged(channelId, state, turn);
264
370
  return this.runTurnAttempt(channelId, state, turn, true);
265
371
  }
266
372
  async runTurnAttempt(channelId, state, turn, allowFreshRetryAfterStaleResume) {
@@ -270,15 +376,24 @@ export class ClaudeCliClient {
270
376
  const sessionId = state.sessionEstablished ? state.sessionId : randomUUID();
271
377
  const sessionArgs = state.sessionEstablished ? ['--resume', sessionId] : ['--session-id', sessionId];
272
378
  try {
273
- const text = await this.run(state, [...this.args, ...sessionArgs, ...STREAM_JSON_ARGS], turn.prompt, turn.options);
379
+ const text = await this.run(state, [...this.args, ...sessionArgs, ...STREAM_JSON_ARGS], turn.prompt, turn.promptContext, turn.options);
274
380
  state.sessionId = sessionId;
275
381
  state.sessionEstablished = true;
276
- await this.persistSessionBestEffort(channelId, sessionId);
382
+ state.sessionCwd = this.resolveTurnCwd(turn);
383
+ if (turn.sessionPersistence === 'persistent') {
384
+ await this.persistSessionBestEffort(channelId, sessionId, state.sessionCwd);
385
+ }
277
386
  return text;
278
387
  }
279
388
  catch (error) {
280
389
  if (state.sessionEstablished && isStaleResumeFailure(error)) {
281
- await this.resetPersistedSessionBestEffort(channelId, state);
390
+ if (turn.sessionPersistence === 'persistent') {
391
+ await this.resetPersistedSessionBestEffort(channelId, state);
392
+ }
393
+ else {
394
+ state.sessionEstablished = false;
395
+ state.sessionId = undefined;
396
+ }
282
397
  if (allowFreshRetryAfterStaleResume) {
283
398
  return this.runTurnAttempt(channelId, state, turn, false);
284
399
  }
@@ -311,8 +426,8 @@ export class ClaudeCliClient {
311
426
  try {
312
427
  const stored = await this.sessionStore.load(agentId);
313
428
  this.persistedSessions.clear();
314
- for (const [channelId, sessionId] of Object.entries(stored)) {
315
- this.persistedSessions.set(channelId, sessionId);
429
+ for (const [channelId, sessionValue] of Object.entries(stored)) {
430
+ this.persistedSessions.set(channelId, parsePersistedSessionRecord(sessionValue));
316
431
  }
317
432
  this.loadedSessionStoreAgentId = agentId;
318
433
  }
@@ -330,41 +445,47 @@ export class ClaudeCliClient {
330
445
  return;
331
446
  }
332
447
  state.sessionHydrated = true;
333
- const persistedSessionId = this.persistedSessions.get(channelId);
334
- if (!persistedSessionId) {
448
+ const persistedSession = this.persistedSessions.get(channelId);
449
+ if (!persistedSession) {
335
450
  return;
336
451
  }
337
- state.sessionId = persistedSessionId;
452
+ state.sessionId = persistedSession.sessionId;
453
+ state.sessionCwd = persistedSession.cwd;
338
454
  state.sessionEstablished = true;
339
455
  }
340
- async persistSession(channelId, sessionId) {
456
+ async persistSession(channelId, sessionId, cwd) {
341
457
  if (!this.sessionStore || !this.loadedSessionStoreAgentId) {
342
458
  return;
343
459
  }
344
460
  await this.enqueueSessionStoreWrite(async () => {
345
- if (this.persistedSessions.get(channelId) === sessionId) {
461
+ const current = this.persistedSessions.get(channelId);
462
+ if (current?.sessionId === sessionId && current.cwd === cwd) {
346
463
  return;
347
464
  }
348
- this.persistedSessions.set(channelId, sessionId);
349
- await this.sessionStore.save(this.loadedSessionStoreAgentId, Object.fromEntries(this.persistedSessions));
465
+ this.persistedSessions.set(channelId, { sessionId, cwd });
466
+ await this.sessionStore.save(this.loadedSessionStoreAgentId, Object.fromEntries([...this.persistedSessions.entries()].map(([persistedChannelId, record]) => [
467
+ persistedChannelId,
468
+ serializePersistedSessionRecord(record),
469
+ ])));
350
470
  });
351
471
  }
352
- async persistSessionBestEffort(channelId, sessionId) {
472
+ async persistSessionBestEffort(channelId, sessionId, cwd) {
353
473
  try {
354
- await this.persistSession(channelId, sessionId);
474
+ await this.persistSession(channelId, sessionId, cwd);
355
475
  }
356
476
  catch (error) {
357
- console.error('[agents-host] failed to persist Claude session map; keeping reply delivery', {
477
+ this.logger.error('failed to persist Claude session map; keeping reply delivery', {
358
478
  agentId: this.loadedSessionStoreAgentId,
359
479
  channelId,
360
480
  sessionId,
361
- error,
481
+ error: summarizeError(error),
362
482
  });
363
483
  }
364
484
  }
365
485
  async resetPersistedSession(channelId, state) {
366
486
  state.sessionEstablished = false;
367
487
  state.sessionId = undefined;
488
+ state.sessionCwd = undefined;
368
489
  if (!this.sessionStore || !this.loadedSessionStoreAgentId) {
369
490
  return;
370
491
  }
@@ -373,18 +494,40 @@ export class ClaudeCliClient {
373
494
  return;
374
495
  }
375
496
  this.persistedSessions.delete(channelId);
376
- await this.sessionStore.save(this.loadedSessionStoreAgentId, Object.fromEntries(this.persistedSessions));
497
+ await this.sessionStore.save(this.loadedSessionStoreAgentId, Object.fromEntries([...this.persistedSessions.entries()].map(([persistedChannelId, record]) => [
498
+ persistedChannelId,
499
+ serializePersistedSessionRecord(record),
500
+ ])));
377
501
  });
378
502
  }
503
+ resolveTurnCwd(turn) {
504
+ return turn.promptContext?.taskWorkspace?.rootPath ?? this.runtime.cwd;
505
+ }
506
+ async resetSessionIfCwdChanged(channelId, state, turn) {
507
+ if (!state.sessionEstablished) {
508
+ return;
509
+ }
510
+ const nextCwd = this.resolveTurnCwd(turn);
511
+ if (state.sessionCwd === nextCwd) {
512
+ return;
513
+ }
514
+ if (turn.sessionPersistence === 'persistent') {
515
+ await this.resetPersistedSessionBestEffort(channelId, state);
516
+ return;
517
+ }
518
+ state.sessionEstablished = false;
519
+ state.sessionId = undefined;
520
+ state.sessionCwd = undefined;
521
+ }
379
522
  async resetPersistedSessionBestEffort(channelId, state) {
380
523
  try {
381
524
  await this.resetPersistedSession(channelId, state);
382
525
  }
383
526
  catch (error) {
384
- console.error('[agents-host] failed to clear stale Claude session map; retrying in-memory only', {
527
+ this.logger.error('failed to clear stale Claude session map; retrying in-memory only', {
385
528
  agentId: this.loadedSessionStoreAgentId,
386
529
  channelId,
387
- error,
530
+ error: summarizeError(error),
388
531
  });
389
532
  }
390
533
  }
@@ -393,10 +536,12 @@ export class ClaudeCliClient {
393
536
  this.sessionStoreWriteQueue = queuedWrite.catch(() => undefined);
394
537
  await queuedWrite;
395
538
  }
396
- async run(state, args, prompt, options) {
539
+ async run(state, args, prompt, promptContext, options) {
397
540
  return new Promise((resolve, reject) => {
398
- const child = this.runtime.spawn(this.command, args, {
541
+ const launch = resolveClaudeLaunch(this.command, args, this.runtime.cwd);
542
+ const child = this.runtime.spawn(launch.command, launch.args, {
399
543
  stdio: ['pipe', 'pipe', 'pipe'],
544
+ cwd: promptContext?.taskWorkspace?.rootPath ?? this.runtime.cwd,
400
545
  });
401
546
  state.activeChild = child;
402
547
  const collector = new ClaudeStreamCollector(options?.onProgress);
@@ -441,11 +586,12 @@ export class ClaudeCliClient {
441
586
  });
442
587
  child.stderr.on('data', (chunk) => {
443
588
  stderr += chunk;
589
+ this.logger?.childStderr('claude stderr', summarizeChildStderr(chunk));
444
590
  });
445
591
  child.once('error', (error) => {
446
592
  settleReject(error);
447
593
  });
448
- child.once('close', (code) => {
594
+ child.once('close', (code, signal) => {
449
595
  if (state.activeChild === child) {
450
596
  state.activeChild = undefined;
451
597
  }
@@ -460,7 +606,8 @@ export class ClaudeCliClient {
460
606
  }
461
607
  }
462
608
  if (code !== 0) {
463
- settleReject(new Error(`Claude CLI failed with code ${code}: ${stderr.trim()}`));
609
+ const stderrSummary = summarizeChildStderr(stderr);
610
+ settleReject(new ClaudeCliProcessError(code, signal, stderrSummary.bytes, stderrSummary.lineCount, matchesStaleResumeText(stderr)));
464
611
  return;
465
612
  }
466
613
  settleResolve(collector.getFinalText());
@@ -1,6 +1,7 @@
1
1
  export interface ClaudeChannelSessionStore {
2
2
  load(agentId: string): Promise<Record<string, string>>;
3
3
  save(agentId: string, sessions: Record<string, string>): Promise<void>;
4
+ close?(): Promise<void> | void;
4
5
  }
5
6
  export interface FileClaudeChannelSessionStoreOptions {
6
7
  resolvePath(agentId: string): string;
@@ -0,0 +1,11 @@
1
+ import type { ProviderAdapter } from '../provider-adapter.js';
2
+ import type { ProviderGenerateOptions, ProviderInput, ProviderReply } from '../../types.js';
3
+ import { ProviderTurnPreparer } from '../../context/turn-preparation.js';
4
+ import { CodexCliClient } from './cli-client.js';
5
+ export declare class CodexProviderAdapter implements ProviderAdapter {
6
+ private readonly cli;
7
+ private readonly turnPreparer;
8
+ constructor(cli: CodexCliClient, turnPreparer: ProviderTurnPreparer);
9
+ generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
10
+ dispose(): Promise<void>;
11
+ }