@wrongstack/acp 0.305.0 → 0.306.0
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/client/terminal-server.d.ts +33 -1
- package/dist/client.js +62 -4
- package/dist/index.js +62 -4
- package/package.json +2 -2
|
@@ -18,6 +18,12 @@ export interface TerminalServerOptions {
|
|
|
18
18
|
}
|
|
19
19
|
export declare class TerminalServer {
|
|
20
20
|
private readonly terminals;
|
|
21
|
+
/**
|
|
22
|
+
* Stable per-instance identifier for debug logs. 8 hex chars is enough
|
|
23
|
+
* to disambiguate concurrent TerminalServers in a trace; not meant to
|
|
24
|
+
* be cryptographically unique.
|
|
25
|
+
*/
|
|
26
|
+
private readonly instanceId;
|
|
21
27
|
private readonly projectRoot;
|
|
22
28
|
private readonly commandTimeoutMs;
|
|
23
29
|
private readonly outputByteLimit;
|
|
@@ -25,6 +31,7 @@ export declare class TerminalServer {
|
|
|
25
31
|
private readonly maxTerminals;
|
|
26
32
|
private readonly abortSignal;
|
|
27
33
|
private readonly abortHandler;
|
|
34
|
+
private disposed;
|
|
28
35
|
private nextId;
|
|
29
36
|
constructor(opts: TerminalServerOptions);
|
|
30
37
|
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
@@ -63,7 +70,32 @@ export declare class TerminalServer {
|
|
|
63
70
|
kill(terminalId: string): void;
|
|
64
71
|
/** Kill the process if alive and remove the record. */
|
|
65
72
|
release(terminalId: string): void;
|
|
66
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Release all resources held by this server: kill every active terminal
|
|
75
|
+
* and detach the host `AbortSignal` listener.
|
|
76
|
+
*
|
|
77
|
+
* Idempotent — calling it multiple times is safe. Required because the
|
|
78
|
+
* previously-coded `releaseAll()` was the only path that removed the
|
|
79
|
+
* abort listener: if the host never called it (unhandled error path,
|
|
80
|
+
* host crash, GC of the session without explicit close), the listener
|
|
81
|
+
* pinned `this` (terminals Map, output buffers) for the lifetime of the
|
|
82
|
+
* signal. With `dispose()` this is no longer leak-prone.
|
|
83
|
+
*
|
|
84
|
+
* Also exposed as `[Symbol.dispose]` for `using` blocks in Node ≥ 22.
|
|
85
|
+
*
|
|
86
|
+
* RAM-leak audit 2026-08-11, MEDIUM (Finding 2).
|
|
87
|
+
*/
|
|
88
|
+
dispose(): void;
|
|
89
|
+
/** Alias for `dispose()` — enables `using new TerminalServer(...)`. */
|
|
90
|
+
[Symbol.dispose](): void;
|
|
91
|
+
/**
|
|
92
|
+
* Kill all active terminals. Used on session close.
|
|
93
|
+
*
|
|
94
|
+
* @deprecated Prefer `dispose()` (or `using { … }` via `Symbol.dispose`).
|
|
95
|
+
* `releaseAll` is retained as a delegated wrapper for callers that still
|
|
96
|
+
* reference it; new code should call `dispose()` directly so the
|
|
97
|
+
* host-signal listener is removed unconditionally.
|
|
98
|
+
*/
|
|
67
99
|
releaseAll(): void;
|
|
68
100
|
private resolveCwd;
|
|
69
101
|
private buildEnv;
|
package/dist/client.js
CHANGED
|
@@ -515,26 +515,36 @@ function makePermissionPolicy(decide) {
|
|
|
515
515
|
|
|
516
516
|
// src/client/terminal-server.ts
|
|
517
517
|
import { spawn } from "node:child_process";
|
|
518
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
518
519
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
519
520
|
import * as path2 from "node:path";
|
|
520
521
|
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
521
522
|
import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
|
|
522
523
|
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
524
|
+
var DEBUG_DISPOSE = typeof process !== "undefined" && !!process.env?.WRONGSTACK_DEBUG && process.env.WRONGSTACK_DEBUG !== "0" && process.env.WRONGSTACK_DEBUG !== "false";
|
|
523
525
|
var TerminalServer = class {
|
|
524
526
|
terminals = /* @__PURE__ */ new Map();
|
|
527
|
+
/**
|
|
528
|
+
* Stable per-instance identifier for debug logs. 8 hex chars is enough
|
|
529
|
+
* to disambiguate concurrent TerminalServers in a trace; not meant to
|
|
530
|
+
* be cryptographically unique.
|
|
531
|
+
*/
|
|
532
|
+
instanceId;
|
|
525
533
|
projectRoot;
|
|
526
534
|
commandTimeoutMs;
|
|
527
535
|
outputByteLimit;
|
|
528
536
|
maxOutputByteLimit;
|
|
529
537
|
maxTerminals;
|
|
530
538
|
abortSignal;
|
|
531
|
-
abortHandler = () => this.
|
|
539
|
+
abortHandler = () => this.dispose();
|
|
540
|
+
disposed = false;
|
|
532
541
|
nextId = 1;
|
|
533
542
|
constructor(opts) {
|
|
534
543
|
this.projectRoot = path2.resolve(opts.projectRoot);
|
|
535
544
|
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
536
545
|
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
537
546
|
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
547
|
+
this.instanceId = `term_srv_${randomBytes2(4).toString("hex")}`;
|
|
538
548
|
this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
|
|
539
549
|
this.abortSignal = opts.signal;
|
|
540
550
|
if (opts.signal) {
|
|
@@ -543,6 +553,11 @@ var TerminalServer = class {
|
|
|
543
553
|
}
|
|
544
554
|
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
545
555
|
create(params) {
|
|
556
|
+
if (this.disposed) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
"TerminalServer is disposed \u2014 create a new TerminalServer instead of reusing this one"
|
|
559
|
+
);
|
|
560
|
+
}
|
|
546
561
|
if (this.terminals.size >= this.maxTerminals) {
|
|
547
562
|
throw new Error(
|
|
548
563
|
`terminal limit reached (${this.maxTerminals}); release an existing terminal before creating another`
|
|
@@ -697,13 +712,56 @@ var TerminalServer = class {
|
|
|
697
712
|
treeKill2(state.proc, { force: true });
|
|
698
713
|
this.terminals.delete(terminalId);
|
|
699
714
|
}
|
|
700
|
-
/**
|
|
701
|
-
|
|
715
|
+
/**
|
|
716
|
+
* Release all resources held by this server: kill every active terminal
|
|
717
|
+
* and detach the host `AbortSignal` listener.
|
|
718
|
+
*
|
|
719
|
+
* Idempotent — calling it multiple times is safe. Required because the
|
|
720
|
+
* previously-coded `releaseAll()` was the only path that removed the
|
|
721
|
+
* abort listener: if the host never called it (unhandled error path,
|
|
722
|
+
* host crash, GC of the session without explicit close), the listener
|
|
723
|
+
* pinned `this` (terminals Map, output buffers) for the lifetime of the
|
|
724
|
+
* signal. With `dispose()` this is no longer leak-prone.
|
|
725
|
+
*
|
|
726
|
+
* Also exposed as `[Symbol.dispose]` for `using` blocks in Node ≥ 22.
|
|
727
|
+
*
|
|
728
|
+
* RAM-leak audit 2026-08-11, MEDIUM (Finding 2).
|
|
729
|
+
*/
|
|
730
|
+
dispose() {
|
|
731
|
+
if (this.disposed) return;
|
|
732
|
+
if (DEBUG_DISPOSE) {
|
|
733
|
+
const activeChildren = this.terminals.size;
|
|
734
|
+
console.debug(
|
|
735
|
+
JSON.stringify({
|
|
736
|
+
event: "terminal_server.disposed",
|
|
737
|
+
instanceId: this.instanceId,
|
|
738
|
+
activeChildren,
|
|
739
|
+
hadSignal: this.abortSignal !== void 0,
|
|
740
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
741
|
+
})
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
this.disposed = true;
|
|
702
745
|
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
703
746
|
for (const id of [...this.terminals.keys()]) {
|
|
704
747
|
this.release(id);
|
|
705
748
|
}
|
|
706
749
|
}
|
|
750
|
+
/** Alias for `dispose()` — enables `using new TerminalServer(...)`. */
|
|
751
|
+
[Symbol.dispose]() {
|
|
752
|
+
this.dispose();
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Kill all active terminals. Used on session close.
|
|
756
|
+
*
|
|
757
|
+
* @deprecated Prefer `dispose()` (or `using { … }` via `Symbol.dispose`).
|
|
758
|
+
* `releaseAll` is retained as a delegated wrapper for callers that still
|
|
759
|
+
* reference it; new code should call `dispose()` directly so the
|
|
760
|
+
* host-signal listener is removed unconditionally.
|
|
761
|
+
*/
|
|
762
|
+
releaseAll() {
|
|
763
|
+
this.dispose();
|
|
764
|
+
}
|
|
707
765
|
resolveCwd(cwd) {
|
|
708
766
|
if (!cwd) return this.projectRoot;
|
|
709
767
|
const resolved = path2.resolve(cwd);
|
|
@@ -1966,7 +2024,7 @@ var ACPSession = class _ACPSession {
|
|
|
1966
2024
|
this.callbackAbort.abort();
|
|
1967
2025
|
this.promptCallbackAbort?.abort();
|
|
1968
2026
|
this.promptCallbackAbort = null;
|
|
1969
|
-
this.terminalServer.
|
|
2027
|
+
this.terminalServer.dispose();
|
|
1970
2028
|
if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
|
|
1971
2029
|
try {
|
|
1972
2030
|
await this.closeSession();
|
package/dist/index.js
CHANGED
|
@@ -1870,26 +1870,36 @@ function makePermissionPolicy(decide) {
|
|
|
1870
1870
|
|
|
1871
1871
|
// src/client/terminal-server.ts
|
|
1872
1872
|
import { spawn } from "node:child_process";
|
|
1873
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1873
1874
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
1874
1875
|
import * as path3 from "node:path";
|
|
1875
1876
|
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
1876
1877
|
import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
|
|
1877
1878
|
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
1879
|
+
var DEBUG_DISPOSE = typeof process !== "undefined" && !!process.env?.WRONGSTACK_DEBUG && process.env.WRONGSTACK_DEBUG !== "0" && process.env.WRONGSTACK_DEBUG !== "false";
|
|
1878
1880
|
var TerminalServer = class {
|
|
1879
1881
|
terminals = /* @__PURE__ */ new Map();
|
|
1882
|
+
/**
|
|
1883
|
+
* Stable per-instance identifier for debug logs. 8 hex chars is enough
|
|
1884
|
+
* to disambiguate concurrent TerminalServers in a trace; not meant to
|
|
1885
|
+
* be cryptographically unique.
|
|
1886
|
+
*/
|
|
1887
|
+
instanceId;
|
|
1880
1888
|
projectRoot;
|
|
1881
1889
|
commandTimeoutMs;
|
|
1882
1890
|
outputByteLimit;
|
|
1883
1891
|
maxOutputByteLimit;
|
|
1884
1892
|
maxTerminals;
|
|
1885
1893
|
abortSignal;
|
|
1886
|
-
abortHandler = () => this.
|
|
1894
|
+
abortHandler = () => this.dispose();
|
|
1895
|
+
disposed = false;
|
|
1887
1896
|
nextId = 1;
|
|
1888
1897
|
constructor(opts) {
|
|
1889
1898
|
this.projectRoot = path3.resolve(opts.projectRoot);
|
|
1890
1899
|
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
1891
1900
|
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
1892
1901
|
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
1902
|
+
this.instanceId = `term_srv_${randomBytes2(4).toString("hex")}`;
|
|
1893
1903
|
this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
|
|
1894
1904
|
this.abortSignal = opts.signal;
|
|
1895
1905
|
if (opts.signal) {
|
|
@@ -1898,6 +1908,11 @@ var TerminalServer = class {
|
|
|
1898
1908
|
}
|
|
1899
1909
|
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
1900
1910
|
create(params) {
|
|
1911
|
+
if (this.disposed) {
|
|
1912
|
+
throw new Error(
|
|
1913
|
+
"TerminalServer is disposed \u2014 create a new TerminalServer instead of reusing this one"
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1901
1916
|
if (this.terminals.size >= this.maxTerminals) {
|
|
1902
1917
|
throw new Error(
|
|
1903
1918
|
`terminal limit reached (${this.maxTerminals}); release an existing terminal before creating another`
|
|
@@ -2052,13 +2067,56 @@ var TerminalServer = class {
|
|
|
2052
2067
|
treeKill2(state.proc, { force: true });
|
|
2053
2068
|
this.terminals.delete(terminalId);
|
|
2054
2069
|
}
|
|
2055
|
-
/**
|
|
2056
|
-
|
|
2070
|
+
/**
|
|
2071
|
+
* Release all resources held by this server: kill every active terminal
|
|
2072
|
+
* and detach the host `AbortSignal` listener.
|
|
2073
|
+
*
|
|
2074
|
+
* Idempotent — calling it multiple times is safe. Required because the
|
|
2075
|
+
* previously-coded `releaseAll()` was the only path that removed the
|
|
2076
|
+
* abort listener: if the host never called it (unhandled error path,
|
|
2077
|
+
* host crash, GC of the session without explicit close), the listener
|
|
2078
|
+
* pinned `this` (terminals Map, output buffers) for the lifetime of the
|
|
2079
|
+
* signal. With `dispose()` this is no longer leak-prone.
|
|
2080
|
+
*
|
|
2081
|
+
* Also exposed as `[Symbol.dispose]` for `using` blocks in Node ≥ 22.
|
|
2082
|
+
*
|
|
2083
|
+
* RAM-leak audit 2026-08-11, MEDIUM (Finding 2).
|
|
2084
|
+
*/
|
|
2085
|
+
dispose() {
|
|
2086
|
+
if (this.disposed) return;
|
|
2087
|
+
if (DEBUG_DISPOSE) {
|
|
2088
|
+
const activeChildren = this.terminals.size;
|
|
2089
|
+
console.debug(
|
|
2090
|
+
JSON.stringify({
|
|
2091
|
+
event: "terminal_server.disposed",
|
|
2092
|
+
instanceId: this.instanceId,
|
|
2093
|
+
activeChildren,
|
|
2094
|
+
hadSignal: this.abortSignal !== void 0,
|
|
2095
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2096
|
+
})
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
this.disposed = true;
|
|
2057
2100
|
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
2058
2101
|
for (const id of [...this.terminals.keys()]) {
|
|
2059
2102
|
this.release(id);
|
|
2060
2103
|
}
|
|
2061
2104
|
}
|
|
2105
|
+
/** Alias for `dispose()` — enables `using new TerminalServer(...)`. */
|
|
2106
|
+
[Symbol.dispose]() {
|
|
2107
|
+
this.dispose();
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Kill all active terminals. Used on session close.
|
|
2111
|
+
*
|
|
2112
|
+
* @deprecated Prefer `dispose()` (or `using { … }` via `Symbol.dispose`).
|
|
2113
|
+
* `releaseAll` is retained as a delegated wrapper for callers that still
|
|
2114
|
+
* reference it; new code should call `dispose()` directly so the
|
|
2115
|
+
* host-signal listener is removed unconditionally.
|
|
2116
|
+
*/
|
|
2117
|
+
releaseAll() {
|
|
2118
|
+
this.dispose();
|
|
2119
|
+
}
|
|
2062
2120
|
resolveCwd(cwd) {
|
|
2063
2121
|
if (!cwd) return this.projectRoot;
|
|
2064
2122
|
const resolved = path3.resolve(cwd);
|
|
@@ -3321,7 +3379,7 @@ var ACPSession = class _ACPSession {
|
|
|
3321
3379
|
this.callbackAbort.abort();
|
|
3322
3380
|
this.promptCallbackAbort?.abort();
|
|
3323
3381
|
this.promptCallbackAbort = null;
|
|
3324
|
-
this.terminalServer.
|
|
3382
|
+
this.terminalServer.dispose();
|
|
3325
3383
|
if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
|
|
3326
3384
|
try {
|
|
3327
3385
|
await this.closeSession();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/acp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.306.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "ACP (Agent Client Protocol) integration for WrongStack — client + agent support",
|
|
6
6
|
"keywords": [
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@agentclientprotocol/sdk": "^1.3.0",
|
|
55
|
-
"@wrongstack/core": "0.
|
|
55
|
+
"@wrongstack/core": "0.306.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/node": "^26.1.2",
|