@borgee/agents-host 0.2.74 → 0.2.94
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 +11 -24
- package/dist/agents-host.d.ts +6 -4
- package/dist/agents-host.js +127 -16
- package/dist/background-runs.d.ts +24 -0
- package/dist/background-runs.js +184 -0
- package/dist/chat/chat-control-plane.d.ts +7 -1
- package/dist/chat/sdk-chat-control-plane.d.ts +7 -2
- package/dist/chat/sdk-chat-control-plane.js +22 -0
- package/dist/context/claude-file-brief.js +3 -3
- package/dist/context/injection.d.ts +18 -12
- package/dist/context/injection.js +53 -53
- package/dist/context/prompt.js +10 -10
- package/dist/context/resolved-working-folder.d.ts +3 -0
- package/dist/context/resolved-working-folder.js +106 -0
- package/dist/context/turn-preparation.js +1 -1
- package/dist/managed-daemon.js +30 -30
- package/dist/plugin-sdk.js +315 -29
- package/dist/plugin-sdk.js.map +3 -3
- package/dist/policy/authorization-audit.d.ts +1 -1
- package/dist/policy/copilot-permission.d.ts +9 -0
- package/dist/policy/copilot-permission.js +120 -1
- package/dist/progress-to-activity.d.ts +1 -1
- package/dist/progress-to-activity.js +1 -0
- package/dist/providers/claude/adapter.d.ts +2 -1
- package/dist/providers/claude/adapter.js +36 -1
- package/dist/providers/claude/background-run-observer.d.ts +32 -0
- package/dist/providers/claude/background-run-observer.js +381 -0
- package/dist/providers/claude/cli-client.d.ts +12 -0
- package/dist/providers/claude/cli-client.js +453 -72
- package/dist/providers/claude/foreground-handoff.d.ts +4 -0
- package/dist/providers/claude/foreground-handoff.js +45 -0
- package/dist/providers/claude/task-cancellation-protocol.d.ts +14 -0
- package/dist/providers/claude/task-cancellation-protocol.js +21 -0
- package/dist/providers/codex/adapter.d.ts +1 -1
- package/dist/providers/codex/adapter.js +7 -0
- package/dist/providers/codex/cli-client.js +8 -7
- package/dist/providers/codex/project-doc.js +6 -6
- package/dist/providers/copilot/activity-metadata.d.ts +3 -0
- package/dist/providers/copilot/activity-metadata.js +19 -0
- package/dist/providers/copilot/adapter.d.ts +6 -2
- package/dist/providers/copilot/adapter.js +27 -1
- package/dist/providers/copilot/cli-client.d.ts +37 -10
- package/dist/providers/copilot/cli-client.js +756 -122
- package/dist/providers/copilot/sdk-session.d.ts +149 -0
- package/dist/providers/copilot/sdk-session.js +981 -0
- package/dist/providers/create-provider.js +33 -0
- package/dist/providers/provider-adapter.d.ts +24 -1
- package/dist/providers/provider-adapter.js +17 -0
- package/dist/types.d.ts +40 -11
- package/dist/vendor/claude-agent-acp/LICENSE +191 -0
- package/dist/vendor/claude-agent-acp/NOTICE +8 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.d.ts +1017 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.js +6305 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-foreground-handoff-bridge.js +322 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-task-cancellation-bridge.js +101 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-task-lifecycle-bridge.js +58 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.d.ts +130 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.js +304 -0
- package/dist/vendor/claude-agent-acp/dist/index.d.ts +3 -0
- package/dist/vendor/claude-agent-acp/dist/index.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/index.js +75 -0
- package/dist/vendor/claude-agent-acp/dist/lib.d.ts +6 -0
- package/dist/vendor/claude-agent-acp/dist/lib.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/lib.js +5 -0
- package/dist/vendor/claude-agent-acp/dist/settings.d.ts +68 -0
- package/dist/vendor/claude-agent-acp/dist/settings.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/settings.js +185 -0
- package/dist/vendor/claude-agent-acp/dist/tools.d.ts +102 -0
- package/dist/vendor/claude-agent-acp/dist/tools.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/tools.js +1000 -0
- package/dist/vendor/claude-agent-acp/dist/utils.d.ts +16 -0
- package/dist/vendor/claude-agent-acp/dist/utils.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/utils.js +81 -0
- package/dist/vendor/claude-agent-acp/package.json +85 -0
- package/package.json +15 -10
- package/skills/borgee-agent/references/task-properties.md +3 -3
- package/skills/borgee-agent/scripts/borgee-agent.mjs +1 -1
- package/skills/borgee-agent/scripts/borgee-agent.py +1 -1
- package/dist/context/resolved-workspace.d.ts +0 -3
- package/dist/context/resolved-workspace.js +0 -106
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Readable, Writable } from 'node:stream';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { createRequire } from 'node:module';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
4
|
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import spawn from 'cross-spawn';
|
|
7
7
|
import { DEFAULT_PROJECTION_STRATEGY, normalizeProjectionStrategy, } from '../../projection-strategy-values.js';
|
|
8
8
|
import { assertClaudeCommandCompatibility, DEFAULT_CLAUDE_COMMAND, isLegacyClaudeCompatibilityAlias, } from '../../config.js';
|
|
@@ -11,10 +11,143 @@ import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js
|
|
|
11
11
|
import { AcpProgressCollector } from '../acp-progress-collector.js';
|
|
12
12
|
import { ProviderTurnCancelledError } from '../provider-adapter.js';
|
|
13
13
|
import { readClaudeActivityMetadata } from './activity-metadata.js';
|
|
14
|
+
import { ClaudeBackgroundRunObserver } from './background-run-observer.js';
|
|
15
|
+
import { CLAUDE_FOREGROUND_HANDOFF_METHOD, isClaudeUpdateFromIsolatedTurn, readClaudeForegroundSettledTurn, } from './foreground-handoff.js';
|
|
14
16
|
import { resolveCopilotPermissionResponse } from '../../policy/copilot-permission.js';
|
|
15
17
|
import { buildClaudeSessionPromptAppend } from '../../context/prompt.js';
|
|
16
|
-
import {
|
|
18
|
+
import { isDiscussionOnlyResolvedWorkingFolder } from '../../context/resolved-working-folder.js';
|
|
17
19
|
import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
|
|
20
|
+
import { CLAUDE_TASK_CANCELLATION_METHOD, supportsClaudeTaskCancellation, } from './task-cancellation-protocol.js';
|
|
21
|
+
class ClaudeSessionUpdatePump {
|
|
22
|
+
session;
|
|
23
|
+
onFailure;
|
|
24
|
+
onForegroundSettledWithBackground;
|
|
25
|
+
observer;
|
|
26
|
+
activeTurn;
|
|
27
|
+
closed = false;
|
|
28
|
+
failure;
|
|
29
|
+
constructor(session, onFailure, onBackgroundTasksActiveChanged, onForegroundSettledWithBackground, targetedCancellationSupported) {
|
|
30
|
+
this.session = session;
|
|
31
|
+
this.onFailure = onFailure;
|
|
32
|
+
this.onForegroundSettledWithBackground = onForegroundSettledWithBackground;
|
|
33
|
+
this.observer = new ClaudeBackgroundRunObserver(undefined, {
|
|
34
|
+
onBackgroundTasksActiveChanged,
|
|
35
|
+
targetedCancellationSupported,
|
|
36
|
+
});
|
|
37
|
+
void this.run();
|
|
38
|
+
}
|
|
39
|
+
beginTurn(onProgress, foregroundIsolationFence) {
|
|
40
|
+
if (this.closed) {
|
|
41
|
+
throw new Error('Claude ACP session update pump is closed');
|
|
42
|
+
}
|
|
43
|
+
if (this.failure) {
|
|
44
|
+
throw this.failure;
|
|
45
|
+
}
|
|
46
|
+
if (this.activeTurn) {
|
|
47
|
+
throw new Error('Claude ACP session already has an active turn');
|
|
48
|
+
}
|
|
49
|
+
const turn = {
|
|
50
|
+
queue: [],
|
|
51
|
+
foregroundIsolationFence,
|
|
52
|
+
};
|
|
53
|
+
this.activeTurn = turn;
|
|
54
|
+
this.observer.setOnProgress(onProgress);
|
|
55
|
+
return {
|
|
56
|
+
next: () => {
|
|
57
|
+
if (turn.failure) {
|
|
58
|
+
return Promise.reject(turn.failure);
|
|
59
|
+
}
|
|
60
|
+
const queued = turn.queue.shift();
|
|
61
|
+
if (queued) {
|
|
62
|
+
return Promise.resolve(queued);
|
|
63
|
+
}
|
|
64
|
+
if (this.activeTurn !== turn || this.closed) {
|
|
65
|
+
return Promise.reject(new Error('Claude ACP turn update stream is closed'));
|
|
66
|
+
}
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
turn.waiter = { resolve, reject };
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
close: () => {
|
|
72
|
+
if (this.activeTurn !== turn) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.activeTurn = undefined;
|
|
76
|
+
turn.waiter?.reject(new Error('Claude ACP turn update stream is closed'));
|
|
77
|
+
turn.waiter = undefined;
|
|
78
|
+
turn.queue.length = 0;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
close() {
|
|
83
|
+
if (this.closed) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.closed = true;
|
|
87
|
+
this.observer.dispose();
|
|
88
|
+
const turn = this.activeTurn;
|
|
89
|
+
this.activeTurn = undefined;
|
|
90
|
+
turn?.waiter?.reject(new Error('Claude ACP session update pump is closed'));
|
|
91
|
+
}
|
|
92
|
+
async run() {
|
|
93
|
+
try {
|
|
94
|
+
while (!this.closed) {
|
|
95
|
+
const update = await this.session.nextUpdate();
|
|
96
|
+
if (this.closed) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const lifecycle = update.kind === 'session_update' && this.observer.consume(update.update);
|
|
100
|
+
if (lifecycle) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (update.kind === 'session_update'
|
|
104
|
+
&& isClaudeUpdateFromIsolatedTurn(update.update, this.activeTurn?.foregroundIsolationFence)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const settledForegroundTurn = update.kind === 'session_update'
|
|
108
|
+
? readClaudeForegroundSettledTurn(update.update)
|
|
109
|
+
: undefined;
|
|
110
|
+
const turn = this.activeTurn;
|
|
111
|
+
if (turn
|
|
112
|
+
&& settledForegroundTurn !== undefined
|
|
113
|
+
&& turn.foregroundSettledTurn !== settledForegroundTurn) {
|
|
114
|
+
turn.foregroundSettledTurn = settledForegroundTurn;
|
|
115
|
+
this.onForegroundSettledWithBackground(settledForegroundTurn);
|
|
116
|
+
}
|
|
117
|
+
if (!turn) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const waiter = turn.waiter;
|
|
121
|
+
if (waiter) {
|
|
122
|
+
turn.waiter = undefined;
|
|
123
|
+
waiter.resolve(update);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
turn.queue.push(update);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (!this.closed) {
|
|
132
|
+
const normalized = normalizeError(error);
|
|
133
|
+
this.observer.fail();
|
|
134
|
+
this.failure = normalized;
|
|
135
|
+
const turn = this.activeTurn;
|
|
136
|
+
if (turn) {
|
|
137
|
+
// ActiveSession mirrors prompt failures onto its update queue. Keep
|
|
138
|
+
// that rejection scoped to this turn/session; transport failures use
|
|
139
|
+
// the connection and child-process fatal handlers instead.
|
|
140
|
+
turn.failure = normalized;
|
|
141
|
+
turn.waiter?.reject(normalized);
|
|
142
|
+
turn.waiter = undefined;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.onFailure(normalized);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
18
151
|
const SESSION_TAINTED_ERRORS = new WeakSet();
|
|
19
152
|
const IDLE_BACKEND_STOPPED_MESSAGE = 'Claude ACP backend stopped after idle timeout';
|
|
20
153
|
/**
|
|
@@ -32,7 +165,6 @@ const DEFAULT_SESSION_CAPABILITIES = {
|
|
|
32
165
|
resumeSession: false,
|
|
33
166
|
};
|
|
34
167
|
const CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV = 'CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD';
|
|
35
|
-
const require = createRequire(import.meta.url);
|
|
36
168
|
const CLAUDE_CANCELLED_REPLY_TEXT = 'Info: Operation cancelled by user';
|
|
37
169
|
const DEFAULT_RUNTIME = {
|
|
38
170
|
spawn,
|
|
@@ -283,7 +415,12 @@ function isSessionTainted(error) {
|
|
|
283
415
|
return error instanceof Error && SESSION_TAINTED_ERRORS.has(error);
|
|
284
416
|
}
|
|
285
417
|
function resolveBundledClaudeAdapterPath() {
|
|
286
|
-
|
|
418
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
419
|
+
const adapterPath = resolve(packageRoot, 'dist', 'vendor', 'claude-agent-acp', 'dist', 'index.js');
|
|
420
|
+
if (!existsSync(adapterPath)) {
|
|
421
|
+
throw new Error(`Shipped Claude ACP adapter is missing at ${adapterPath}; reinstall @borgee/agents-host`);
|
|
422
|
+
}
|
|
423
|
+
return adapterPath;
|
|
287
424
|
}
|
|
288
425
|
function isRelativePathLike(value) {
|
|
289
426
|
return value.startsWith('./') || value.startsWith('../');
|
|
@@ -489,16 +626,45 @@ export class ClaudeCliClient {
|
|
|
489
626
|
imageInputConfig;
|
|
490
627
|
runtime;
|
|
491
628
|
channels = new Map();
|
|
629
|
+
channelWorkerRoutes = new Map();
|
|
630
|
+
targetedBackgroundRunCancellationConfigured;
|
|
492
631
|
/**
|
|
493
632
|
* The provider session currently bound to channelId, or undefined before a
|
|
494
633
|
* turn has opened one. Surfaced so agents-host can record which session
|
|
495
634
|
* worked a task without the agent being asked to report its own id.
|
|
496
635
|
*/
|
|
497
636
|
sessionIdForChannel(channelId) {
|
|
498
|
-
return this.
|
|
637
|
+
return this.channelWorkerRoutes.get(channelId)?.state.session?.sessionId
|
|
638
|
+
?? this.channels.get(channelId)?.session?.sessionId;
|
|
639
|
+
}
|
|
640
|
+
async handoffForegroundTurn(channelId) {
|
|
641
|
+
const state = this.channelWorkerRoutes.get(channelId)?.state ?? this.channels.get(channelId);
|
|
642
|
+
const session = state?.session;
|
|
643
|
+
if (!state?.activeTurn || !session || !this.connection) {
|
|
644
|
+
return { outcome: 'not_found' };
|
|
645
|
+
}
|
|
646
|
+
const response = await this.connection.agent.request(CLAUDE_FOREGROUND_HANDOFF_METHOD, { sessionId: session.sessionId });
|
|
647
|
+
const result = asObject(response);
|
|
648
|
+
const outcome = result?.outcome;
|
|
649
|
+
if (outcome === 'admitted' || outcome === 'released') {
|
|
650
|
+
const foregroundTurn = result?.foregroundTurn;
|
|
651
|
+
if (!Number.isSafeInteger(foregroundTurn)
|
|
652
|
+
|| foregroundTurn <= 0) {
|
|
653
|
+
throw new Error('Claude ACP foreground handoff omitted its ownership fence');
|
|
654
|
+
}
|
|
655
|
+
if (state.session !== session) {
|
|
656
|
+
return { outcome: 'admitted' };
|
|
657
|
+
}
|
|
658
|
+
state.foregroundIsolationFence = Math.max(state.foregroundIsolationFence ?? 0, foregroundTurn);
|
|
659
|
+
return { outcome: 'admitted' };
|
|
660
|
+
}
|
|
661
|
+
if (outcome === 'not_found') {
|
|
662
|
+
return { outcome: 'not_found' };
|
|
663
|
+
}
|
|
664
|
+
throw new Error('Claude ACP returned an unusable foreground handoff result');
|
|
499
665
|
}
|
|
500
666
|
async cancelTurn(channelId) {
|
|
501
|
-
const state = this.channels.get(channelId);
|
|
667
|
+
const state = this.channelWorkerRoutes.get(channelId)?.state ?? this.channels.get(channelId);
|
|
502
668
|
const session = state?.session;
|
|
503
669
|
if (!state?.activeTurn || !session) {
|
|
504
670
|
return false;
|
|
@@ -513,8 +679,33 @@ export class ClaudeCliClient {
|
|
|
513
679
|
await this.connection?.agent.notify(cancelMethod, { sessionId: session.sessionId });
|
|
514
680
|
return true;
|
|
515
681
|
}
|
|
682
|
+
async cancelBackgroundRun(channelId, providerRunId) {
|
|
683
|
+
if (!this.targetedBackgroundRunCancellationConfigured
|
|
684
|
+
|| !this.targetedBackgroundRunCancellationAvailable
|
|
685
|
+
|| providerRunId.length === 0) {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
const state = this.channelWorkerRoutes.get(channelId)?.state ?? this.channels.get(channelId);
|
|
689
|
+
const session = state?.session;
|
|
690
|
+
if (!state || state.channelId !== channelId || !session || !this.connection) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
this.idleBackendShutdown.cancel();
|
|
694
|
+
const response = await this.connection.agent.request(CLAUDE_TASK_CANCELLATION_METHOD, {
|
|
695
|
+
sessionId: session.sessionId,
|
|
696
|
+
taskId: providerRunId,
|
|
697
|
+
});
|
|
698
|
+
if (response?.outcome === 'accepted') {
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
if (response?.outcome === 'not_found' || response?.outcome === 'unsupported') {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
throw new Error('Claude ACP returned an invalid targeted task cancellation response');
|
|
705
|
+
}
|
|
516
706
|
persistedSessions = new Map();
|
|
517
707
|
closingSessions = new WeakSet();
|
|
708
|
+
sessionUpdatePumps = new WeakMap();
|
|
518
709
|
pendingSessionStarts = new Set();
|
|
519
710
|
pendingSessionCloses = new Set();
|
|
520
711
|
pendingSessionStoreOperations = new Set();
|
|
@@ -539,6 +730,7 @@ export class ClaudeCliClient {
|
|
|
539
730
|
sessionStoreLoadPromise = null;
|
|
540
731
|
sessionStoreWriteQueue = Promise.resolve();
|
|
541
732
|
sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
|
|
733
|
+
targetedBackgroundRunCancellationAvailable = false;
|
|
542
734
|
idleBackendShutdown;
|
|
543
735
|
childStderr = '';
|
|
544
736
|
constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger(), imageInputConfig = {}) {
|
|
@@ -550,6 +742,8 @@ export class ClaudeCliClient {
|
|
|
550
742
|
this.imageInputConfig = imageInputConfig;
|
|
551
743
|
this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
|
|
552
744
|
assertClaudeCommandCompatibility(command, args, 'Claude provider runtime');
|
|
745
|
+
this.targetedBackgroundRunCancellationConfigured =
|
|
746
|
+
useBundledClaudeAdapter(command, args) || useLegacyClaudeCompatibilityAlias(command, args);
|
|
553
747
|
this.idleBackendShutdown = new IdleBackendShutdownScheduler({
|
|
554
748
|
idleShutdownMs: this.runtime.idleBackendShutdownMs,
|
|
555
749
|
isIdle: () => this.isBackendIdle(),
|
|
@@ -586,11 +780,11 @@ export class ClaudeCliClient {
|
|
|
586
780
|
? maybeOptions
|
|
587
781
|
: promptOrOptions;
|
|
588
782
|
const sessionRouting = resolveSessionRouting(preparedTurn);
|
|
589
|
-
const state = this.
|
|
783
|
+
const state = this.resolveChannelState(sessionRouting);
|
|
590
784
|
this.idleBackendShutdown.cancel();
|
|
591
785
|
const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn, sessionRouting.persistence, options);
|
|
592
786
|
state.queue.push(turn);
|
|
593
|
-
this.processChannelQueue(
|
|
787
|
+
this.processChannelQueue(state);
|
|
594
788
|
return turn.promise;
|
|
595
789
|
}
|
|
596
790
|
async dispose() {
|
|
@@ -717,9 +911,13 @@ export class ClaudeCliClient {
|
|
|
717
911
|
},
|
|
718
912
|
});
|
|
719
913
|
this.sessionCapabilities = readSessionCapabilities(initializeResponse);
|
|
914
|
+
this.targetedBackgroundRunCancellationAvailable =
|
|
915
|
+
this.targetedBackgroundRunCancellationConfigured
|
|
916
|
+
&& supportsClaudeTaskCancellation(initializeResponse);
|
|
720
917
|
this.logger?.debug('initialized Claude ACP connection', {
|
|
721
918
|
loadSession: this.sessionCapabilities.loadSession,
|
|
722
919
|
resumeSession: this.sessionCapabilities.resumeSession,
|
|
920
|
+
targetedBackgroundRunCancellation: this.targetedBackgroundRunCancellationAvailable,
|
|
723
921
|
});
|
|
724
922
|
}
|
|
725
923
|
catch (error) {
|
|
@@ -729,24 +927,45 @@ export class ClaudeCliClient {
|
|
|
729
927
|
throw normalized;
|
|
730
928
|
}
|
|
731
929
|
}
|
|
732
|
-
getOrCreateChannelState(
|
|
733
|
-
let state = this.channels.get(
|
|
930
|
+
getOrCreateChannelState(routingKey, resolvedChannelId, sessionPersistence) {
|
|
931
|
+
let state = this.channels.get(routingKey);
|
|
734
932
|
if (!state) {
|
|
735
933
|
state = {
|
|
934
|
+
routingKey,
|
|
736
935
|
channelId: resolvedChannelId,
|
|
737
936
|
sessionPersistence,
|
|
937
|
+
backgroundTasksActive: false,
|
|
938
|
+
backgroundContinuationPending: false,
|
|
939
|
+
persistentFollowUpAdopted: false,
|
|
738
940
|
processing: false,
|
|
739
941
|
queue: [],
|
|
740
942
|
};
|
|
741
|
-
this.channels.set(
|
|
943
|
+
this.channels.set(routingKey, state);
|
|
742
944
|
}
|
|
743
945
|
else if (state.channelId !== resolvedChannelId
|
|
744
946
|
|| state.sessionPersistence !== sessionPersistence) {
|
|
745
|
-
throw new Error(`Claude session routing changed for key "${
|
|
947
|
+
throw new Error(`Claude session routing changed for key "${routingKey}"`);
|
|
746
948
|
}
|
|
747
949
|
return state;
|
|
748
950
|
}
|
|
749
|
-
|
|
951
|
+
resolveChannelState(routing) {
|
|
952
|
+
const workerRoute = this.channelWorkerRoutes.get(routing.channelId);
|
|
953
|
+
if (workerRoute) {
|
|
954
|
+
return workerRoute.state;
|
|
955
|
+
}
|
|
956
|
+
if (routing.persistence === 'ephemeral') {
|
|
957
|
+
const foregroundState = this.channels.get(routing.channelId);
|
|
958
|
+
const state = foregroundState
|
|
959
|
+
&& foregroundState.channelId === routing.channelId
|
|
960
|
+
&& foregroundState.sessionPersistence === 'persistent'
|
|
961
|
+
? foregroundState
|
|
962
|
+
: this.getOrCreateChannelState(routing.key, routing.channelId, routing.persistence);
|
|
963
|
+
this.channelWorkerRoutes.set(routing.channelId, { phase: 'candidate', state });
|
|
964
|
+
return state;
|
|
965
|
+
}
|
|
966
|
+
return this.getOrCreateChannelState(routing.key, routing.channelId, routing.persistence);
|
|
967
|
+
}
|
|
968
|
+
processChannelQueue(state) {
|
|
750
969
|
if (state.processing) {
|
|
751
970
|
return;
|
|
752
971
|
}
|
|
@@ -761,22 +980,29 @@ export class ClaudeCliClient {
|
|
|
761
980
|
state.activeTurn = turn;
|
|
762
981
|
try {
|
|
763
982
|
state.cwd = this.resolveSessionCwd(turn.preparedTurn.promptContext);
|
|
764
|
-
state.toolPermissionMode =
|
|
983
|
+
state.toolPermissionMode = isDiscussionOnlyResolvedWorkingFolder(turn.preparedTurn.promptContext)
|
|
765
984
|
? 'deny-all'
|
|
766
985
|
: 'default';
|
|
767
986
|
const sessionPromptAppend = resolveTurnSessionPromptAppend(turn.preparedTurn);
|
|
768
987
|
state.visibilityKey = resolveSessionVisibilityKey(turn.preparedTurn.promptContext, sessionPromptAppend, turn.preparedTurn.projectionStrategy);
|
|
988
|
+
const adoptingPersistentRoute = turn.sessionPersistence === 'persistent'
|
|
989
|
+
&& state.routingKey !== state.channelId
|
|
990
|
+
&& !state.persistentFollowUpAdopted;
|
|
991
|
+
if (adoptingPersistentRoute) {
|
|
992
|
+
state.persistentFollowUpAdopted = true;
|
|
993
|
+
state.sessionPersistence = 'persistent';
|
|
994
|
+
}
|
|
769
995
|
await this.ensureStarted();
|
|
770
|
-
await this.recycleSessionIfScopeChanged(
|
|
771
|
-
const session = await this.getOrCreateSession(
|
|
996
|
+
await this.recycleSessionIfScopeChanged(state.routingKey, state);
|
|
997
|
+
const session = await this.getOrCreateSession(state, turn.preparedTurn, sessionPromptAppend);
|
|
772
998
|
let reply;
|
|
773
999
|
try {
|
|
774
1000
|
const promptInput = await this.buildPromptInput(turn.preparedTurn);
|
|
775
|
-
reply = await this.runTurn(session, promptInput, turn.options);
|
|
1001
|
+
reply = await this.runTurn(session, promptInput, turn.options, state.foregroundIsolationFence);
|
|
776
1002
|
}
|
|
777
1003
|
catch (error) {
|
|
778
1004
|
if (isSessionTainted(error)) {
|
|
779
|
-
this.invalidateSession(
|
|
1005
|
+
this.invalidateSession(state.routingKey, state, session);
|
|
780
1006
|
this.rejectQueuedTurnsAfterSessionTaint(state, error);
|
|
781
1007
|
}
|
|
782
1008
|
throw error;
|
|
@@ -793,26 +1019,17 @@ export class ClaudeCliClient {
|
|
|
793
1019
|
}
|
|
794
1020
|
finally {
|
|
795
1021
|
state.processing = false;
|
|
796
|
-
if (state.sessionPersistence === 'ephemeral' && !this.fatalError && !this.disposing && state.queue.length === 0) {
|
|
797
|
-
const session = state.session;
|
|
798
|
-
state.session = undefined;
|
|
799
|
-
state.sessionCwd = undefined;
|
|
800
|
-
state.sessionVisibilityKey = undefined;
|
|
801
|
-
if (session) {
|
|
802
|
-
this.closeSession(session);
|
|
803
|
-
}
|
|
804
|
-
this.channels.delete(channelId);
|
|
805
|
-
}
|
|
806
1022
|
if (state.queue.length > 0 && !this.fatalError) {
|
|
807
|
-
this.processChannelQueue(
|
|
1023
|
+
this.processChannelQueue(state);
|
|
808
1024
|
}
|
|
809
1025
|
else {
|
|
1026
|
+
this.reconcileIdleChannelState(state);
|
|
810
1027
|
this.idleBackendShutdown.reconcile();
|
|
811
1028
|
}
|
|
812
1029
|
}
|
|
813
1030
|
})();
|
|
814
1031
|
}
|
|
815
|
-
async getOrCreateSession(
|
|
1032
|
+
async getOrCreateSession(state, turn, systemPromptAppend) {
|
|
816
1033
|
if (state.session) {
|
|
817
1034
|
return state.session;
|
|
818
1035
|
}
|
|
@@ -824,12 +1041,12 @@ export class ClaudeCliClient {
|
|
|
824
1041
|
}
|
|
825
1042
|
const cwd = state.cwd ?? this.runtime.cwd;
|
|
826
1043
|
const persistedSessionId = state.sessionPersistence === 'persistent'
|
|
827
|
-
? await this.readPersistedSessionId(channelId, cwd, state.visibilityKey)
|
|
1044
|
+
? await this.readPersistedSessionId(state.channelId, cwd, state.visibilityKey)
|
|
828
1045
|
: undefined;
|
|
829
1046
|
const additionalDirectories = resolveSessionAdditionalDirectories(turn.promptContext);
|
|
830
1047
|
const sessionPromise = persistedSessionId
|
|
831
|
-
? this.restoreOrCreateSession(channelId, persistedSessionId, cwd, systemPromptAppend, additionalDirectories)
|
|
832
|
-
: this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
|
|
1048
|
+
? this.restoreOrCreateSession(state.channelId, persistedSessionId, cwd, systemPromptAppend, additionalDirectories)
|
|
1049
|
+
: this.startFreshSession(state.channelId, cwd, systemPromptAppend, additionalDirectories);
|
|
833
1050
|
this.pendingSessionStarts.add(sessionPromise);
|
|
834
1051
|
state.sessionPromise = sessionPromise;
|
|
835
1052
|
let sessionAdopted = false;
|
|
@@ -854,10 +1071,16 @@ export class ClaudeCliClient {
|
|
|
854
1071
|
state.session = session;
|
|
855
1072
|
state.sessionCwd = cwd;
|
|
856
1073
|
state.sessionVisibilityKey = state.visibilityKey;
|
|
1074
|
+
if (state.sessionPersistence === 'persistent') {
|
|
1075
|
+
await this.persistSessionBestEffort(state.channelId, session.sessionId, state.visibilityKey, cwd);
|
|
1076
|
+
}
|
|
857
1077
|
return session;
|
|
858
1078
|
}
|
|
859
1079
|
catch (error) {
|
|
860
1080
|
state.session = undefined;
|
|
1081
|
+
state.backgroundTasksActive = false;
|
|
1082
|
+
state.backgroundContinuationPending = false;
|
|
1083
|
+
state.foregroundIsolationFence = undefined;
|
|
861
1084
|
state.sessionCwd = undefined;
|
|
862
1085
|
state.sessionVisibilityKey = undefined;
|
|
863
1086
|
throw error;
|
|
@@ -874,10 +1097,6 @@ export class ClaudeCliClient {
|
|
|
874
1097
|
}
|
|
875
1098
|
const session = await this.connection.agent.buildSession(buildSessionRequest(cwd, systemPromptAppend, additionalDirectories)).start();
|
|
876
1099
|
this.logger?.debug('started fresh Claude ACP session', { channelId, cwd });
|
|
877
|
-
const state = this.channels.get(channelId);
|
|
878
|
-
if (state?.sessionPersistence === 'persistent') {
|
|
879
|
-
await this.persistSessionBestEffort(channelId, session.sessionId, state.visibilityKey, cwd);
|
|
880
|
-
}
|
|
881
1100
|
return session;
|
|
882
1101
|
}
|
|
883
1102
|
async restoreOrCreateSession(channelId, sessionId, cwd, systemPromptAppend, additionalDirectories) {
|
|
@@ -939,7 +1158,7 @@ export class ClaudeCliClient {
|
|
|
939
1158
|
}
|
|
940
1159
|
}
|
|
941
1160
|
resolveSessionCwd(promptContext) {
|
|
942
|
-
return promptContext?.
|
|
1161
|
+
return promptContext?.resolvedWorkingFolder?.rootPath ?? this.runtime.cwd;
|
|
943
1162
|
}
|
|
944
1163
|
async recycleSessionIfScopeChanged(channelId, state) {
|
|
945
1164
|
const session = state.session;
|
|
@@ -951,6 +1170,9 @@ export class ClaudeCliClient {
|
|
|
951
1170
|
&& state.sessionVisibilityKey === state.visibilityKey) {
|
|
952
1171
|
return;
|
|
953
1172
|
}
|
|
1173
|
+
if (state.backgroundTasksActive || state.backgroundContinuationPending) {
|
|
1174
|
+
throw new Error('Claude session scope cannot change while provider-owned background work is running');
|
|
1175
|
+
}
|
|
954
1176
|
this.logger?.debug('recycling Claude ACP session after session scope changed', {
|
|
955
1177
|
channelId,
|
|
956
1178
|
previousCwd: state.sessionCwd,
|
|
@@ -959,57 +1181,113 @@ export class ClaudeCliClient {
|
|
|
959
1181
|
nextVisibilityKey: state.visibilityKey,
|
|
960
1182
|
});
|
|
961
1183
|
state.session = undefined;
|
|
1184
|
+
state.backgroundTasksActive = false;
|
|
1185
|
+
state.backgroundContinuationPending = false;
|
|
1186
|
+
state.foregroundIsolationFence = undefined;
|
|
962
1187
|
state.sessionCwd = undefined;
|
|
963
1188
|
state.sessionVisibilityKey = undefined;
|
|
964
1189
|
if (!this.disposing && !this.backendClosed && state.sessionPersistence === 'persistent') {
|
|
965
|
-
await this.clearPersistedSessionBestEffort(channelId);
|
|
1190
|
+
await this.clearPersistedSessionBestEffort(state.channelId);
|
|
966
1191
|
}
|
|
967
1192
|
this.closeSession(session);
|
|
968
1193
|
}
|
|
969
|
-
async runTurn(session, prompt, options) {
|
|
1194
|
+
async runTurn(session, prompt, options, foregroundIsolationFence) {
|
|
1195
|
+
let updatePump = this.sessionUpdatePumps.get(session);
|
|
1196
|
+
if (!updatePump) {
|
|
1197
|
+
updatePump = new ClaudeSessionUpdatePump(session, (error) => {
|
|
1198
|
+
const binding = [...this.channels.entries()].find(([, candidate]) => candidate.session === session);
|
|
1199
|
+
if (binding) {
|
|
1200
|
+
const [, state] = binding;
|
|
1201
|
+
this.logger?.debug('discarding failed Claude ACP session update pump', {
|
|
1202
|
+
channelId: state.channelId,
|
|
1203
|
+
error: error.message,
|
|
1204
|
+
});
|
|
1205
|
+
this.invalidateSession(state.routingKey, state, session);
|
|
1206
|
+
}
|
|
1207
|
+
}, (active) => {
|
|
1208
|
+
const binding = [...this.channels.entries()].find(([, candidate]) => candidate.session === session);
|
|
1209
|
+
if (!binding) {
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
const [, state] = binding;
|
|
1213
|
+
state.backgroundTasksActive = active;
|
|
1214
|
+
state.backgroundContinuationPending = false;
|
|
1215
|
+
if (active) {
|
|
1216
|
+
this.promoteWorkerRoute(state, session);
|
|
1217
|
+
this.idleBackendShutdown.cancel();
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
this.reconcileIdleChannelState(state);
|
|
1221
|
+
this.idleBackendShutdown.reconcile();
|
|
1222
|
+
}, (foregroundTurn) => {
|
|
1223
|
+
const binding = [...this.channels.entries()].find(([, candidate]) => candidate.session === session);
|
|
1224
|
+
if (!binding) {
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
const [, state] = binding;
|
|
1228
|
+
state.backgroundContinuationPending = true;
|
|
1229
|
+
state.foregroundIsolationFence = Math.max(state.foregroundIsolationFence ?? 0, foregroundTurn);
|
|
1230
|
+
this.promoteWorkerRoute(state, session);
|
|
1231
|
+
this.idleBackendShutdown.cancel();
|
|
1232
|
+
}, this.targetedBackgroundRunCancellationAvailable);
|
|
1233
|
+
this.sessionUpdatePumps.set(session, updatePump);
|
|
1234
|
+
}
|
|
1235
|
+
const turnUpdates = updatePump.beginTurn(options?.onProgress, foregroundIsolationFence);
|
|
970
1236
|
const promptPromise = this.raceWithFatal(session.prompt(prompt));
|
|
971
1237
|
const promptFailure = new Promise((_, reject) => {
|
|
972
1238
|
void promptPromise.catch((error) => reject(markSessionTainted(error)));
|
|
973
1239
|
});
|
|
974
1240
|
const collector = new AcpProgressCollector(options?.onProgress, readClaudeActivityMetadata);
|
|
975
1241
|
const compactionObserver = new ClaudeCompactionObserver(options?.onProgress);
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
update = await this.raceWithFatal(Promise.race([session.nextUpdate(), promptFailure]));
|
|
980
|
-
}
|
|
981
|
-
catch (error) {
|
|
982
|
-
if (this.fatalError && error === this.fatalError) {
|
|
983
|
-
throw error;
|
|
984
|
-
}
|
|
985
|
-
throw markSessionTainted(error);
|
|
986
|
-
}
|
|
987
|
-
if (update.kind === 'stop') {
|
|
988
|
-
let response;
|
|
1242
|
+
try {
|
|
1243
|
+
for (;;) {
|
|
1244
|
+
let update;
|
|
989
1245
|
try {
|
|
990
|
-
|
|
1246
|
+
update = await this.raceWithFatal(Promise.race([turnUpdates.next(), promptFailure]));
|
|
991
1247
|
}
|
|
992
1248
|
catch (error) {
|
|
1249
|
+
if (this.fatalError && error === this.fatalError) {
|
|
1250
|
+
throw error;
|
|
1251
|
+
}
|
|
993
1252
|
throw markSessionTainted(error);
|
|
994
1253
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}
|
|
1254
|
+
if (update.kind === 'stop') {
|
|
1255
|
+
let response;
|
|
1256
|
+
try {
|
|
1257
|
+
response = await promptPromise;
|
|
1258
|
+
}
|
|
1259
|
+
catch (error) {
|
|
1260
|
+
throw markSessionTainted(error);
|
|
1261
|
+
}
|
|
1262
|
+
const output = collector.getFinalText();
|
|
1263
|
+
if (response.stopReason === 'cancelled') {
|
|
1264
|
+
throw new ProviderTurnCancelledError('Claude ACP turn cancelled', {
|
|
1265
|
+
publicReplyText: hasVisibleText(output) ? output : CLAUDE_CANCELLED_REPLY_TEXT,
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
if (response.stopReason !== 'end_turn' && !hasVisibleText(output)) {
|
|
1269
|
+
throw new Error(`Claude ACP turn stopped with stopReason "${response.stopReason}"`);
|
|
1270
|
+
}
|
|
1271
|
+
return output;
|
|
1000
1272
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1273
|
+
const settledForegroundTurn = update.kind === 'session_update'
|
|
1274
|
+
? readClaudeForegroundSettledTurn(update.update)
|
|
1275
|
+
: undefined;
|
|
1276
|
+
if (settledForegroundTurn !== undefined) {
|
|
1277
|
+
return collector.getFinalText();
|
|
1003
1278
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1279
|
+
if (update.update.sessionUpdate === 'agent_message_chunk' &&
|
|
1280
|
+
update.update.content.type === 'text') {
|
|
1281
|
+
compactionObserver.consumeAnswerChunk(update.update.content.text);
|
|
1282
|
+
}
|
|
1283
|
+
if (update.update.sessionUpdate === 'usage_update') {
|
|
1284
|
+
compactionObserver.consumeUsageUpdate(update.update.used, update.update.size);
|
|
1285
|
+
}
|
|
1286
|
+
collector.consume(update);
|
|
1011
1287
|
}
|
|
1012
|
-
|
|
1288
|
+
}
|
|
1289
|
+
finally {
|
|
1290
|
+
turnUpdates.close();
|
|
1013
1291
|
}
|
|
1014
1292
|
}
|
|
1015
1293
|
async raceWithFatal(promise) {
|
|
@@ -1035,6 +1313,9 @@ export class ClaudeCliClient {
|
|
|
1035
1313
|
this.closeSession(state.session);
|
|
1036
1314
|
}
|
|
1037
1315
|
state.session = undefined;
|
|
1316
|
+
state.backgroundTasksActive = false;
|
|
1317
|
+
state.backgroundContinuationPending = false;
|
|
1318
|
+
state.foregroundIsolationFence = undefined;
|
|
1038
1319
|
state.sessionCwd = undefined;
|
|
1039
1320
|
state.sessionVisibilityKey = undefined;
|
|
1040
1321
|
state.sessionPromise = undefined;
|
|
@@ -1044,6 +1325,7 @@ export class ClaudeCliClient {
|
|
|
1044
1325
|
}
|
|
1045
1326
|
}
|
|
1046
1327
|
this.channels.clear();
|
|
1328
|
+
this.channelWorkerRoutes.clear();
|
|
1047
1329
|
this.shutdownPromise = this.shutdownBackend(error);
|
|
1048
1330
|
}
|
|
1049
1331
|
handlePermissionRequest(params) {
|
|
@@ -1055,19 +1337,106 @@ export class ClaudeCliClient {
|
|
|
1055
1337
|
logger: this.logger,
|
|
1056
1338
|
agentId: this.resolveSessionStoreAgentId()?.trim() || undefined,
|
|
1057
1339
|
channelId: state?.channelId,
|
|
1340
|
+
sessionBound: state !== undefined,
|
|
1058
1341
|
toolPermissionMode: state?.toolPermissionMode ?? 'default',
|
|
1059
1342
|
});
|
|
1060
1343
|
}
|
|
1061
1344
|
invalidateSession(channelId, state, session) {
|
|
1062
1345
|
if (state.session === session) {
|
|
1063
1346
|
state.session = undefined;
|
|
1347
|
+
state.backgroundTasksActive = false;
|
|
1348
|
+
state.backgroundContinuationPending = false;
|
|
1349
|
+
state.foregroundIsolationFence = undefined;
|
|
1064
1350
|
state.sessionCwd = undefined;
|
|
1065
1351
|
state.sessionVisibilityKey = undefined;
|
|
1066
1352
|
}
|
|
1067
1353
|
this.logger?.debug('discarding tainted Claude ACP session', { channelId });
|
|
1068
1354
|
this.closeSession(session);
|
|
1069
1355
|
if (!this.disposing && !this.backendClosed && state.sessionPersistence === 'persistent') {
|
|
1070
|
-
void this.clearPersistedSessionBestEffort(channelId);
|
|
1356
|
+
void this.clearPersistedSessionBestEffort(state.channelId);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
promoteWorkerRoute(state, session) {
|
|
1360
|
+
this.channelWorkerRoutes.set(state.channelId, {
|
|
1361
|
+
phase: 'promoted',
|
|
1362
|
+
state,
|
|
1363
|
+
});
|
|
1364
|
+
state.sessionPersistence = 'persistent';
|
|
1365
|
+
void (async () => {
|
|
1366
|
+
if (!await this.ensureSessionStoreLoaded()) {
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const route = this.channelWorkerRoutes.get(state.channelId);
|
|
1370
|
+
if (route?.state !== state
|
|
1371
|
+
|| route.phase !== 'promoted'
|
|
1372
|
+
|| state.session !== session
|
|
1373
|
+
|| (!state.backgroundTasksActive && !state.persistentFollowUpAdopted)) {
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
await this.persistSessionBestEffort(state.channelId, session.sessionId, state.visibilityKey, state.sessionCwd ?? state.cwd ?? this.runtime.cwd);
|
|
1377
|
+
})().catch((error) => {
|
|
1378
|
+
this.logger.error('failed to initialize Claude session persistence for background work', {
|
|
1379
|
+
agentId: this.loadedSessionStoreAgentId,
|
|
1380
|
+
channelId: state.channelId,
|
|
1381
|
+
sessionId: session.sessionId,
|
|
1382
|
+
error: summarizeError(error),
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
releaseWorkerRoute(state, clearPersistent = false) {
|
|
1387
|
+
const route = this.channelWorkerRoutes.get(state.channelId);
|
|
1388
|
+
if (route?.state !== state) {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
this.channelWorkerRoutes.delete(state.channelId);
|
|
1392
|
+
if (clearPersistent && state.session) {
|
|
1393
|
+
void this.clearPersistedSessionBestEffort(state.channelId);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
reconcileIdleChannelState(state) {
|
|
1397
|
+
if (this.channels.get(state.routingKey) !== state
|
|
1398
|
+
|| this.disposing
|
|
1399
|
+
|| this.fatalError
|
|
1400
|
+
|| this.backendClosed
|
|
1401
|
+
|| state.processing
|
|
1402
|
+
|| state.activeTurn
|
|
1403
|
+
|| state.queue.length > 0
|
|
1404
|
+
|| state.sessionPromise) {
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const route = this.channelWorkerRoutes.get(state.channelId);
|
|
1408
|
+
if (route?.state === state
|
|
1409
|
+
&& route.phase === 'promoted'
|
|
1410
|
+
&& !state.backgroundTasksActive
|
|
1411
|
+
&& !state.backgroundContinuationPending) {
|
|
1412
|
+
if (state.routingKey === state.channelId) {
|
|
1413
|
+
this.releaseWorkerRoute(state);
|
|
1414
|
+
}
|
|
1415
|
+
else if (!state.persistentFollowUpAdopted) {
|
|
1416
|
+
state.sessionPersistence = 'ephemeral';
|
|
1417
|
+
this.releaseWorkerRoute(state, true);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
else if (route?.state === state
|
|
1421
|
+
&& route.phase === 'candidate'
|
|
1422
|
+
&& (state.routingKey === state.channelId
|
|
1423
|
+
|| state.sessionPersistence === 'ephemeral')) {
|
|
1424
|
+
this.releaseWorkerRoute(state);
|
|
1425
|
+
}
|
|
1426
|
+
if (state.sessionPersistence !== 'ephemeral'
|
|
1427
|
+
|| state.backgroundTasksActive
|
|
1428
|
+
|| state.backgroundContinuationPending) {
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const session = state.session;
|
|
1432
|
+
state.session = undefined;
|
|
1433
|
+
state.foregroundIsolationFence = undefined;
|
|
1434
|
+
state.sessionCwd = undefined;
|
|
1435
|
+
state.sessionVisibilityKey = undefined;
|
|
1436
|
+
this.releaseWorkerRoute(state);
|
|
1437
|
+
this.channels.delete(state.routingKey);
|
|
1438
|
+
if (session) {
|
|
1439
|
+
this.closeSession(session);
|
|
1071
1440
|
}
|
|
1072
1441
|
}
|
|
1073
1442
|
closeSession(session) {
|
|
@@ -1075,6 +1444,8 @@ export class ClaudeCliClient {
|
|
|
1075
1444
|
return;
|
|
1076
1445
|
}
|
|
1077
1446
|
this.closingSessions.add(session);
|
|
1447
|
+
this.sessionUpdatePumps.get(session)?.close();
|
|
1448
|
+
this.sessionUpdatePumps.delete(session);
|
|
1078
1449
|
const agent = this.connection?.agent;
|
|
1079
1450
|
const closeMethod = this.runtime.methods.agent.session?.close;
|
|
1080
1451
|
const closePromise = typeof agent?.closeSession === 'function'
|
|
@@ -1260,10 +1631,14 @@ export class ClaudeCliClient {
|
|
|
1260
1631
|
sessions.push(state.session);
|
|
1261
1632
|
}
|
|
1262
1633
|
state.session = undefined;
|
|
1634
|
+
state.backgroundTasksActive = false;
|
|
1635
|
+
state.backgroundContinuationPending = false;
|
|
1636
|
+
state.foregroundIsolationFence = undefined;
|
|
1263
1637
|
state.sessionCwd = undefined;
|
|
1264
1638
|
state.sessionVisibilityKey = undefined;
|
|
1265
1639
|
this.channels.delete(channelId);
|
|
1266
1640
|
}
|
|
1641
|
+
this.channelWorkerRoutes.clear();
|
|
1267
1642
|
this.logger?.debug('stopping idle Claude ACP backend', {
|
|
1268
1643
|
channels: sessions.length,
|
|
1269
1644
|
idleShutdownMs: this.runtime.idleBackendShutdownMs,
|
|
@@ -1275,6 +1650,7 @@ export class ClaudeCliClient {
|
|
|
1275
1650
|
this.startPromise = undefined;
|
|
1276
1651
|
this.childExitPromise = undefined;
|
|
1277
1652
|
this.sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
|
|
1653
|
+
this.targetedBackgroundRunCancellationAvailable = false;
|
|
1278
1654
|
this.idleShutdownPromise = undefined;
|
|
1279
1655
|
});
|
|
1280
1656
|
this.idleShutdownPromise = idleShutdownPromise;
|
|
@@ -1291,7 +1667,12 @@ export class ClaudeCliClient {
|
|
|
1291
1667
|
return false;
|
|
1292
1668
|
}
|
|
1293
1669
|
for (const state of this.channels.values()) {
|
|
1294
|
-
if (state.activeTurn ||
|
|
1670
|
+
if (state.activeTurn ||
|
|
1671
|
+
state.backgroundTasksActive ||
|
|
1672
|
+
state.backgroundContinuationPending ||
|
|
1673
|
+
state.queue.length > 0 ||
|
|
1674
|
+
state.sessionPromise ||
|
|
1675
|
+
state.processing) {
|
|
1295
1676
|
return false;
|
|
1296
1677
|
}
|
|
1297
1678
|
}
|