farming-code 2.2.16 → 2.2.17
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 +2 -2
- package/README.zh_cn.md +2 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/backend/acp-runtime.js +221 -10
- package/backend/agent-manager.js +396 -47
- package/backend/agent-provider-session.js +15 -1
- package/backend/codex-rollout-follower.js +83 -0
- package/backend/codex-session-archive.js +12 -1
- package/backend/codex-session-history.js +94 -0
- package/backend/config-manager.js +3 -3
- package/backend/farming-app-cli.js +8 -0
- package/backend/native-pty-host-client.js +59 -2
- package/backend/provider-adapters.js +117 -36
- package/backend/provider-session-service.js +138 -34
- package/backend/server.js +5 -1
- package/backend/storage-layout.js +5 -0
- package/backend/usage-history-client.js +305 -0
- package/backend/usage-history-scanner.generated.js +2250 -0
- package/backend/usage-history-scanner.ts +2718 -0
- package/backend/usage-history-smoke.js +64 -0
- package/backend/usage-history-worker.js +34 -0
- package/backend/usage-monitor.js +188 -588
- package/backend/vendor/usage-parsers/CC_STATISTICS_LICENSE +21 -0
- package/backend/vendor/usage-parsers/CODEXBAR_LICENSE +21 -0
- package/backend/vendor/usage-parsers/VENDOR.json +45 -0
- package/dist/assets/App-6yWdwzj8.js +210 -0
- package/dist/assets/code-dark-5esj0bRu.css +1 -0
- package/dist/assets/{index-iKBvPjWS.js → index-C8ZFKye1.js} +2 -2
- package/dist/assets/main-DSO3i_Gs.css +1 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +9 -3
- package/package.json +12 -8
- package/shared/backend-connection-status.d.ts +25 -0
- package/shared/backend-connection-status.js +40 -0
- package/dist/assets/App-y6p2yRar.js +0 -206
- package/dist/assets/code-dark-DKBGxoPX.css +0 -1
- package/dist/assets/main-CdkLOXYl.css +0 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<a href="https://github.com/zhuwenzhuang/farming/releases"><img alt="Release" src="https://img.shields.io/github/v/release/zhuwenzhuang/farming?label=release"></a>
|
|
16
16
|
<a href="https://www.npmjs.com/package/farming-code"><img alt="npm" src="https://img.shields.io/npm/v/farming-code?label=npm"></a>
|
|
17
17
|
<a href="./LICENSE"><img alt="License" src="https://img.shields.io/github/license/zhuwenzhuang/farming"></a>
|
|
18
|
-
<img alt="Node.js 22+" src="https://img.shields.io/badge/node
|
|
18
|
+
<img alt="Node.js 22.13 LTS or 24+" src="https://img.shields.io/badge/node-22.13_LTS_%7C_24%2B-339933?logo=nodedotjs&logoColor=white">
|
|
19
19
|
<img alt="macOS and Linux" src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-555">
|
|
20
20
|
</p>
|
|
21
21
|
|
|
@@ -25,7 +25,7 @@ Farming runs on the same development machine as your repositories and coding CLI
|
|
|
25
25
|
|
|
26
26
|
## Quick Start
|
|
27
27
|
|
|
28
|
-
With Node.js 22 or
|
|
28
|
+
With Node.js 22.13 LTS (22.x) or Node.js 24+ and at least one supported coding CLI installed and signed in:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npm install --global farming-code@latest && farming daemon
|
package/README.zh_cn.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<a href="https://github.com/zhuwenzhuang/farming/releases"><img alt="Release" src="https://img.shields.io/github/v/release/zhuwenzhuang/farming?label=release"></a>
|
|
16
16
|
<a href="https://www.npmjs.com/package/farming-code"><img alt="npm" src="https://img.shields.io/npm/v/farming-code?label=npm"></a>
|
|
17
17
|
<a href="./LICENSE"><img alt="License" src="https://img.shields.io/github/license/zhuwenzhuang/farming"></a>
|
|
18
|
-
<img alt="Node.js 22+" src="https://img.shields.io/badge/node
|
|
18
|
+
<img alt="Node.js 22.13 LTS or 24+" src="https://img.shields.io/badge/node-22.13_LTS_%7C_24%2B-339933?logo=nodedotjs&logoColor=white">
|
|
19
19
|
<img alt="macOS and Linux" src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-555">
|
|
20
20
|
</p>
|
|
21
21
|
|
|
@@ -25,7 +25,7 @@ Farming 与代码仓库和 Coding CLI 运行在同一台开发机上。Agent 进
|
|
|
25
25
|
|
|
26
26
|
## 快速开始
|
|
27
27
|
|
|
28
|
-
准备好 Node.js 22
|
|
28
|
+
准备好 Node.js 22.13 LTS(22.x)或 Node.js 24+,以及至少一个已安装、已登录的受支持 Coding CLI:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npm install --global farming-code@latest && farming daemon
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -58,6 +58,22 @@ beside the vendored files at `frontend/vendor/ghostty-web/LICENSE`.
|
|
|
58
58
|
If vendored assets are updated, keep this notice in sync with the package name,
|
|
59
59
|
version, and license.
|
|
60
60
|
|
|
61
|
+
Farming's TypeScript token-history scanner adapts the cumulative Codex
|
|
62
|
+
accounting and copied-prefix classification semantics from
|
|
63
|
+
[`CodexBar`](https://github.com/steipete/CodexBar) 0.45.2 at commit
|
|
64
|
+
`91560ca98e776b96fdf910d4a0423c2f0c07a3b9` (MIT, Copyright (c) 2026 Peter
|
|
65
|
+
Steinberger). It adapts Claude assistant usage extraction, streaming message-id
|
|
66
|
+
deduplication, and cached-input normalization from
|
|
67
|
+
[`cc-statistics`](https://github.com/androidZzT/cc-statistics) 1.1.0 at commit
|
|
68
|
+
`c98be0af52bbc7f09a1f277747744ace48d9e014` (MIT, Copyright (c) 2026
|
|
69
|
+
androidZzT).
|
|
70
|
+
|
|
71
|
+
Farming does not bundle either application's runtime. The adapted TypeScript
|
|
72
|
+
scanner, incremental filesystem checkpoints, SQLite schema, scan budgeting,
|
|
73
|
+
and product result shape are maintained by Farming. Exact source files,
|
|
74
|
+
adaptation scope, revisions, and both MIT licenses are retained under
|
|
75
|
+
`backend/vendor/usage-parsers/`.
|
|
76
|
+
|
|
61
77
|
The CRT skin bundles one font file under `frontend/skins/crt/fonts/`:
|
|
62
78
|
|
|
63
79
|
| Font | License | Purpose |
|
package/backend/acp-runtime.js
CHANGED
|
@@ -2,9 +2,10 @@ const EventEmitter = require('events');
|
|
|
2
2
|
const crypto = require('crypto');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const { spawn } = require('child_process');
|
|
5
|
+
const { execFile, spawn } = require('child_process');
|
|
6
6
|
const { Readable, Writable } = require('stream');
|
|
7
7
|
const { createRequire } = require('module');
|
|
8
|
+
const { promisify } = require('util');
|
|
8
9
|
const packageJson = require('../package.json');
|
|
9
10
|
const { AcpCheckpointStore } = require('./acp-checkpoint-store');
|
|
10
11
|
const { AcpSessionState } = require('./acp-session-state');
|
|
@@ -14,6 +15,7 @@ const { PACKAGED_CODEX_ACP_ARG } = require('./acp/packaged-codex-acp');
|
|
|
14
15
|
const { permissionSecurityWarnings } = require('./acp/permission-security');
|
|
15
16
|
const { rejectPatch } = require('./acp/patch-decisions');
|
|
16
17
|
const { getProviderAdapter, listProviderAdapters } = require('./provider-adapters');
|
|
18
|
+
const { isSafeProviderSessionId } = require('./provider-session-id');
|
|
17
19
|
|
|
18
20
|
const ADAPTER_VERSIONS = Object.freeze(Object.fromEntries(
|
|
19
21
|
listProviderAdapters().filter(adapter => adapter.acp).map(adapter => [adapter.id, adapter.acp.version]),
|
|
@@ -26,6 +28,9 @@ const DEFAULT_CANCEL_TIMEOUT_MS = 15_000;
|
|
|
26
28
|
const DEFAULT_HISTORY_REPLAY_MIN_WAIT_MS = 350;
|
|
27
29
|
const DEFAULT_HISTORY_REPLAY_QUIET_MS = 150;
|
|
28
30
|
const DEFAULT_HISTORY_REPLAY_MAX_WAIT_MS = 5_000;
|
|
31
|
+
const IDENTITY_ADAPTER_GRACEFUL_EXIT_MS = 3_000;
|
|
32
|
+
const IDENTITY_ADAPTER_TERMINATE_MS = 1_000;
|
|
33
|
+
const IDENTITY_ADAPTER_KILL_MS = 1_000;
|
|
29
34
|
const CODEX_SET_SESSION_MODEL_METHOD = 'session/set_model';
|
|
30
35
|
const CODEX_STEER_METHOD = '_codex/session/steer';
|
|
31
36
|
const CODEX_ACP_PACKAGE = '@agentclientprotocol/codex-acp';
|
|
@@ -38,6 +43,7 @@ const CODEX_ACP_VENDOR_ENTRY = path.join(
|
|
|
38
43
|
'acp',
|
|
39
44
|
`codex-acp-${CODEX_ACP_VERSION}.js`,
|
|
40
45
|
);
|
|
46
|
+
const execFileAsync = promisify(execFile);
|
|
41
47
|
|
|
42
48
|
let sdkPromise;
|
|
43
49
|
const runtimeRequire = createRequire(__filename);
|
|
@@ -290,6 +296,81 @@ function isCodexSteerUnavailableError(error) {
|
|
|
290
296
|
|| text.includes('cannot steer a compact turn');
|
|
291
297
|
}
|
|
292
298
|
|
|
299
|
+
function childHasExited(child) {
|
|
300
|
+
return !child
|
|
301
|
+
|| (child.exitCode !== null && child.exitCode !== undefined)
|
|
302
|
+
|| (child.signalCode !== null && child.signalCode !== undefined);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function processGroupHasExited(binding) {
|
|
306
|
+
if (!binding?.ownsProcessGroup || !binding.child?.pid || process.platform === 'win32') {
|
|
307
|
+
return childHasExited(binding?.child);
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
process.kill(-binding.child.pid, 0);
|
|
311
|
+
return false;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
return error?.code === 'ESRCH';
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function waitForProcessTreeExit(binding, timeoutMs) {
|
|
318
|
+
const deadline = Date.now() + timeoutMs;
|
|
319
|
+
while (Date.now() <= deadline) {
|
|
320
|
+
if (processGroupHasExited(binding)) return true;
|
|
321
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
322
|
+
}
|
|
323
|
+
return processGroupHasExited(binding);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function signalProcessTree(binding, signal) {
|
|
327
|
+
if (!binding?.child || processGroupHasExited(binding)) return;
|
|
328
|
+
if (binding.ownsProcessGroup && binding.child.pid && process.platform !== 'win32') {
|
|
329
|
+
try {
|
|
330
|
+
process.kill(-binding.child.pid, signal);
|
|
331
|
+
return;
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (error?.code === 'ESRCH') return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
binding.child.kill(signal);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function deleteProviderSessionIdentity(options = {}) {
|
|
340
|
+
const provider = String(options.provider || '').trim().toLowerCase();
|
|
341
|
+
const sessionId = String(options.sessionId || '').trim();
|
|
342
|
+
if (!isSafeProviderSessionId(sessionId)) {
|
|
343
|
+
throw new Error('Provider session rollback requires a safe exact session id');
|
|
344
|
+
}
|
|
345
|
+
let args;
|
|
346
|
+
if (provider === 'codex') args = ['delete', '--force', sessionId];
|
|
347
|
+
else if (provider === 'opencode') args = ['session', 'delete', sessionId];
|
|
348
|
+
else throw new Error(`${provider || 'Provider'} does not support identity rollback`);
|
|
349
|
+
await execFileAsync(options.executable || getProviderAdapter(provider)?.executable, args, {
|
|
350
|
+
cwd: options.cwd,
|
|
351
|
+
env: options.env,
|
|
352
|
+
timeout: 15_000,
|
|
353
|
+
maxBuffer: 1024 * 1024,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function attachProviderSessionIdentity(error, identity, rollbackError) {
|
|
358
|
+
const target = error instanceof Error ? error : new Error(String(error || 'Provider session identity failed'));
|
|
359
|
+
Object.defineProperty(target, 'providerSessionIdentity', {
|
|
360
|
+
value: identity,
|
|
361
|
+
enumerable: false,
|
|
362
|
+
configurable: true,
|
|
363
|
+
});
|
|
364
|
+
if (rollbackError) {
|
|
365
|
+
Object.defineProperty(target, 'providerSessionRollbackError', {
|
|
366
|
+
value: rollbackError,
|
|
367
|
+
enumerable: false,
|
|
368
|
+
configurable: true,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return target;
|
|
372
|
+
}
|
|
373
|
+
|
|
293
374
|
class AcpRuntime extends EventEmitter {
|
|
294
375
|
constructor(options = {}) {
|
|
295
376
|
super();
|
|
@@ -304,6 +385,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
304
385
|
this.historyReplayMinWaitMs = options.historyReplayMinWaitMs ?? DEFAULT_HISTORY_REPLAY_MIN_WAIT_MS;
|
|
305
386
|
this.historyReplayQuietMs = options.historyReplayQuietMs ?? DEFAULT_HISTORY_REPLAY_QUIET_MS;
|
|
306
387
|
this.historyReplayMaxWaitMs = options.historyReplayMaxWaitMs ?? DEFAULT_HISTORY_REPLAY_MAX_WAIT_MS;
|
|
388
|
+
this.deleteProviderSessionIdentity = options.deleteProviderSessionIdentity || deleteProviderSessionIdentity;
|
|
307
389
|
this.checkpointStore = options.checkpointStore
|
|
308
390
|
|| (options.configDir ? new AcpCheckpointStore(options.configDir, options.checkpointOptions) : null);
|
|
309
391
|
this.bindings = new Map();
|
|
@@ -331,10 +413,12 @@ class AcpRuntime extends EventEmitter {
|
|
|
331
413
|
launch,
|
|
332
414
|
restartOptions: { ...options, agentId, provider },
|
|
333
415
|
approvalMode: options.approvalMode || 'approve',
|
|
416
|
+
ownsProcessGroup: options.identityOnly === true && process.platform !== 'win32',
|
|
334
417
|
child: null,
|
|
335
418
|
connection: null,
|
|
336
419
|
initializeResponse: null,
|
|
337
420
|
sessionId: '',
|
|
421
|
+
untrustedSessionId: '',
|
|
338
422
|
state: 'connecting',
|
|
339
423
|
error: '',
|
|
340
424
|
stopReason: '',
|
|
@@ -366,6 +450,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
366
450
|
cwd: binding.cwd,
|
|
367
451
|
env: binding.env,
|
|
368
452
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
453
|
+
detached: binding.ownsProcessGroup,
|
|
369
454
|
});
|
|
370
455
|
binding.child = child;
|
|
371
456
|
child.stderr.on('data', chunk => {
|
|
@@ -512,8 +597,12 @@ class AcpRuntime extends EventEmitter {
|
|
|
512
597
|
this.sessionSetupTimeoutMs,
|
|
513
598
|
'ACP session/new'
|
|
514
599
|
);
|
|
515
|
-
|
|
516
|
-
if (!
|
|
600
|
+
const returnedSessionId = String(sessionResponse.sessionId || '').trim();
|
|
601
|
+
if (!isSafeProviderSessionId(returnedSessionId)) {
|
|
602
|
+
binding.untrustedSessionId = returnedSessionId;
|
|
603
|
+
throw new Error('ACP session/new returned an invalid resumable session id');
|
|
604
|
+
}
|
|
605
|
+
binding.sessionId = returnedSessionId;
|
|
517
606
|
binding.sessionState = new AcpSessionState({ provider, sessionId: binding.sessionId, cwd: binding.cwd, maxUpdates: this.maxUpdates });
|
|
518
607
|
}
|
|
519
608
|
binding.modes = sessionResponse?.modes || null;
|
|
@@ -546,11 +635,109 @@ class AcpRuntime extends EventEmitter {
|
|
|
546
635
|
} catch (error) {
|
|
547
636
|
const runtimeError = new Error(acpErrorMessage(error), { cause: error });
|
|
548
637
|
this.handleExit(binding, runtimeError);
|
|
549
|
-
|
|
638
|
+
if (options.identityOnly === true) {
|
|
639
|
+
const returnedIdentity = binding.sessionId || binding.untrustedSessionId;
|
|
640
|
+
const identity = returnedIdentity
|
|
641
|
+
? {
|
|
642
|
+
provider,
|
|
643
|
+
executable: options.executable || getProviderAdapter(provider)?.executable,
|
|
644
|
+
env: binding.env,
|
|
645
|
+
cwd: binding.cwd,
|
|
646
|
+
sessionId: returnedIdentity,
|
|
647
|
+
producerStopped: false,
|
|
648
|
+
}
|
|
649
|
+
: null;
|
|
650
|
+
let producerStopped = false;
|
|
651
|
+
try {
|
|
652
|
+
await this.unregisterAgentAndWait(agentId);
|
|
653
|
+
producerStopped = true;
|
|
654
|
+
if (identity) identity.producerStopped = true;
|
|
655
|
+
} catch (cleanupError) {
|
|
656
|
+
Object.defineProperty(runtimeError, 'adapterCleanupError', {
|
|
657
|
+
value: cleanupError,
|
|
658
|
+
enumerable: false,
|
|
659
|
+
configurable: true,
|
|
660
|
+
});
|
|
661
|
+
if (identity) attachProviderSessionIdentity(runtimeError, identity);
|
|
662
|
+
}
|
|
663
|
+
if (identity && producerStopped && isSafeProviderSessionId(identity.sessionId)) {
|
|
664
|
+
try {
|
|
665
|
+
await this.deleteProviderSessionIdentity(identity);
|
|
666
|
+
} catch (rollbackError) {
|
|
667
|
+
attachProviderSessionIdentity(runtimeError, identity, rollbackError);
|
|
668
|
+
}
|
|
669
|
+
} else if (identity && producerStopped) {
|
|
670
|
+
attachProviderSessionIdentity(runtimeError, identity);
|
|
671
|
+
}
|
|
672
|
+
} else {
|
|
673
|
+
this.unregisterAgent(agentId);
|
|
674
|
+
}
|
|
550
675
|
throw runtimeError;
|
|
551
676
|
}
|
|
552
677
|
}
|
|
553
678
|
|
|
679
|
+
async createSessionIdentity(options = {}) {
|
|
680
|
+
const agentId = `provider-session-${crypto.randomUUID()}`;
|
|
681
|
+
let prepared = null;
|
|
682
|
+
let result = null;
|
|
683
|
+
let failure = null;
|
|
684
|
+
let identity = null;
|
|
685
|
+
try {
|
|
686
|
+
prepared = await this.prepareAgent({
|
|
687
|
+
...options,
|
|
688
|
+
agentId,
|
|
689
|
+
identityOnly: true,
|
|
690
|
+
sessionId: '',
|
|
691
|
+
});
|
|
692
|
+
const binding = this.requireBinding(agentId);
|
|
693
|
+
identity = {
|
|
694
|
+
provider: String(options.provider || '').trim().toLowerCase(),
|
|
695
|
+
executable: options.executable || getProviderAdapter(options.provider)?.executable,
|
|
696
|
+
env: binding.env,
|
|
697
|
+
cwd: binding.cwd,
|
|
698
|
+
sessionId: prepared.sessionId,
|
|
699
|
+
producerStopped: false,
|
|
700
|
+
};
|
|
701
|
+
result = {
|
|
702
|
+
...prepared,
|
|
703
|
+
sessionRequestOptions: this.getSessionRequestOptions(agentId),
|
|
704
|
+
};
|
|
705
|
+
} catch (error) {
|
|
706
|
+
failure = error instanceof Error ? error : new Error(String(error || 'Provider session identity failed'));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (prepared) {
|
|
710
|
+
let producerStopped = false;
|
|
711
|
+
try {
|
|
712
|
+
await this.unregisterAgentAndWait(agentId);
|
|
713
|
+
producerStopped = true;
|
|
714
|
+
if (identity) identity.producerStopped = true;
|
|
715
|
+
} catch (cleanupError) {
|
|
716
|
+
if (!failure) {
|
|
717
|
+
failure = cleanupError instanceof Error
|
|
718
|
+
? cleanupError
|
|
719
|
+
: new Error(String(cleanupError || 'ACP identity adapter cleanup failed'));
|
|
720
|
+
} else {
|
|
721
|
+
Object.defineProperty(failure, 'adapterCleanupError', {
|
|
722
|
+
value: cleanupError,
|
|
723
|
+
enumerable: false,
|
|
724
|
+
configurable: true,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
if (identity) failure = attachProviderSessionIdentity(failure, identity);
|
|
728
|
+
}
|
|
729
|
+
if (failure && identity?.sessionId && producerStopped) {
|
|
730
|
+
try {
|
|
731
|
+
await this.deleteProviderSessionIdentity(identity);
|
|
732
|
+
} catch (rollbackError) {
|
|
733
|
+
failure = attachProviderSessionIdentity(failure, identity, rollbackError);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (failure) throw failure;
|
|
738
|
+
return result;
|
|
739
|
+
}
|
|
740
|
+
|
|
554
741
|
checkpointIdentity(binding, sessionId = binding?.sessionId) {
|
|
555
742
|
if (!binding || !sessionId) return null;
|
|
556
743
|
return {
|
|
@@ -1589,11 +1776,10 @@ class AcpRuntime extends EventEmitter {
|
|
|
1589
1776
|
this.emitRuntime(binding);
|
|
1590
1777
|
}
|
|
1591
1778
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
if (!binding) return;
|
|
1779
|
+
detachAgentBinding(binding) {
|
|
1780
|
+
if (!binding || this.bindings.get(binding.agentId) !== binding) return false;
|
|
1595
1781
|
if (binding.sessionState && !binding.promptActive) this.scheduleCheckpoint(binding, { exact: true });
|
|
1596
|
-
this.bindings.delete(agentId);
|
|
1782
|
+
this.bindings.delete(binding.agentId);
|
|
1597
1783
|
for (const resolve of binding.permissionResolvers.values()) resolve({ outcome: { outcome: 'cancelled' } });
|
|
1598
1784
|
for (const resolve of binding.elicitationResolvers.values()) resolve({ action: 'cancel' });
|
|
1599
1785
|
binding.permissionResolvers.clear();
|
|
@@ -1607,9 +1793,33 @@ class AcpRuntime extends EventEmitter {
|
|
|
1607
1793
|
try {
|
|
1608
1794
|
binding.connection?.close();
|
|
1609
1795
|
} catch {
|
|
1610
|
-
//
|
|
1796
|
+
// Process cleanup below remains authoritative if transport cleanup races.
|
|
1797
|
+
}
|
|
1798
|
+
if (binding.child?.stdin?.writable && !binding.child.stdin.destroyed) {
|
|
1799
|
+
try {
|
|
1800
|
+
binding.child.stdin.end();
|
|
1801
|
+
} catch {
|
|
1802
|
+
// Process cleanup below remains authoritative if stdin already closed.
|
|
1803
|
+
}
|
|
1611
1804
|
}
|
|
1612
|
-
|
|
1805
|
+
return true;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
unregisterAgent(agentId) {
|
|
1809
|
+
const binding = this.bindings.get(agentId);
|
|
1810
|
+
if (!binding || !this.detachAgentBinding(binding)) return;
|
|
1811
|
+
signalProcessTree(binding, 'SIGTERM');
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
async unregisterAgentAndWait(agentId) {
|
|
1815
|
+
const binding = this.bindings.get(agentId);
|
|
1816
|
+
if (!binding || !this.detachAgentBinding(binding)) return;
|
|
1817
|
+
if (await waitForProcessTreeExit(binding, IDENTITY_ADAPTER_GRACEFUL_EXIT_MS)) return;
|
|
1818
|
+
signalProcessTree(binding, 'SIGTERM');
|
|
1819
|
+
if (await waitForProcessTreeExit(binding, IDENTITY_ADAPTER_TERMINATE_MS)) return;
|
|
1820
|
+
signalProcessTree(binding, 'SIGKILL');
|
|
1821
|
+
if (await waitForProcessTreeExit(binding, IDENTITY_ADAPTER_KILL_MS)) return;
|
|
1822
|
+
throw new Error(`ACP identity adapter process tree ${binding.child?.pid || ''} did not exit`);
|
|
1613
1823
|
}
|
|
1614
1824
|
|
|
1615
1825
|
async dispose() {
|
|
@@ -1634,4 +1844,5 @@ module.exports = {
|
|
|
1634
1844
|
supportsCodexSteer,
|
|
1635
1845
|
isCodexSteerUnavailableError,
|
|
1636
1846
|
CODEX_STEER_METHOD,
|
|
1847
|
+
deleteProviderSessionIdentity,
|
|
1637
1848
|
};
|