@borgee/agents-host 0.2.1 → 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.
- package/README.md +184 -21
- package/dist/agents-host-supervisor.d.ts +7 -5
- package/dist/agents-host-supervisor.js +24 -4
- package/dist/agents-host.d.ts +89 -15
- package/dist/agents-host.js +2099 -142
- package/dist/chat/chat-control-plane.d.ts +14 -3
- package/dist/chat/sdk-chat-control-plane.d.ts +16 -4
- package/dist/chat/sdk-chat-control-plane.js +69 -9
- package/dist/cli-args.d.ts +46 -5
- package/dist/cli-args.js +313 -32
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +112 -5
- package/dist/compatibility-gates.d.ts +35 -0
- package/dist/compatibility-gates.js +127 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +23 -5
- package/dist/connections-state-store.d.ts +81 -0
- package/dist/connections-state-store.js +228 -0
- package/dist/context/injection.d.ts +109 -0
- package/dist/context/injection.js +350 -0
- package/dist/context/prompt.d.ts +4 -1
- package/dist/context/prompt.js +170 -1
- package/dist/context/turn-preparation.d.ts +9 -0
- package/dist/context/turn-preparation.js +106 -0
- package/dist/debug.d.ts +44 -0
- package/dist/debug.js +135 -0
- package/dist/gateway/localhost-gateway.d.ts +52 -0
- package/dist/gateway/localhost-gateway.js +857 -0
- package/dist/index.js +7 -5
- package/dist/local-config.d.ts +4 -1
- package/dist/local-config.js +24 -7
- package/dist/managed-daemon-log.d.ts +34 -0
- package/dist/managed-daemon-log.js +261 -0
- package/dist/managed-daemon.d.ts +220 -0
- package/dist/managed-daemon.js +1601 -0
- package/dist/policy/authorization-audit.d.ts +63 -0
- package/dist/policy/authorization-audit.js +94 -0
- package/dist/policy/copilot-permission.d.ts +15 -0
- package/dist/policy/copilot-permission.js +193 -0
- package/dist/policy/gateway-authorization.d.ts +42 -0
- package/dist/policy/gateway-authorization.js +162 -0
- package/dist/providers/awaiting-user.d.ts +12 -0
- package/dist/providers/awaiting-user.js +151 -0
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +8 -12
- package/dist/providers/claude/cli-client.d.ts +12 -5
- package/dist/providers/claude/cli-client.js +184 -37
- package/dist/providers/claude/session-store.d.ts +24 -0
- package/dist/providers/claude/session-store.js +65 -12
- package/dist/providers/codex/adapter.d.ts +11 -0
- package/dist/providers/codex/adapter.js +19 -0
- package/dist/providers/codex/cli-client.d.ts +103 -0
- package/dist/providers/codex/cli-client.js +1133 -0
- package/dist/providers/codex/project-doc.d.ts +3 -0
- package/dist/providers/codex/project-doc.js +66 -0
- package/dist/providers/codex/session-store.d.ts +38 -0
- package/dist/providers/codex/session-store.js +150 -0
- package/dist/providers/copilot/adapter.d.ts +3 -1
- package/dist/providers/copilot/adapter.js +8 -12
- package/dist/providers/copilot/cli-client.d.ts +20 -2
- package/dist/providers/copilot/cli-client.js +251 -71
- package/dist/providers/copilot/session-store.d.ts +24 -0
- package/dist/providers/copilot/session-store.js +65 -12
- package/dist/providers/create-provider.d.ts +11 -2
- package/dist/providers/create-provider.js +131 -12
- package/dist/run.d.ts +1 -0
- package/dist/run.js +5 -2
- package/dist/state-paths.d.ts +13 -1
- package/dist/state-paths.js +84 -3
- package/dist/task-thread-resolution.d.ts +10 -0
- package/dist/task-thread-resolution.js +48 -0
- package/dist/types.d.ts +174 -1
- package/dist/visible-mentions.d.ts +3 -0
- package/dist/visible-mentions.js +15 -0
- package/package.json +19 -17
- package/skills/borgee-agent/SKILL.md +33 -0
- package/skills/borgee-agent/borgee-agent.mjs +473 -0
- 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
|
-
|
|
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 {
|
|
1
|
+
import { createAwaitingUserProgressHandler, parseProviderReply } from '../awaiting-user.js';
|
|
2
2
|
export class ClaudeProviderAdapter {
|
|
3
3
|
cli;
|
|
4
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
13
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
159
|
-
*
|
|
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(
|
|
262
|
+
async generateReply(channelIdOrTurn, promptOrOptions, maybeOptions) {
|
|
181
263
|
if (this.stopped) {
|
|
182
264
|
throw new Error('Claude CLI backend stopped');
|
|
183
265
|
}
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
315
|
-
this.persistedSessions.set(channelId,
|
|
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
|
|
334
|
-
if (!
|
|
448
|
+
const persistedSession = this.persistedSessions.get(channelId);
|
|
449
|
+
if (!persistedSession) {
|
|
335
450
|
return;
|
|
336
451
|
}
|
|
337
|
-
state.sessionId =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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());
|