farming-code 2.2.8 → 2.2.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/README.md +42 -28
- package/README.zh_cn.md +42 -28
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1592 -741
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +54 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +35 -0
- package/backend/control-api.js +192 -17
- package/backend/executable-discovery.js +2 -0
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +354 -84
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +176 -17
- package/backend/workspace-directory.js +232 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-DWsa7DXG.js +206 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
- package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
- package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
- package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
- package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
- package/dist/assets/code-dark-DPiuyeSp.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
- package/dist/assets/main-Bqtb5kuz.css +1 -0
- package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
- package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
- package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/farming-2/site.webmanifest +2 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1978 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-8dYAM6ql.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-D073SnW4.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
package/backend/acp-runtime.js
CHANGED
|
@@ -5,17 +5,18 @@ const { spawn } = require('child_process');
|
|
|
5
5
|
const { Readable, Writable } = require('stream');
|
|
6
6
|
const { createRequire } = require('module');
|
|
7
7
|
const packageJson = require('../package.json');
|
|
8
|
+
const { AcpCheckpointStore } = require('./acp-checkpoint-store');
|
|
8
9
|
const { AcpSessionState } = require('./acp-session-state');
|
|
10
|
+
const { readCodexHistoryImageData } = require('./codex-transcript');
|
|
9
11
|
const { AcpClientFileSystem, AcpClientTerminalManager } = require('./acp/client-services');
|
|
10
12
|
const { permissionSecurityWarnings } = require('./acp/permission-security');
|
|
11
13
|
const { rejectPatch } = require('./acp/patch-decisions');
|
|
14
|
+
const { getProviderAdapter, listProviderAdapters } = require('./provider-adapters');
|
|
15
|
+
|
|
16
|
+
const ADAPTER_VERSIONS = Object.freeze(Object.fromEntries(
|
|
17
|
+
listProviderAdapters().filter(adapter => adapter.acp).map(adapter => [adapter.id, adapter.acp.version]),
|
|
18
|
+
));
|
|
12
19
|
|
|
13
|
-
const ADAPTER_VERSIONS = Object.freeze({
|
|
14
|
-
codex: '1.1.2',
|
|
15
|
-
claude: '0.58.1',
|
|
16
|
-
opencode: 'native',
|
|
17
|
-
qoder: 'native',
|
|
18
|
-
});
|
|
19
20
|
const DEFAULT_INITIALIZE_TIMEOUT_MS = 15_000;
|
|
20
21
|
const DEFAULT_SESSION_SETUP_TIMEOUT_MS = 120_000;
|
|
21
22
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
@@ -44,67 +45,38 @@ function adapterEntry(packageName) {
|
|
|
44
45
|
return entry;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
version: ADAPTER_VERSIONS.codex,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
if (normalized === 'claude') {
|
|
48
|
+
function nodeAdapterLaunch(entry, env = process.env) {
|
|
49
|
+
const runtimeEnv = env && typeof env === 'object' ? env : process.env;
|
|
50
|
+
const nodeBin = runtimeEnv.FARMING_NODE_BIN || process.execPath;
|
|
51
|
+
const ldPath = runtimeEnv.FARMING_NODE_LD || '';
|
|
52
|
+
const libraryPath = runtimeEnv.FARMING_NODE_LIBRARY_PATH || '';
|
|
53
|
+
if (ldPath && libraryPath) {
|
|
57
54
|
return {
|
|
58
|
-
command:
|
|
59
|
-
args: [
|
|
60
|
-
version: ADAPTER_VERSIONS.claude,
|
|
55
|
+
command: ldPath,
|
|
56
|
+
args: ['--library-path', libraryPath, nodeBin, entry],
|
|
61
57
|
};
|
|
62
58
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
return { command: nodeBin, args: [entry] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveAcpLaunch(provider, options = {}) {
|
|
63
|
+
const adapter = getProviderAdapter(provider);
|
|
64
|
+
if (!adapter?.acp) throw new Error(`Unsupported ACP provider: ${provider}`);
|
|
65
|
+
if (adapter.acp.launch) {
|
|
66
|
+
return { ...adapter.acp.launch(options), version: adapter.acp.version };
|
|
69
67
|
}
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
};
|
|
68
|
+
if (adapter.acp.packageName) {
|
|
69
|
+
const launch = nodeAdapterLaunch(
|
|
70
|
+
adapterEntry(adapter.acp.packageName),
|
|
71
|
+
options.runtimeEnv || process.env,
|
|
72
|
+
);
|
|
73
|
+
return { ...launch, version: adapter.acp.version };
|
|
76
74
|
}
|
|
77
75
|
throw new Error(`Unsupported ACP provider: ${provider}`);
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
function codexAcpEnvironment(options = {}) {
|
|
81
|
-
|
|
82
|
-
let config = {};
|
|
83
|
-
if (env.CODEX_CONFIG) {
|
|
84
|
-
try {
|
|
85
|
-
const parsed = JSON.parse(env.CODEX_CONFIG);
|
|
86
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) config = parsed;
|
|
87
|
-
} catch {
|
|
88
|
-
// A selected Farming profile below replaces an invalid adapter config.
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (options.model && options.model !== 'config') config.model = options.model;
|
|
93
|
-
if (options.reasoningEffort && options.reasoningEffort !== 'config') {
|
|
94
|
-
config.model_reasoning_effort = options.reasoningEffort;
|
|
95
|
-
}
|
|
96
|
-
if (options.serviceTier && !['config', 'default'].includes(options.serviceTier)) {
|
|
97
|
-
config.service_tier = options.serviceTier;
|
|
98
|
-
}
|
|
99
|
-
if (Object.keys(config).length > 0) env.CODEX_CONFIG = JSON.stringify(config);
|
|
100
|
-
|
|
101
|
-
const initialMode = {
|
|
102
|
-
ask: 'read-only',
|
|
103
|
-
approve: 'agent',
|
|
104
|
-
full: 'agent-full-access',
|
|
105
|
-
}[options.approvalMode];
|
|
106
|
-
if (initialMode) env.INITIAL_AGENT_MODE = initialMode;
|
|
107
|
-
return env;
|
|
79
|
+
return getProviderAdapter('codex').prepareAcpEnvironment(options);
|
|
108
80
|
}
|
|
109
81
|
|
|
110
82
|
function selectedPermission(option) {
|
|
@@ -226,6 +198,10 @@ function interactiveRuntimeState(binding, fallback = '') {
|
|
|
226
198
|
return binding.sessionId ? 'idle' : 'connecting';
|
|
227
199
|
}
|
|
228
200
|
|
|
201
|
+
function clone(value) {
|
|
202
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
203
|
+
}
|
|
204
|
+
|
|
229
205
|
class AcpRuntime extends EventEmitter {
|
|
230
206
|
constructor(options = {}) {
|
|
231
207
|
super();
|
|
@@ -240,6 +216,8 @@ class AcpRuntime extends EventEmitter {
|
|
|
240
216
|
this.historyReplayMinWaitMs = options.historyReplayMinWaitMs ?? DEFAULT_HISTORY_REPLAY_MIN_WAIT_MS;
|
|
241
217
|
this.historyReplayQuietMs = options.historyReplayQuietMs ?? DEFAULT_HISTORY_REPLAY_QUIET_MS;
|
|
242
218
|
this.historyReplayMaxWaitMs = options.historyReplayMaxWaitMs ?? DEFAULT_HISTORY_REPLAY_MAX_WAIT_MS;
|
|
219
|
+
this.checkpointStore = options.checkpointStore
|
|
220
|
+
|| (options.configDir ? new AcpCheckpointStore(options.configDir, options.checkpointOptions) : null);
|
|
243
221
|
this.bindings = new Map();
|
|
244
222
|
this.permissionSequence = 0;
|
|
245
223
|
this.elicitationSequence = 0;
|
|
@@ -256,8 +234,9 @@ class AcpRuntime extends EventEmitter {
|
|
|
256
234
|
const binding = {
|
|
257
235
|
agentId,
|
|
258
236
|
provider,
|
|
237
|
+
providerHomeId: String(options.providerHomeId || 'default'),
|
|
259
238
|
cwd: path.resolve(options.cwd || process.cwd()),
|
|
260
|
-
env: provider
|
|
239
|
+
env: (getProviderAdapter(provider)?.prepareAcpEnvironment || (value => value.env || process.env))(options),
|
|
261
240
|
launch,
|
|
262
241
|
restartOptions: { ...options, agentId, provider },
|
|
263
242
|
approvalMode: options.approvalMode || 'approve',
|
|
@@ -278,9 +257,11 @@ class AcpRuntime extends EventEmitter {
|
|
|
278
257
|
subagentStates: new Map(),
|
|
279
258
|
interactionOrigins: new Map(),
|
|
280
259
|
promptActive: false,
|
|
260
|
+
historyReplayActive: false,
|
|
281
261
|
sessionState: null,
|
|
282
262
|
authTerminal: null,
|
|
283
263
|
patchDecisions: new Map(),
|
|
264
|
+
checkpointProof: null,
|
|
284
265
|
stderr: '',
|
|
285
266
|
updatedAt: new Date().toISOString(),
|
|
286
267
|
};
|
|
@@ -337,25 +318,81 @@ class AcpRuntime extends EventEmitter {
|
|
|
337
318
|
let historyMode = 'new';
|
|
338
319
|
if (requestedSessionId) {
|
|
339
320
|
const capabilities = binding.initializeResponse.agentCapabilities || {};
|
|
340
|
-
|
|
321
|
+
let opened = false;
|
|
322
|
+
let restoredCheckpointState = null;
|
|
323
|
+
let restoredCheckpoint = null;
|
|
324
|
+
let saved = null;
|
|
325
|
+
if (options.historyMode === 'checkpoint' && this.checkpointStore) {
|
|
326
|
+
saved = await this.checkpointStore.load(
|
|
327
|
+
this.checkpointIdentity(binding, requestedSessionId),
|
|
328
|
+
{ allowDirty: true },
|
|
329
|
+
);
|
|
330
|
+
restoredCheckpoint = this.restoreBindingCheckpoint(binding, saved?.state, {
|
|
331
|
+
sessionId: requestedSessionId,
|
|
332
|
+
});
|
|
333
|
+
restoredCheckpointState = restoredCheckpoint?.sessionState || null;
|
|
334
|
+
}
|
|
335
|
+
if (capabilities.sessionCapabilities?.resume && restoredCheckpoint) {
|
|
336
|
+
const providerStateMatches = restoredCheckpoint?.complete === true && saved?.exact === true
|
|
337
|
+
? await this.checkpointMatchesProviderSession(connection, capabilities, sessionRequest, saved)
|
|
338
|
+
: false;
|
|
339
|
+
if (providerStateMatches) {
|
|
340
|
+
binding.sessionId = requestedSessionId;
|
|
341
|
+
binding.sessionState = restoredCheckpointState;
|
|
342
|
+
binding.subagentStates = restoredCheckpoint.subagentStates;
|
|
343
|
+
binding.patchDecisions = restoredCheckpoint.patchDecisions;
|
|
344
|
+
binding.checkpointProof = restoredCheckpoint.providerProof;
|
|
345
|
+
try {
|
|
346
|
+
sessionResponse = await withTimeout(
|
|
347
|
+
connection.resumeSession(sessionRequest),
|
|
348
|
+
this.sessionSetupTimeoutMs,
|
|
349
|
+
'ACP session/resume'
|
|
350
|
+
);
|
|
351
|
+
historyMode = 'checkpoint';
|
|
352
|
+
opened = true;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (!capabilities.loadSession) throw error;
|
|
355
|
+
console.warn(`ACP checkpoint resume failed for ${provider}; replaying history:`, error && (error.message || error));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!opened && options.historyMode !== 'resume' && capabilities.loadSession) {
|
|
360
|
+
const replayRevisionBase = Math.max(revisionBase, Number(restoredCheckpointState?.revision || 0));
|
|
341
361
|
binding.sessionId = requestedSessionId;
|
|
342
362
|
binding.sessionState = new AcpSessionState({
|
|
343
363
|
provider,
|
|
344
364
|
sessionId: requestedSessionId,
|
|
345
365
|
cwd: binding.cwd,
|
|
346
366
|
maxUpdates: this.maxUpdates,
|
|
347
|
-
revisionBase,
|
|
348
|
-
resetBeforeRevision:
|
|
367
|
+
revisionBase: replayRevisionBase,
|
|
368
|
+
resetBeforeRevision: replayRevisionBase,
|
|
349
369
|
});
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
370
|
+
binding.historyReplayActive = true;
|
|
371
|
+
try {
|
|
372
|
+
sessionResponse = await withTimeout(
|
|
373
|
+
connection.loadSession(sessionRequest),
|
|
374
|
+
this.sessionSetupTimeoutMs,
|
|
375
|
+
'ACP session/load'
|
|
376
|
+
);
|
|
377
|
+
if (provider === 'qoder') await this.waitForHistoryReplay(binding);
|
|
378
|
+
} finally {
|
|
379
|
+
binding.historyReplayActive = false;
|
|
380
|
+
}
|
|
356
381
|
binding.sessionState.finishHistoryReplay();
|
|
382
|
+
if (provider === 'codex' && binding.sessionState.hasCodexHistoryImageReferences()) {
|
|
383
|
+
let imageDataByPath = new Map();
|
|
384
|
+
try {
|
|
385
|
+
imageDataByPath = await readCodexHistoryImageData(requestedSessionId, {
|
|
386
|
+
codexHome: binding.env.CODEX_HOME,
|
|
387
|
+
});
|
|
388
|
+
} catch {
|
|
389
|
+
// Local history images can still be restored from their adapter paths.
|
|
390
|
+
}
|
|
391
|
+
await binding.sessionState.hydrateCodexHistoryAttachments({ imageDataByPath });
|
|
392
|
+
}
|
|
357
393
|
historyMode = 'load';
|
|
358
|
-
|
|
394
|
+
opened = true;
|
|
395
|
+
} else if (!opened && capabilities.sessionCapabilities?.resume) {
|
|
359
396
|
binding.sessionId = requestedSessionId;
|
|
360
397
|
binding.sessionState = new AcpSessionState({
|
|
361
398
|
provider,
|
|
@@ -370,8 +407,9 @@ class AcpRuntime extends EventEmitter {
|
|
|
370
407
|
'ACP session/resume'
|
|
371
408
|
);
|
|
372
409
|
historyMode = 'resume';
|
|
410
|
+
opened = true;
|
|
373
411
|
} else {
|
|
374
|
-
throw new Error(`${provider} ACP Agent cannot load or resume session ${requestedSessionId}`);
|
|
412
|
+
if (!opened) throw new Error(`${provider} ACP Agent cannot load or resume session ${requestedSessionId}`);
|
|
375
413
|
}
|
|
376
414
|
} else {
|
|
377
415
|
sessionResponse = await withTimeout(
|
|
@@ -399,6 +437,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
399
437
|
}
|
|
400
438
|
binding.state = 'idle';
|
|
401
439
|
binding.updatedAt = new Date().toISOString();
|
|
440
|
+
this.scheduleCheckpoint(binding, { exact: true });
|
|
402
441
|
this.emitRuntime(binding);
|
|
403
442
|
this.emitSession(binding);
|
|
404
443
|
return {
|
|
@@ -417,6 +456,130 @@ class AcpRuntime extends EventEmitter {
|
|
|
417
456
|
}
|
|
418
457
|
}
|
|
419
458
|
|
|
459
|
+
checkpointIdentity(binding, sessionId = binding?.sessionId) {
|
|
460
|
+
if (!binding || !sessionId) return null;
|
|
461
|
+
return {
|
|
462
|
+
provider: binding.provider,
|
|
463
|
+
providerHomeId: binding.providerHomeId || 'default',
|
|
464
|
+
sessionId,
|
|
465
|
+
cwd: binding.cwd,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
scheduleCheckpoint(binding, _options = {}) {
|
|
470
|
+
const identity = this.checkpointIdentity(binding);
|
|
471
|
+
if (!this.checkpointStore || !identity || !binding.sessionState) return;
|
|
472
|
+
// ACP currently has no conditional resume or provider-owned revision
|
|
473
|
+
// token. A timestamp/list check has a TOCTOU window, so runtime snapshots
|
|
474
|
+
// remain dirty and are used only as revision/reset fences. Full history
|
|
475
|
+
// load is the authoritative recovery path until such a proof is available.
|
|
476
|
+
this.checkpointStore.schedule(identity, this.bindingCheckpoint(binding), { exact: false });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async markCheckpointDirty(binding) {
|
|
480
|
+
const identity = this.checkpointIdentity(binding);
|
|
481
|
+
if (!this.checkpointStore || !identity) return;
|
|
482
|
+
await this.checkpointStore.markDirty(identity);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async writeCheckpoint(binding, _options = {}) {
|
|
486
|
+
const identity = this.checkpointIdentity(binding);
|
|
487
|
+
if (!this.checkpointStore || !identity || !binding.sessionState) return;
|
|
488
|
+
await this.checkpointStore.write(identity, this.bindingCheckpoint(binding), { exact: false });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
bindingCheckpoint(binding) {
|
|
492
|
+
return {
|
|
493
|
+
exportCheckpoint: () => ({
|
|
494
|
+
version: 2,
|
|
495
|
+
complete: false,
|
|
496
|
+
sessionState: binding.sessionState?.exportCheckpoint() || null,
|
|
497
|
+
subagentStates: [...binding.subagentStates.entries()].map(([sessionId, state]) => ({
|
|
498
|
+
sessionId,
|
|
499
|
+
state: state.exportCheckpoint(),
|
|
500
|
+
})),
|
|
501
|
+
patchDecisions: [...binding.patchDecisions.entries()],
|
|
502
|
+
providerProof: binding.checkpointProof ? clone(binding.checkpointProof) : null,
|
|
503
|
+
}),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
restoreBindingCheckpoint(binding, checkpoint, options = {}) {
|
|
508
|
+
if (!checkpoint) return null;
|
|
509
|
+
const mainCheckpoint = checkpoint.version === 2
|
|
510
|
+
? checkpoint.sessionState
|
|
511
|
+
: checkpoint;
|
|
512
|
+
const sessionState = AcpSessionState.fromCheckpoint(mainCheckpoint, {
|
|
513
|
+
provider: binding.provider,
|
|
514
|
+
sessionId: options.sessionId || binding.sessionId,
|
|
515
|
+
cwd: binding.cwd,
|
|
516
|
+
maxUpdates: this.maxUpdates,
|
|
517
|
+
});
|
|
518
|
+
if (!sessionState) return null;
|
|
519
|
+
const subagentStates = new Map();
|
|
520
|
+
if (checkpoint.version === 2 && Array.isArray(checkpoint.subagentStates)) {
|
|
521
|
+
for (const item of checkpoint.subagentStates.slice(0, 32)) {
|
|
522
|
+
const sessionId = String(item?.sessionId || '');
|
|
523
|
+
if (!sessionId) continue;
|
|
524
|
+
const state = AcpSessionState.fromCheckpoint(item?.state, {
|
|
525
|
+
provider: binding.provider,
|
|
526
|
+
sessionId,
|
|
527
|
+
cwd: binding.cwd,
|
|
528
|
+
maxUpdates: this.maxUpdates,
|
|
529
|
+
});
|
|
530
|
+
if (state) subagentStates.set(sessionId, state);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const patchDecisions = new Map();
|
|
534
|
+
if (checkpoint.version === 2 && Array.isArray(checkpoint.patchDecisions)) {
|
|
535
|
+
for (const item of checkpoint.patchDecisions) {
|
|
536
|
+
if (!Array.isArray(item) || item.length !== 2) continue;
|
|
537
|
+
const key = String(item[0] || '');
|
|
538
|
+
const decision = String(item[1] || '');
|
|
539
|
+
if (key && ['kept', 'reverted'].includes(decision)) patchDecisions.set(key, decision);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
sessionState,
|
|
544
|
+
subagentStates,
|
|
545
|
+
patchDecisions,
|
|
546
|
+
providerProof: checkpoint.version === 2 ? clone(checkpoint.providerProof) : null,
|
|
547
|
+
complete: checkpoint.version === 2 && checkpoint.complete === true,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async checkpointMatchesProviderSession(connection, capabilities, request, saved) {
|
|
552
|
+
const proof = saved?.state?.providerProof;
|
|
553
|
+
if (
|
|
554
|
+
!capabilities?.sessionCapabilities?.list
|
|
555
|
+
|| !proof
|
|
556
|
+
|| typeof proof.token !== 'string'
|
|
557
|
+
|| !proof.token
|
|
558
|
+
) return false;
|
|
559
|
+
let cursor = '';
|
|
560
|
+
try {
|
|
561
|
+
for (let page = 0; page < 10; page += 1) {
|
|
562
|
+
const response = await withTimeout(connection.listSessions({
|
|
563
|
+
cwd: request.cwd,
|
|
564
|
+
...(cursor ? { cursor } : {}),
|
|
565
|
+
}), this.requestTimeoutMs, 'ACP session/list checkpoint validation');
|
|
566
|
+
const session = (response?.sessions || []).find(item => item?.sessionId === request.sessionId);
|
|
567
|
+
if (session) {
|
|
568
|
+
const sessionCwd = path.resolve(String(session.cwd || ''));
|
|
569
|
+
const token = String(session?._meta?.checkpointRevision || session?.checkpointRevision || '');
|
|
570
|
+
return sessionCwd === path.resolve(request.cwd)
|
|
571
|
+
&& token === proof.token
|
|
572
|
+
&& path.resolve(String(proof.cwd || '')) === path.resolve(request.cwd);
|
|
573
|
+
}
|
|
574
|
+
cursor = String(response?.nextCursor || '');
|
|
575
|
+
if (!cursor) return false;
|
|
576
|
+
}
|
|
577
|
+
} catch (error) {
|
|
578
|
+
console.warn('Failed to validate ACP checkpoint against provider session metadata:', error && (error.message || error));
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
420
583
|
async officialConnection(handlers, child) {
|
|
421
584
|
const sdk = await loadAcpSdk();
|
|
422
585
|
const stream = sdk.ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
@@ -473,7 +636,14 @@ class AcpRuntime extends EventEmitter {
|
|
|
473
636
|
if (parentTool) binding.sessionState.touchEntry(parentTool);
|
|
474
637
|
}
|
|
475
638
|
binding.updatedAt = new Date().toISOString();
|
|
476
|
-
|
|
639
|
+
if (isPrimarySession && !binding.historyReplayActive && !binding.promptActive) {
|
|
640
|
+
this.scheduleCheckpoint(binding, { exact: true });
|
|
641
|
+
}
|
|
642
|
+
// A loaded history can contain hundreds of ordered updates. Applying
|
|
643
|
+
// them one by one is necessary, but broadcasting every replay step
|
|
644
|
+
// makes clients repeatedly abort/refetch and remount rich content.
|
|
645
|
+
// prepareAgent emits one complete snapshot after the replay settles.
|
|
646
|
+
if (!binding.historyReplayActive) this.emitSession(binding);
|
|
477
647
|
}
|
|
478
648
|
},
|
|
479
649
|
requestPermission: request => this.requestPermission(binding, request),
|
|
@@ -588,6 +758,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
588
758
|
const binding = this.requireBinding(agentId);
|
|
589
759
|
if (!['idle', 'error'].includes(binding.state)) throw new Error(`ACP Agent is not ready (${binding.state})`);
|
|
590
760
|
const content = Array.isArray(prompt) ? prompt : [{ type: 'text', text: String(prompt || '') }];
|
|
761
|
+
await this.markCheckpointDirty(binding);
|
|
591
762
|
binding.sessionState.beginPrompt(content);
|
|
592
763
|
binding.promptActive = true;
|
|
593
764
|
binding.state = 'working';
|
|
@@ -602,6 +773,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
602
773
|
binding.promptActive = false;
|
|
603
774
|
binding.state = 'idle';
|
|
604
775
|
binding.updatedAt = new Date().toISOString();
|
|
776
|
+
this.scheduleCheckpoint(binding, { exact: true });
|
|
605
777
|
this.emitSession(binding);
|
|
606
778
|
this.emitRuntime(binding);
|
|
607
779
|
return { sessionId: binding.sessionId, stopReason: binding.stopReason };
|
|
@@ -616,6 +788,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
616
788
|
binding.promptActive = false;
|
|
617
789
|
binding.state = 'error';
|
|
618
790
|
binding.error = runtimeError.message;
|
|
791
|
+
this.scheduleCheckpoint(binding, { exact: true });
|
|
619
792
|
this.emitSession(binding);
|
|
620
793
|
this.emitRuntime(binding);
|
|
621
794
|
throw runtimeError;
|
|
@@ -808,7 +981,11 @@ class AcpRuntime extends EventEmitter {
|
|
|
808
981
|
approvalMode: binding.approvalMode,
|
|
809
982
|
...(binding.sessionId ? { sessionId: binding.sessionId } : {}),
|
|
810
983
|
...(revisionBase > 0 ? { revisionBase } : {}),
|
|
984
|
+
...(binding.sessionId ? { historyMode: 'checkpoint' } : {}),
|
|
811
985
|
};
|
|
986
|
+
if (binding.sessionState && !binding.promptActive) {
|
|
987
|
+
await this.writeCheckpoint(binding, { exact: true });
|
|
988
|
+
}
|
|
812
989
|
this.unregisterAgent(agentId);
|
|
813
990
|
return this.prepareAgent(options);
|
|
814
991
|
}
|
|
@@ -862,12 +1039,19 @@ class AcpRuntime extends EventEmitter {
|
|
|
862
1039
|
);
|
|
863
1040
|
binding.sessionState.currentModeId = String(modeId || '');
|
|
864
1041
|
if (binding.modes) binding.modes = { ...binding.modes, currentModeId: binding.sessionState.currentModeId };
|
|
1042
|
+
this.scheduleCheckpoint(binding, { exact: true });
|
|
865
1043
|
this.emitSession(binding);
|
|
866
1044
|
return { sessionId: binding.sessionId, modeId: binding.sessionState.currentModeId };
|
|
867
1045
|
}
|
|
868
1046
|
|
|
869
1047
|
async setSessionConfigOption(agentId, configId, value) {
|
|
870
1048
|
const binding = this.requireBinding(agentId);
|
|
1049
|
+
return this.enqueueSessionConfigMutation(binding, () => (
|
|
1050
|
+
this.setSessionConfigOptionNow(binding, configId, value)
|
|
1051
|
+
));
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async setSessionConfigOptionNow(binding, configId, value) {
|
|
871
1055
|
const option = binding.configOptions?.find(candidate => candidate.id === String(configId || ''));
|
|
872
1056
|
if (
|
|
873
1057
|
binding.provider === 'codex'
|
|
@@ -885,7 +1069,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
885
1069
|
));
|
|
886
1070
|
if (typeof reasoning?.currentValue === 'string' && reasoning.currentValue) {
|
|
887
1071
|
await this.refreshCodexSessionModel(binding, String(value ?? ''), reasoning.currentValue);
|
|
888
|
-
return this.applySessionConfigOption(binding, configId, value);
|
|
1072
|
+
return this.applySessionConfigOption(binding, configId, value, { force: true });
|
|
889
1073
|
}
|
|
890
1074
|
this.emitSession(binding);
|
|
891
1075
|
return { sessionId: binding.sessionId, configOptions: binding.configOptions };
|
|
@@ -895,6 +1079,12 @@ class AcpRuntime extends EventEmitter {
|
|
|
895
1079
|
|
|
896
1080
|
async setSessionConfigOptions(agentId, changes) {
|
|
897
1081
|
const binding = this.requireBinding(agentId);
|
|
1082
|
+
return this.enqueueSessionConfigMutation(binding, () => (
|
|
1083
|
+
this.setSessionConfigOptionsNow(binding, changes)
|
|
1084
|
+
));
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async setSessionConfigOptionsNow(binding, changes) {
|
|
898
1088
|
const normalized = Array.isArray(changes)
|
|
899
1089
|
? changes.filter(change => change && typeof change.configId === 'string' && Object.prototype.hasOwnProperty.call(change, 'value'))
|
|
900
1090
|
: [];
|
|
@@ -926,17 +1116,28 @@ class AcpRuntime extends EventEmitter {
|
|
|
926
1116
|
String(modelChange.value ?? ''),
|
|
927
1117
|
String(currentReasoning?.currentValue || reasoningChange.value || '')
|
|
928
1118
|
);
|
|
929
|
-
response = await this.applySessionConfigOption(binding, modelChange.configId, modelChange.value);
|
|
1119
|
+
response = await this.applySessionConfigOption(binding, modelChange.configId, modelChange.value, { force: true });
|
|
930
1120
|
handled.add(modelChange);
|
|
931
1121
|
handled.add(reasoningChange);
|
|
932
1122
|
}
|
|
933
1123
|
for (const change of normalized) {
|
|
934
1124
|
if (handled.has(change)) continue;
|
|
935
|
-
response = await this.
|
|
1125
|
+
response = await this.setSessionConfigOptionNow(binding, change.configId, change.value);
|
|
936
1126
|
}
|
|
937
1127
|
return response;
|
|
938
1128
|
}
|
|
939
1129
|
|
|
1130
|
+
async enqueueSessionConfigMutation(binding, operation) {
|
|
1131
|
+
const previous = binding.configMutationTail || Promise.resolve();
|
|
1132
|
+
const pending = previous.catch(() => {}).then(operation);
|
|
1133
|
+
binding.configMutationTail = pending;
|
|
1134
|
+
try {
|
|
1135
|
+
return await pending;
|
|
1136
|
+
} finally {
|
|
1137
|
+
if (binding.configMutationTail === pending) binding.configMutationTail = null;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
940
1141
|
async refreshCodexSessionModel(binding, model, effort) {
|
|
941
1142
|
await withTimeout(
|
|
942
1143
|
binding.connection.request(CODEX_SET_SESSION_MODEL_METHOD, {
|
|
@@ -949,16 +1150,31 @@ class AcpRuntime extends EventEmitter {
|
|
|
949
1150
|
}
|
|
950
1151
|
|
|
951
1152
|
async applySessionConfigOption(binding, configId, value, options = {}) {
|
|
1153
|
+
const normalizedConfigId = String(configId || '');
|
|
1154
|
+
const currentOption = binding.configOptions?.find(candidate => candidate.id === normalizedConfigId);
|
|
1155
|
+
if (options.force !== true && currentOption?.currentValue === value) {
|
|
1156
|
+
if (options.emit !== false) this.emitSession(binding);
|
|
1157
|
+
return { sessionId: binding.sessionId, configOptions: binding.configOptions };
|
|
1158
|
+
}
|
|
952
1159
|
const request = typeof value === 'boolean'
|
|
953
|
-
? { sessionId: binding.sessionId, configId:
|
|
954
|
-
: { sessionId: binding.sessionId, configId:
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1160
|
+
? { sessionId: binding.sessionId, configId: normalizedConfigId, type: 'boolean', value }
|
|
1161
|
+
: { sessionId: binding.sessionId, configId: normalizedConfigId, value: String(value ?? '') };
|
|
1162
|
+
let response;
|
|
1163
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1164
|
+
response = await withTimeout(
|
|
1165
|
+
binding.connection.setSessionConfigOption(request),
|
|
1166
|
+
this.requestTimeoutMs,
|
|
1167
|
+
'ACP session/set_config_option'
|
|
1168
|
+
);
|
|
1169
|
+
binding.configOptions = response?.configOptions || binding.configOptions;
|
|
1170
|
+
const confirmed = binding.configOptions?.find(candidate => candidate.id === normalizedConfigId);
|
|
1171
|
+
if (confirmed?.currentValue === request.value) break;
|
|
1172
|
+
if (attempt === 1) {
|
|
1173
|
+
throw new Error(`ACP Agent did not confirm config option ${normalizedConfigId}`);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
961
1176
|
binding.sessionState.configOptions = JSON.parse(JSON.stringify(binding.configOptions));
|
|
1177
|
+
this.scheduleCheckpoint(binding, { exact: true });
|
|
962
1178
|
if (options.emit !== false) this.emitSession(binding);
|
|
963
1179
|
return { sessionId: binding.sessionId, configOptions: binding.configOptions };
|
|
964
1180
|
}
|
|
@@ -1070,6 +1286,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
1070
1286
|
};
|
|
1071
1287
|
binding.sessionState?.touchEntry(entry);
|
|
1072
1288
|
binding.updatedAt = new Date().toISOString();
|
|
1289
|
+
this.scheduleCheckpoint(binding, { exact: true });
|
|
1073
1290
|
this.emitSession(binding);
|
|
1074
1291
|
return { ...result, toolCallId: String(toolCallId || '') };
|
|
1075
1292
|
}
|
|
@@ -1202,6 +1419,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
1202
1419
|
unregisterAgent(agentId) {
|
|
1203
1420
|
const binding = this.bindings.get(agentId);
|
|
1204
1421
|
if (!binding) return;
|
|
1422
|
+
if (binding.sessionState && !binding.promptActive) this.scheduleCheckpoint(binding, { exact: true });
|
|
1205
1423
|
this.bindings.delete(agentId);
|
|
1206
1424
|
for (const resolve of binding.permissionResolvers.values()) resolve({ outcome: { outcome: 'cancelled' } });
|
|
1207
1425
|
for (const resolve of binding.elicitationResolvers.values()) resolve({ action: 'cancel' });
|
|
@@ -1221,8 +1439,13 @@ class AcpRuntime extends EventEmitter {
|
|
|
1221
1439
|
if (binding.child && !binding.child.killed) binding.child.kill('SIGTERM');
|
|
1222
1440
|
}
|
|
1223
1441
|
|
|
1224
|
-
dispose() {
|
|
1442
|
+
async dispose() {
|
|
1443
|
+
for (const binding of this.bindings.values()) {
|
|
1444
|
+
if (binding.sessionState && !binding.promptActive) this.scheduleCheckpoint(binding, { exact: true });
|
|
1445
|
+
}
|
|
1446
|
+
if (this.checkpointStore) await this.checkpointStore.flush();
|
|
1225
1447
|
for (const agentId of [...this.bindings.keys()]) this.unregisterAgent(agentId);
|
|
1448
|
+
if (this.checkpointStore) await this.checkpointStore.dispose();
|
|
1226
1449
|
}
|
|
1227
1450
|
}
|
|
1228
1451
|
|