ds-harness-remote 0.5.10 → 0.5.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp/adapters/claude-process.js +1 -1
- package/dist/acp/adapters/claude-process.js.map +1 -1
- package/dist/acp/adapters/cursor-process.d.ts +3 -0
- package/dist/acp/adapters/cursor-process.d.ts.map +1 -1
- package/dist/acp/adapters/cursor-process.js +55 -1
- package/dist/acp/adapters/cursor-process.js.map +1 -1
- package/dist/acp/adapters/isolated-process.d.ts +45 -0
- package/dist/acp/adapters/isolated-process.d.ts.map +1 -0
- package/dist/acp/adapters/isolated-process.js +308 -0
- package/dist/acp/adapters/isolated-process.js.map +1 -0
- package/dist/acp/adapters/opencode-native-api.d.ts +16 -0
- package/dist/acp/adapters/opencode-native-api.d.ts.map +1 -0
- package/dist/acp/adapters/opencode-native-api.js +103 -0
- package/dist/acp/adapters/opencode-native-api.js.map +1 -0
- package/dist/acp/adapters/opencode-process.d.ts +22 -2
- package/dist/acp/adapters/opencode-process.d.ts.map +1 -1
- package/dist/acp/adapters/opencode-process.js +273 -27
- package/dist/acp/adapters/opencode-process.js.map +1 -1
- package/dist/acp/adapters/pi-process.d.ts +3 -40
- package/dist/acp/adapters/pi-process.d.ts.map +1 -1
- package/dist/acp/adapters/pi-process.js +3 -408
- package/dist/acp/adapters/pi-process.js.map +1 -1
- package/dist/acp/adapters/session-models.d.ts +33 -0
- package/dist/acp/adapters/session-models.d.ts.map +1 -0
- package/dist/acp/adapters/session-models.js +78 -0
- package/dist/acp/adapters/session-models.js.map +1 -0
- package/dist/acp/gateway.d.ts +4 -1
- package/dist/acp/gateway.d.ts.map +1 -1
- package/dist/acp/gateway.js +182 -28
- package/dist/acp/gateway.js.map +1 -1
- package/dist/acp/history-events.d.ts +8 -0
- package/dist/acp/history-events.d.ts.map +1 -0
- package/dist/acp/history-events.js +119 -0
- package/dist/acp/history-events.js.map +1 -0
- package/dist/acp/method-policy.d.ts +26 -16
- package/dist/acp/method-policy.d.ts.map +1 -1
- package/dist/acp/method-policy.js +6 -5
- package/dist/acp/method-policy.js.map +1 -1
- package/dist/acp/peer-bridge.d.ts +3 -2
- package/dist/acp/peer-bridge.d.ts.map +1 -1
- package/dist/acp/peer-bridge.js +6 -1
- package/dist/acp/peer-bridge.js.map +1 -1
- package/dist/acp/virtual-catalog.d.ts +5 -5
- package/dist/acp/virtual-catalog.d.ts.map +1 -1
- package/dist/acp/virtual-catalog.js +8 -21
- package/dist/acp/virtual-catalog.js.map +1 -1
- package/dist/acp/virtual-contract.d.ts +13 -6
- package/dist/acp/virtual-contract.d.ts.map +1 -1
- package/dist/acp/virtual-contract.js +3 -3
- package/dist/acp/virtual-contract.js.map +1 -1
- package/dist/acp/virtual-event-carrier.d.ts +1 -0
- package/dist/acp/virtual-event-carrier.d.ts.map +1 -1
- package/dist/acp/virtual-event-carrier.js +1 -1
- package/dist/acp/virtual-event-carrier.js.map +1 -1
- package/dist/acp/virtual-harness.d.ts +8 -3
- package/dist/acp/virtual-harness.d.ts.map +1 -1
- package/dist/acp/virtual-harness.js +128 -60
- package/dist/acp/virtual-harness.js.map +1 -1
- package/dist/acp/virtual-live.d.ts +3 -2
- package/dist/acp/virtual-live.d.ts.map +1 -1
- package/dist/acp/virtual-live.js +5 -4
- package/dist/acp/virtual-live.js.map +1 -1
- package/dist/acp/workspace-tools.js +1 -1
- package/dist/acp/workspace-tools.js.map +1 -1
- package/dist/client/navigation.js +1 -1
- package/dist/client/navigation.js.map +1 -1
- package/dist/client/settings.js +1 -1
- package/dist/client/settings.js.map +1 -1
- package/dist/client/shell.d.ts.map +1 -1
- package/dist/client/shell.js +3 -2
- package/dist/client/shell.js.map +1 -1
- package/dist/client/types.d.ts +3 -2
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/workspaces.d.ts.map +1 -1
- package/dist/client/workspaces.js +130 -48
- package/dist/client/workspaces.js.map +1 -1
- package/dist/client-capabilities.d.ts.map +1 -1
- package/dist/client-capabilities.js +3 -1
- package/dist/client-capabilities.js.map +1 -1
- package/dist/client-control.d.ts +3 -3
- package/dist/client-control.d.ts.map +1 -1
- package/dist/client-control.js +23 -3
- package/dist/client-control.js.map +1 -1
- package/dist/client-locale.d.ts +8 -4
- package/dist/client-locale.d.ts.map +1 -1
- package/dist/client-locale.js +16 -8
- package/dist/client-locale.js.map +1 -1
- package/dist/client-runtime-contract.d.ts +2 -1
- package/dist/client-runtime-contract.d.ts.map +1 -1
- package/dist/client-runtime-contract.js.map +1 -1
- package/dist/client-runtime.d.ts +3 -3
- package/dist/client-runtime.d.ts.map +1 -1
- package/dist/client-runtime.js.map +1 -1
- package/dist/client.github.js +115 -57
- package/dist/client.js +115 -57
- package/dist/client.js.map +1 -1
- package/dist/codex/app-server.d.ts +10 -0
- package/dist/codex/app-server.d.ts.map +1 -1
- package/dist/codex/app-server.js +24 -10
- package/dist/codex/app-server.js.map +1 -1
- package/dist/codex/domain.d.ts +3 -0
- package/dist/codex/domain.d.ts.map +1 -1
- package/dist/codex/domain.js +93 -17
- package/dist/codex/domain.js.map +1 -1
- package/dist/codex/method-policy.d.ts +2 -2
- package/dist/codex/peer-bridge.d.ts.map +1 -1
- package/dist/codex/peer-bridge.js +7 -5
- package/dist/codex/peer-bridge.js.map +1 -1
- package/dist/codex/virtual-harness.d.ts.map +1 -1
- package/dist/codex/virtual-harness.js +16 -3
- package/dist/codex/virtual-harness.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2669 -1872
- package/dist/index.js.map +4 -4
- package/dist/remote-typert-gateway.js +42 -1
- package/dist/remote-typert-gateway.js.map +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +11 -1
- package/dist/service.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace-bridge.d.ts.map +1 -1
- package/dist/workspace-bridge.js +26 -4
- package/dist/workspace-bridge.js.map +1 -1
- package/dsh-plugin.json +1 -1
- package/package.json +1 -1
package/dist/acp/gateway.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export declare class AcpRemoteGateway {
|
|
|
47
47
|
private readonly startingClients;
|
|
48
48
|
/** Desired configuration is saved immediately; unfinished cleanup must remain retryable. */
|
|
49
49
|
private readonly pendingReconfiguration;
|
|
50
|
+
private readonly archivingSessions;
|
|
51
|
+
private readonly opencodeOperations;
|
|
50
52
|
constructor(config: ResolvedAcpConfig, logger: SafeLogger, createAcp?: AcpFactory, restartDelaysMs?: readonly number[], terminalEnabled?: () => boolean, terminalSpawner?: RemoteTerminalSpawner | undefined);
|
|
51
53
|
/** Only implemented adapters are advertised; registry entries never select an adapter by executable name. */
|
|
52
54
|
enabledBackends(): string[];
|
|
@@ -65,6 +67,8 @@ export declare class AcpRemoteGateway {
|
|
|
65
67
|
};
|
|
66
68
|
createPeer(context: PeerConnectionContext, publish: PublishAcpFrame): AcpPeerBridge | undefined;
|
|
67
69
|
call(connectionId: string, input: unknown): Promise<unknown>;
|
|
70
|
+
private captureCallScope;
|
|
71
|
+
private callInternal;
|
|
68
72
|
private runPromptInBackground;
|
|
69
73
|
respond(connectionId: string, input: unknown): Promise<{
|
|
70
74
|
resolved: true;
|
|
@@ -102,7 +106,6 @@ export declare class AcpRemoteGateway {
|
|
|
102
106
|
private requireSessionOwner;
|
|
103
107
|
private captureCatalogClient;
|
|
104
108
|
private listCatalogSessions;
|
|
105
|
-
private piSessionCwd;
|
|
106
109
|
private catalogSessionCwd;
|
|
107
110
|
private requireExistingDirectory;
|
|
108
111
|
private listDirectory;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../src/acp/gateway.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../src/acp/gateway.ts"],"names":[],"mappings":"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"}
|
package/dist/acp/gateway.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IsolatedAcpClient } from './adapters/isolated-process.js';
|
|
1
2
|
import { WorkspaceBridge, WorkspaceBridgeState } from '../workspace-bridge.js';
|
|
2
3
|
import { TerminalPolicy } from '../terminal-policy.js';
|
|
3
4
|
import { AcpWorkspaceTools } from './workspace-tools.js';
|
|
@@ -7,7 +8,7 @@ import { homedir } from 'node:os';
|
|
|
7
8
|
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
8
9
|
import { RpcError } from '../safe-error.js';
|
|
9
10
|
import { PLUGIN_VERSION } from '../version.js';
|
|
10
|
-
import {
|
|
11
|
+
import { CursorAcpError, } from './adapters/cursor-process.js';
|
|
11
12
|
import { ClaudeAcpClient } from './adapters/claude-process.js';
|
|
12
13
|
import { PiAcpClient } from './adapters/pi-process.js';
|
|
13
14
|
import { AntigravityAcpClient } from './adapters/antigravity-process.js';
|
|
@@ -63,7 +64,7 @@ export class AcpRemoteGateway {
|
|
|
63
64
|
/** Catch-up buffer for turns that finish while the Client is reconnecting. */
|
|
64
65
|
recentFrames = new Map();
|
|
65
66
|
/** Per-prompt live updates; attached to prompt_completed when streaming was lossy. */
|
|
66
|
-
promptsInProgress = new
|
|
67
|
+
promptsInProgress = new Map();
|
|
67
68
|
cancelledSessions = new Set();
|
|
68
69
|
/** Identity fences asynchronous Pi preparation from a later retry of the same session. */
|
|
69
70
|
preparingPrompts = new Map();
|
|
@@ -72,6 +73,8 @@ export class AcpRemoteGateway {
|
|
|
72
73
|
startingClients = new Map();
|
|
73
74
|
/** Desired configuration is saved immediately; unfinished cleanup must remain retryable. */
|
|
74
75
|
pendingReconfiguration = new Set();
|
|
76
|
+
archivingSessions = new Set();
|
|
77
|
+
opencodeOperations = new Map();
|
|
75
78
|
constructor(config, logger, createAcp = (binary, targetLogger, backend) => {
|
|
76
79
|
if (backend.id === 'antigravity') {
|
|
77
80
|
return new AntigravityAcpClient(binary, targetLogger, undefined, { args: backend.args, cwd: backend.cwd });
|
|
@@ -83,7 +86,7 @@ export class AcpRemoteGateway {
|
|
|
83
86
|
if (backend.id === 'opencode') {
|
|
84
87
|
return new OpencodeAcpClient(binary, targetLogger, undefined, { args: backend.args, cwd: backend.cwd });
|
|
85
88
|
}
|
|
86
|
-
return new
|
|
89
|
+
return new IsolatedAcpClient(binary, targetLogger, { args: backend.args, cwd: backend.cwd, backend: 'cursor' });
|
|
87
90
|
}, restartDelaysMs = DEFAULT_RESTART_DELAYS_MS, terminalEnabled = () => false, terminalSpawner) {
|
|
88
91
|
this.config = config;
|
|
89
92
|
this.logger = logger;
|
|
@@ -169,12 +172,51 @@ export class AcpRemoteGateway {
|
|
|
169
172
|
async call(connectionId, input) {
|
|
170
173
|
const envelope = parseCallEnvelope(input);
|
|
171
174
|
const call = parseAcpCall(envelope.method, envelope.params);
|
|
175
|
+
const sessionId = sessionIdFromParams(call.method, call.params);
|
|
176
|
+
const backend = call.params.backend ?? (sessionId ? this.sessionBackends.get(sessionId) ?? this.defaultBackend() : undefined);
|
|
177
|
+
const assertCurrent = this.captureCallScope(connectionId, typeof backend === 'string' ? backend : this.defaultBackend());
|
|
178
|
+
const reserve = backend === 'opencode' && sessionId !== undefined && ['session/load', 'session/prompt', 'dsh/selectModel'].includes(call.method);
|
|
179
|
+
if (reserve) {
|
|
180
|
+
if (this.archivingSessions.has(sessionId))
|
|
181
|
+
throw new RpcError('ACP_SESSION_BUSY', 'The OpenCode session is being archived.');
|
|
182
|
+
this.opencodeOperations.set(sessionId, (this.opencodeOperations.get(sessionId) ?? 0) + 1);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const result = await this.callInternal(connectionId, call, assertCurrent);
|
|
186
|
+
assertCurrent();
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
if (reserve) {
|
|
191
|
+
const remaining = (this.opencodeOperations.get(sessionId) ?? 1) - 1;
|
|
192
|
+
if (remaining)
|
|
193
|
+
this.opencodeOperations.set(sessionId, remaining);
|
|
194
|
+
else
|
|
195
|
+
this.opencodeOperations.delete(sessionId);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
captureCallScope(connectionId, backend) {
|
|
200
|
+
const peer = this.peers.get(connectionId);
|
|
201
|
+
const instance = this.backendInstances.get(backend);
|
|
202
|
+
return () => {
|
|
203
|
+
if (this.closed || this.backendInstances.get(backend) !== instance
|
|
204
|
+
|| (instance !== undefined && this.requireAcp(backend) !== instance.client)) {
|
|
205
|
+
throw new RpcError('CURSOR_UNAVAILABLE', 'The ACP backend changed while handling the request.');
|
|
206
|
+
}
|
|
207
|
+
if (peer !== undefined && this.peers.get(connectionId) !== peer) {
|
|
208
|
+
throw new RpcError('ACP_CONNECTION_CLOSED', 'The ACP connection is closed.');
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async callInternal(connectionId, call, assertCurrent) {
|
|
213
|
+
assertCurrent();
|
|
172
214
|
this.logger.info('ACP call received', {
|
|
173
215
|
method: call.method,
|
|
174
216
|
sessionId: typeof call.params?.sessionId === 'string' ? shortSessionId(call.params.sessionId) : undefined,
|
|
175
217
|
});
|
|
176
218
|
if (typeof call.params.backend === 'string') {
|
|
177
|
-
const prefix = typeof call.params.sessionId === 'string' ? /^(acp|cursor|pi|claude):/.exec(call.params.sessionId)?.[1] : undefined;
|
|
219
|
+
const prefix = typeof call.params.sessionId === 'string' ? /^(acp|cursor|pi|claude|opencode):/.exec(call.params.sessionId)?.[1] : undefined;
|
|
178
220
|
if (prefix && (prefix === 'acp' ? 'antigravity' : prefix) !== call.params.backend) {
|
|
179
221
|
throw new RpcError('INVALID_MESSAGE', 'The ACP session backend does not match.');
|
|
180
222
|
}
|
|
@@ -192,14 +234,83 @@ export class AcpRemoteGateway {
|
|
|
192
234
|
this.requireAcp(requestedBackend);
|
|
193
235
|
return this.initializeResult(call.params, requestedBackend);
|
|
194
236
|
}
|
|
237
|
+
if (call.method === 'dsh/sessionArchive') {
|
|
238
|
+
const sessionId = String(call.params.sessionId);
|
|
239
|
+
if (this.archivingSessions.has(sessionId) || this.opencodeOperations.has(sessionId))
|
|
240
|
+
throw new RpcError('ACP_SESSION_BUSY', 'The OpenCode session is busy.');
|
|
241
|
+
this.archivingSessions.add(sessionId);
|
|
242
|
+
try {
|
|
243
|
+
const backend = 'opencode';
|
|
244
|
+
const client = this.requireAcp(backend);
|
|
245
|
+
const instance = this.backendInstances.get(backend);
|
|
246
|
+
const cwd = await this.catalogSessionCwd(backend, sessionId);
|
|
247
|
+
assertCurrent();
|
|
248
|
+
if (this.closed || this.requireAcp(backend) !== client || this.backendInstances.get(backend) !== instance)
|
|
249
|
+
throw new RpcError('OPENCODE_ARCHIVE_UNAVAILABLE', 'The OpenCode backend changed. Refresh the session list.');
|
|
250
|
+
if (this.promptsInProgress.has(sessionId) || [...this.approvals.values()].some(item => item.sessionId === sessionId))
|
|
251
|
+
throw new RpcError('ACP_SESSION_BUSY', 'Stop the OpenCode reply and resolve its approvals before archiving.');
|
|
252
|
+
if (!client.archiveSession)
|
|
253
|
+
throw new RpcError('OPENCODE_ARCHIVE_UNAVAILABLE', 'The Host OpenCode adapter does not support native archiving.');
|
|
254
|
+
await client.archiveSession(sessionId, cwd);
|
|
255
|
+
if (this.closed || this.requireAcp(backend) !== client || this.backendInstances.get(backend) !== instance)
|
|
256
|
+
throw new RpcError('OPENCODE_ARCHIVE_UNAVAILABLE', 'The OpenCode backend changed. Refresh the session list to confirm the archive state.');
|
|
257
|
+
const result = { backend, sessionId, archived: true };
|
|
258
|
+
this.inboundChain = this.inboundChain.then(async () => {
|
|
259
|
+
if (!this.closed && this.backendInstances.get(backend) === instance) {
|
|
260
|
+
await Promise.allSettled([...this.peers.values()].map(peer => peer.publishArchived(result)));
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
await this.inboundChain;
|
|
264
|
+
if (this.closed || this.backendInstances.get(backend) !== instance)
|
|
265
|
+
throw new RpcError('OPENCODE_ARCHIVE_UNAVAILABLE', 'The OpenCode backend changed. Refresh the session list to confirm the archive state.');
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
this.archivingSessions.delete(sessionId);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (typeof call.params.sessionId === 'string' && this.archivingSessions.has(call.params.sessionId)
|
|
273
|
+
&& ['session/load', 'session/prompt', 'dsh/selectModel'].includes(call.method))
|
|
274
|
+
throw new RpcError('ACP_SESSION_BUSY', 'The OpenCode session is being archived.');
|
|
275
|
+
const controlId = sessionIdFromParams(call.method, call.params);
|
|
276
|
+
if (controlId && (call.params.backend ?? this.sessionBackends.get(controlId) ?? this.defaultBackend()) === 'opencode'
|
|
277
|
+
&& ['session/load', 'session/prompt', 'dsh/selectModel'].includes(call.method)) {
|
|
278
|
+
const client = this.requireAcp('opencode');
|
|
279
|
+
if (client.isSessionArchived) {
|
|
280
|
+
const cwd = await this.catalogSessionCwd('opencode', controlId);
|
|
281
|
+
const archived = await client.isSessionArchived(controlId, cwd);
|
|
282
|
+
if (this.closed || this.requireAcp('opencode') !== client)
|
|
283
|
+
throw new RpcError('OPENCODE_ARCHIVE_UNAVAILABLE', 'The OpenCode backend changed.');
|
|
284
|
+
if (archived)
|
|
285
|
+
throw new RpcError('ACP_SESSION_ARCHIVED', 'This OpenCode session is archived.');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
assertCurrent();
|
|
195
289
|
if (call.method === 'dsh/directoryList') {
|
|
196
290
|
return this.listDirectory(String(call.params.path));
|
|
197
291
|
}
|
|
198
292
|
if (call.method === 'dsh/workspaceList') {
|
|
199
293
|
const requestedBackend = typeof call.params.backend === 'string' ? call.params.backend : this.defaultBackend();
|
|
200
|
-
if (requestedBackend === 'pi' || requestedBackend === 'claude') {
|
|
294
|
+
if (requestedBackend === 'pi' || requestedBackend === 'claude' || requestedBackend === 'opencode') {
|
|
201
295
|
const sessions = await this.listCatalogSessions(requestedBackend);
|
|
202
|
-
|
|
296
|
+
const discovered = [...new Set(sessions.map(item => item.cwd))].map(path => ({ path, title: basename(path) || 'workspace' }));
|
|
297
|
+
if (discovered.length > 0 || requestedBackend !== 'opencode')
|
|
298
|
+
return discovered;
|
|
299
|
+
const defaultCandidates = new Set();
|
|
300
|
+
const cwd = process.cwd();
|
|
301
|
+
if (cwd)
|
|
302
|
+
defaultCandidates.add(cwd);
|
|
303
|
+
for (const candidate of ['/var/lib/dsh/workspace/ds-harness-remote', '/srv/dsh-workspace', '/var/lib/dsh/local']) {
|
|
304
|
+
try {
|
|
305
|
+
const s = await stat(candidate);
|
|
306
|
+
if (s.isDirectory())
|
|
307
|
+
defaultCandidates.add(candidate);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// ignore
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return [...defaultCandidates].map(path => ({ path, title: basename(path) || 'workspace' }));
|
|
203
314
|
}
|
|
204
315
|
const candidates = new Set();
|
|
205
316
|
const cwd = process.cwd();
|
|
@@ -255,20 +366,24 @@ export class AcpRemoteGateway {
|
|
|
255
366
|
// Prepare one idle process in that directory without awaiting startup.
|
|
256
367
|
if (path.trim() !== '' && call.params.prewarm !== false) {
|
|
257
368
|
const cwd = await this.requireExistingDirectory(path);
|
|
369
|
+
assertCurrent();
|
|
258
370
|
this.requireAcp('antigravity').prewarmSession?.(cwd);
|
|
259
371
|
}
|
|
260
372
|
const items = await discoverAntigravitySessions(path, limit);
|
|
261
373
|
if (path) {
|
|
262
374
|
const cwd = await this.requireExistingDirectory(path);
|
|
375
|
+
assertCurrent();
|
|
263
376
|
for (const item of items)
|
|
264
377
|
this.sessionCwds.set(item.conversationId, { backend, cwd });
|
|
265
378
|
}
|
|
266
379
|
this.logger.info('ACP session list fetched', { count: items.length });
|
|
267
380
|
return { items };
|
|
268
381
|
}
|
|
269
|
-
if (backend === 'pi' || backend === 'claude') {
|
|
382
|
+
if (backend === 'pi' || backend === 'claude' || backend === 'opencode') {
|
|
270
383
|
const cwd = path ? await this.requireExistingDirectory(path) : undefined;
|
|
271
384
|
const sessions = await this.listCatalogSessions(backend, cwd);
|
|
385
|
+
sessions.sort((a, b) => (Date.parse(b.updatedAt ?? '') || 0) - (Date.parse(a.updatedAt ?? '') || 0)
|
|
386
|
+
|| a.sessionId.localeCompare(b.sessionId));
|
|
272
387
|
return { items: sessions.slice(0, limit).map(item => ({ conversationId: item.sessionId, title: item.title ?? 'Untitled Session', createdAt: Date.parse(item.updatedAt ?? '') || 0, updatedAt: Date.parse(item.updatedAt ?? '') || 0, cwd: item.cwd })) };
|
|
273
388
|
}
|
|
274
389
|
return { items: [] };
|
|
@@ -293,9 +408,10 @@ export class AcpRemoteGateway {
|
|
|
293
408
|
throw new RpcError('INVALID_MESSAGE', 'The ACP session backend does not match.');
|
|
294
409
|
}
|
|
295
410
|
this.requireAcp(backend);
|
|
296
|
-
if (backend === 'pi' || backend === 'claude') {
|
|
411
|
+
if (backend === 'pi' || backend === 'claude' || backend === 'opencode') {
|
|
297
412
|
const cwd = await this.catalogSessionCwd(backend, conversationId);
|
|
298
|
-
|
|
413
|
+
// History uses an isolated reader and must neither require nor claim
|
|
414
|
+
// the live session's control ownership.
|
|
299
415
|
const client = this.requireAcp(backend);
|
|
300
416
|
if (!client.history)
|
|
301
417
|
throw new RpcError('METHOD_NOT_ALLOWED', 'The ACP backend does not support history.');
|
|
@@ -312,6 +428,7 @@ export class AcpRemoteGateway {
|
|
|
312
428
|
}
|
|
313
429
|
if (call.method === 'session/new') {
|
|
314
430
|
const cwd = await this.requireExistingDirectory(String(call.params.cwd));
|
|
431
|
+
assertCurrent();
|
|
315
432
|
const requestedBackend = typeof call.params.backend === 'string'
|
|
316
433
|
? call.params.backend
|
|
317
434
|
: this.defaultBackend();
|
|
@@ -321,6 +438,7 @@ export class AcpRemoteGateway {
|
|
|
321
438
|
mcpServers: [],
|
|
322
439
|
...(typeof call.params.mode === 'string' ? { mode: call.params.mode } : {}),
|
|
323
440
|
});
|
|
441
|
+
assertCurrent();
|
|
324
442
|
const sessionId = readSessionId(result);
|
|
325
443
|
if (sessionId !== undefined) {
|
|
326
444
|
const bound = this.sessionBackends.get(sessionId);
|
|
@@ -360,11 +478,16 @@ export class AcpRemoteGateway {
|
|
|
360
478
|
if (boundBackend !== undefined && targetBackend !== boundBackend) {
|
|
361
479
|
throw new RpcError('INVALID_MESSAGE', 'The ACP session backend does not match.');
|
|
362
480
|
}
|
|
363
|
-
if (targetBackend === 'pi' || targetBackend === 'claude')
|
|
481
|
+
if (targetBackend === 'pi' || targetBackend === 'claude' || targetBackend === 'opencode')
|
|
364
482
|
await this.catalogSessionCwd(targetBackend, sessionId);
|
|
483
|
+
assertCurrent();
|
|
365
484
|
this.requireSessionOwner(connectionId, sessionId);
|
|
366
485
|
const acp = this.requireAcp(targetBackend);
|
|
486
|
+
// Track pending loads too, so reconfiguration revokes their ownership
|
|
487
|
+
// before an upstream result can arrive.
|
|
488
|
+
this.sessionBackends.set(sessionId, targetBackend);
|
|
367
489
|
const result = await acp.call(call.method, { ...call.params, ...(sessionId && this.sessionCwds.get(sessionId) ? { cwd: this.sessionCwds.get(sessionId).cwd, mcpServers: [] } : {}) });
|
|
490
|
+
assertCurrent();
|
|
368
491
|
const loadedId = readSessionId(result) ?? sessionId;
|
|
369
492
|
if (loadedId !== undefined) {
|
|
370
493
|
if (loadedId !== sessionId || (this.sessionBackends.has(loadedId) && targetBackend !== undefined && this.sessionBackends.get(loadedId) !== targetBackend)) {
|
|
@@ -379,12 +502,17 @@ export class AcpRemoteGateway {
|
|
|
379
502
|
if (isSessionMutation(call.method) && sessionId !== undefined) {
|
|
380
503
|
this.requireSessionOwner(connectionId, sessionId);
|
|
381
504
|
}
|
|
382
|
-
if ((call.method === 'dsh/sessionModels' || call.method === 'dsh/selectModel') && call.params.backend === 'pi') {
|
|
505
|
+
if ((call.method === 'dsh/sessionModels' || call.method === 'dsh/selectModel') && (call.params.backend === 'pi' || call.params.backend === 'claude' || call.params.backend === 'opencode')) {
|
|
383
506
|
this.requireSessionOwner(connectionId, sessionId);
|
|
384
|
-
const
|
|
385
|
-
await this.
|
|
507
|
+
const backend = call.params.backend;
|
|
508
|
+
const cwd = await this.catalogSessionCwd(backend, sessionId);
|
|
509
|
+
assertCurrent();
|
|
510
|
+
await this.requireAcp(backend).call('session/load', { sessionId, cwd, mcpServers: [] });
|
|
511
|
+
assertCurrent();
|
|
386
512
|
}
|
|
387
513
|
if (call.method === 'dsh/selectModel') {
|
|
514
|
+
if (this.promptsInProgress.has(sessionId))
|
|
515
|
+
throw new RpcError('PROMPT_IN_PROGRESS', 'Stop the ACP reply before changing its model.');
|
|
388
516
|
const session = this.sessionCwds.get(sessionId);
|
|
389
517
|
if (!session || session.backend !== call.params.backend) {
|
|
390
518
|
throw new RpcError('ACP_WORKSPACE_UNAVAILABLE', 'The ACP session has no trusted workspace.');
|
|
@@ -392,6 +520,7 @@ export class AcpRemoteGateway {
|
|
|
392
520
|
// Resume straight into the target model. Loading the old model first
|
|
393
521
|
// starts a second CLI process that would immediately be discarded.
|
|
394
522
|
const result = await this.requireAcp(session.backend).call(call.method, { ...call.params, cwd: session.cwd });
|
|
523
|
+
assertCurrent();
|
|
395
524
|
this.sessionBackends.set(sessionId, session.backend);
|
|
396
525
|
return sanitizeSessionResult(result);
|
|
397
526
|
}
|
|
@@ -402,29 +531,35 @@ export class AcpRemoteGateway {
|
|
|
402
531
|
if (call.method === 'session/prompt' && sessionId !== undefined) {
|
|
403
532
|
if (this.promptsInProgress.has(sessionId))
|
|
404
533
|
throw new RpcError('PROMPT_IN_PROGRESS', 'An ACP reply is already in progress.');
|
|
405
|
-
this.
|
|
534
|
+
const backend = this.sessionBackends.get(sessionId) ?? this.defaultBackend();
|
|
535
|
+
const task = { backend, client: this.requireAcp(backend) };
|
|
536
|
+
this.promptsInProgress.set(sessionId, task);
|
|
406
537
|
this.cancelledSessions.delete(sessionId);
|
|
407
|
-
|
|
538
|
+
const preparationBackend = this.sessionBackends.get(sessionId);
|
|
539
|
+
if (preparationBackend === 'pi' || preparationBackend === 'claude') {
|
|
408
540
|
const preparation = { cancelled: false };
|
|
409
541
|
this.preparingPrompts.set(sessionId, preparation);
|
|
410
542
|
try {
|
|
411
|
-
const cwd = await this.
|
|
543
|
+
const cwd = await this.catalogSessionCwd(preparationBackend, sessionId);
|
|
412
544
|
if (preparation.cancelled)
|
|
413
545
|
return { accepted: false, stopReason: 'cancelled' };
|
|
414
|
-
|
|
546
|
+
assertCurrent();
|
|
547
|
+
await task.client.call('session/load', { sessionId, cwd, mcpServers: [] });
|
|
415
548
|
if (preparation.cancelled)
|
|
416
549
|
return { accepted: false, stopReason: 'cancelled' };
|
|
550
|
+
assertCurrent();
|
|
417
551
|
}
|
|
418
552
|
catch (error) {
|
|
419
553
|
if (preparation.cancelled)
|
|
420
554
|
return { accepted: false, stopReason: 'cancelled' };
|
|
421
|
-
this.promptsInProgress.
|
|
555
|
+
if (this.promptsInProgress.get(sessionId) === task)
|
|
556
|
+
this.promptsInProgress.delete(sessionId);
|
|
422
557
|
throw error;
|
|
423
558
|
}
|
|
424
559
|
finally {
|
|
425
560
|
if (this.preparingPrompts.get(sessionId) === preparation) {
|
|
426
561
|
this.preparingPrompts.delete(sessionId);
|
|
427
|
-
if (preparation.cancelled)
|
|
562
|
+
if (preparation.cancelled && this.promptsInProgress.get(sessionId) === task)
|
|
428
563
|
this.promptsInProgress.delete(sessionId);
|
|
429
564
|
}
|
|
430
565
|
}
|
|
@@ -436,7 +571,7 @@ export class AcpRemoteGateway {
|
|
|
436
571
|
});
|
|
437
572
|
}
|
|
438
573
|
this.turnCatchUp.set(sessionId, []);
|
|
439
|
-
void this.runPromptInBackground(sessionId, call.params);
|
|
574
|
+
void this.runPromptInBackground(sessionId, call.params, task);
|
|
440
575
|
return { accepted: true, stopReason: 'in_progress' };
|
|
441
576
|
}
|
|
442
577
|
if (call.method === 'session/cancel' && sessionId !== undefined) {
|
|
@@ -465,15 +600,17 @@ export class AcpRemoteGateway {
|
|
|
465
600
|
const sessionBackend = sessionId !== undefined ? this.sessionBackends.get(sessionId) : undefined;
|
|
466
601
|
return sanitizeSessionResult(await this.requireAcp(typeof call.params.backend === 'string' ? call.params.backend : sessionBackend).call(call.method, call.params));
|
|
467
602
|
}
|
|
468
|
-
async runPromptInBackground(sessionId, params) {
|
|
603
|
+
async runPromptInBackground(sessionId, params, task) {
|
|
604
|
+
const current = () => !this.closed && this.promptsInProgress.get(sessionId) === task
|
|
605
|
+
&& !this.pendingReconfiguration.has(task.backend) && this.backendInstances.get(task.backend)?.client === task.client;
|
|
469
606
|
try {
|
|
470
|
-
const
|
|
471
|
-
const acp = this.requireAcp(sessionBackend);
|
|
472
|
-
const result = await acp.call('session/prompt', params);
|
|
607
|
+
const result = await task.client.call('session/prompt', params);
|
|
473
608
|
// Cursor emits final session/update lines before the JSON-RPC result.
|
|
474
609
|
// Those notifications are queued on inboundChain; drain it before we
|
|
475
610
|
// snapshot catch-up or the Client only sees an empty prompt_completed.
|
|
476
611
|
await this.inboundChain;
|
|
612
|
+
if (!current())
|
|
613
|
+
return;
|
|
477
614
|
const stopReason = isRecord(result) && typeof result.stopReason === 'string'
|
|
478
615
|
? result.stopReason
|
|
479
616
|
: 'end_turn';
|
|
@@ -496,8 +633,12 @@ export class AcpRemoteGateway {
|
|
|
496
633
|
});
|
|
497
634
|
}
|
|
498
635
|
catch (error) {
|
|
636
|
+
if (!current())
|
|
637
|
+
return;
|
|
499
638
|
this.logger?.warn('ACP session/prompt failed', { code: errorCode(error) });
|
|
500
639
|
await this.inboundChain.catch(() => undefined);
|
|
640
|
+
if (!current())
|
|
641
|
+
return;
|
|
501
642
|
const catchUp = takeTurnCatchUp(this.turnCatchUp, sessionId);
|
|
502
643
|
await this.publishToSession(sessionId, {
|
|
503
644
|
method: 'session/update',
|
|
@@ -512,7 +653,8 @@ export class AcpRemoteGateway {
|
|
|
512
653
|
}).catch(() => undefined);
|
|
513
654
|
}
|
|
514
655
|
finally {
|
|
515
|
-
this.promptsInProgress.
|
|
656
|
+
if (this.promptsInProgress.get(sessionId) === task)
|
|
657
|
+
this.promptsInProgress.delete(sessionId);
|
|
516
658
|
}
|
|
517
659
|
}
|
|
518
660
|
async respond(connectionId, input) {
|
|
@@ -585,7 +727,7 @@ export class AcpRemoteGateway {
|
|
|
585
727
|
return;
|
|
586
728
|
}
|
|
587
729
|
if (owner !== connectionId) {
|
|
588
|
-
throw new RpcError('CURSOR_SESSION_OWNED', 'Another Remote connection owns this
|
|
730
|
+
throw new RpcError('CURSOR_SESSION_OWNED', 'Another Remote connection owns this session.');
|
|
589
731
|
}
|
|
590
732
|
}
|
|
591
733
|
/** Replay frames buffered while no healthy peer could receive them. */
|
|
@@ -636,6 +778,11 @@ export class AcpRemoteGateway {
|
|
|
636
778
|
.then(() => undefined, () => new RpcError('TERMINAL_CLEANUP_FAILED', 'Some ACP terminal processes could not be stopped. Save the backend settings again to retry.'));
|
|
637
779
|
for (const id of sessions) {
|
|
638
780
|
this.sessionOwners.delete(id);
|
|
781
|
+
this.promptsInProgress.delete(id);
|
|
782
|
+
const preparation = this.preparingPrompts.get(id);
|
|
783
|
+
if (preparation)
|
|
784
|
+
preparation.cancelled = true;
|
|
785
|
+
this.preparingPrompts.delete(id);
|
|
639
786
|
this.sessionBackends.delete(id);
|
|
640
787
|
this.recentFrames.delete(id);
|
|
641
788
|
this.turnCatchUp.delete(id);
|
|
@@ -683,6 +830,10 @@ export class AcpRemoteGateway {
|
|
|
683
830
|
this.sessionBackends.clear();
|
|
684
831
|
this.recentFrames.clear();
|
|
685
832
|
this.turnCatchUp.clear();
|
|
833
|
+
this.promptsInProgress.clear();
|
|
834
|
+
for (const preparation of this.preparingPrompts.values())
|
|
835
|
+
preparation.cancelled = true;
|
|
836
|
+
this.preparingPrompts.clear();
|
|
686
837
|
this.approvals.clear();
|
|
687
838
|
const processes = await Promise.allSettled([...this.startingClients.keys()].map(client => client.close()).concat(this.disposeAllInstances()));
|
|
688
839
|
const terminalError = await terminals;
|
|
@@ -998,7 +1149,7 @@ export class AcpRemoteGateway {
|
|
|
998
1149
|
return;
|
|
999
1150
|
}
|
|
1000
1151
|
if (owner !== connectionId && isSessionMutation(method)) {
|
|
1001
|
-
throw new RpcError('CURSOR_SESSION_OWNED', 'Another Remote connection owns this
|
|
1152
|
+
throw new RpcError('CURSOR_SESSION_OWNED', 'Another Remote connection owns this session.');
|
|
1002
1153
|
}
|
|
1003
1154
|
}
|
|
1004
1155
|
requireSessionOwner(connectionId, sessionId) {
|
|
@@ -1041,6 +1192,8 @@ export class AcpRemoteGateway {
|
|
|
1041
1192
|
continue;
|
|
1042
1193
|
this.sessionCwds.set(item.sessionId, { backend, cwd: canonical });
|
|
1043
1194
|
this.sessionBackends.set(item.sessionId, backend);
|
|
1195
|
+
if (backend === 'opencode' && item.archived === true)
|
|
1196
|
+
continue;
|
|
1044
1197
|
items.push({ sessionId: item.sessionId, cwd: canonical,
|
|
1045
1198
|
...(typeof item.title === 'string' ? { title: item.title } : {}),
|
|
1046
1199
|
...(typeof item.updatedAt === 'string' ? { updatedAt: item.updatedAt } : {}) });
|
|
@@ -1055,7 +1208,6 @@ export class AcpRemoteGateway {
|
|
|
1055
1208
|
}
|
|
1056
1209
|
return items;
|
|
1057
1210
|
}
|
|
1058
|
-
piSessionCwd(sessionId) { return this.catalogSessionCwd('pi', sessionId); }
|
|
1059
1211
|
async catalogSessionCwd(backend, sessionId) {
|
|
1060
1212
|
const { assertCurrent } = this.captureCatalogClient(backend);
|
|
1061
1213
|
if (!this.sessionCwds.has(sessionId))
|
|
@@ -1256,6 +1408,8 @@ export function cursorBinaryCandidates(configured) {
|
|
|
1256
1408
|
}
|
|
1257
1409
|
if (configured === 'opencode') {
|
|
1258
1410
|
return [
|
|
1411
|
+
// Official installer location (curl -fsSL https://opencode.ai/install | bash).
|
|
1412
|
+
join(userHome, '.opencode', 'bin', 'opencode'),
|
|
1259
1413
|
join(userHome, '.local', 'bin', 'opencode'),
|
|
1260
1414
|
'opencode',
|
|
1261
1415
|
];
|