@namzu/sdk 14.0.3 → 14.0.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,102 @@
1
1
  # Changelog
2
2
 
3
+ ## 14.0.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 9ee23f1: Stop the package README naming two providers when seven ship
8
+
9
+ "Provider abstraction. OpenRouter and AWS Bedrock today" has been wrong for a
10
+ long time. Seven driver packages ship — and a reader deciding whether this
11
+ kernel can talk to the service they already pay for was being told, on the
12
+ registry page, that it probably cannot.
13
+
14
+ That is the expensive direction for this particular sentence to be wrong in:
15
+ it does not cause a bug, it causes someone to close the tab.
16
+
17
+ Also removed the third-party product names from "What Namzu Is Not". That
18
+ section explained namzu's scope by listing other people's products, which is
19
+ the one thing this repository's own naming rule refuses — a design explained
20
+ by reference to somebody else's has borrowed its shape, and the borrowing
21
+ outlives the sentence. The scope boundaries are unchanged and now stated as
22
+ categories: no front-end framework bindings, no web-framework or edge-runtime
23
+ plumbing, no embedded vector engine.
24
+
25
+ Prose only. No runtime change.
26
+
27
+ ## 14.0.5
28
+
29
+ ### Patch Changes
30
+
31
+ - a2a7aed: `LocalTaskGateway` stops remembering every task it ever launched.
32
+
33
+ `trackedTaskIds` and `settledHandles` had `add` and `set` and no removal
34
+ anywhere in the file. The comment above them said "bounded by the number the
35
+ gateway itself launched", which is true and is not a bound: a gateway built per
36
+ run is bounded by that run, but `SupervisorAgentConfig.gateway` lets a host
37
+ supply its own, and a long-lived host reusing one accumulates an id and a
38
+ settled handle for every task it ever launched, for the life of the process.
39
+
40
+ Both are now capped at 1000 and evicted oldest-first. The cap is far above any
41
+ realistic single run — a fan-out is eight, a long supervisory run is dozens —
42
+ so the listing a supervisor reads at the end of its run is unchanged.
43
+
44
+ The two are evicted **together**. Dropping a tracked id while keeping its
45
+ handle would leave memory nothing can reach, since `listTasks` walks the ids;
46
+ dropping a handle while keeping its id would make a task that ran read as one
47
+ that never launched, which is the exact defect the settled-handle map exists to
48
+ fix. Removing either half of the paired delete fails a test.
49
+
50
+ - 688d6af: Stop the package README promising filesystem isolation the sandbox refuses to claim
51
+
52
+ The README that ships inside this tarball — and therefore the page on the
53
+ registry — said tools run "inside an OS-enforced jail with deny-default file
54
+ I/O", and attributed that to both sandbox tiers.
55
+
56
+ `src/sandbox/isolation.ts` says otherwise, and says it deliberately. The
57
+ namespace tier reports `filesystem: false`, because it unshares the mount
58
+ namespace and never remounts anything, so the child still sees the whole host
59
+ filesystem. The comment beside that table is explicit that claiming otherwise
60
+ would reintroduce the exact defect the table exists to end.
61
+
62
+ So the code was careful and the README was not, in the one direction that
63
+ costs something. An overclaimed security control is worse than an absent one:
64
+ a reader who believes a boundary is there stops looking for one, and this
65
+ sentence was reachable from the registry page by anyone deciding whether it
66
+ was safe to run untrusted input through a tool call.
67
+
68
+ Nothing about the runtime changes. What changes is that the README now
69
+ reproduces the isolation table per tier, names the tier that does **not**
70
+ enforce filesystem isolation and why, and points at `assertIsolation`,
71
+ `isolationOf`, `missingIsolation` and `describeIsolation` — which have always
72
+ been exported, and which refuse a run whose required control the host cannot
73
+ supply rather than quietly running it at a weaker tier.
74
+
75
+ No action required on upgrade. If you read that sentence and concluded your
76
+ tool calls were filesystem-confined on a namespace host, they were not — call
77
+ `assertIsolation` with the controls you actually require, and it will refuse
78
+ rather than pretend.
79
+
80
+ ## 14.0.4
81
+
82
+ ### Patch Changes
83
+
84
+ - b4a3fa7: `StdioTransport.close()` resolves when the child is gone, not when the signal
85
+ was sent.
86
+
87
+ It called `kill('SIGTERM')` and returned. `kill()` returns as soon as the
88
+ signal is delivered, so an awaited `close()` meant "SIGTERM is on its way", and
89
+ a caller that closed a transport and then deleted the child's working directory
90
+ raced the exit — reported as `EBUSY` from a real integration, not inferred. A
91
+ close that does not mean closed makes every teardown built on it a guess, and
92
+ the guess is only wrong sometimes.
93
+
94
+ `close()` now waits for the child's `exit`. A child ignoring SIGTERM is sent
95
+ SIGKILL after two seconds, and a second timer gives up waiting, so `close()`
96
+ cannot hang; both timers are unreferenced, so neither holds the event loop
97
+ open. A spawn that never produced a process emits `error` and no `exit`, and
98
+ that path settles too rather than waiting for an event that is not coming.
99
+
3
100
  ## 14.0.3
4
101
 
5
102
  ### Patch Changes
package/README.md CHANGED
@@ -14,9 +14,9 @@
14
14
 
15
15
  Most "agent frameworks" today are really application frameworks. They ship chat UIs, picking UI layouts, batteries-included hosted dashboards, vendor-specific fast paths, and integration drivers for a handful of databases. You get something you can demo in an hour, and three months later you own a stack where the same framework dictates your frontend, your database, your observability, and your model vendor.
16
16
 
17
- We think agent software should be layered like Unix. At the bottom there needs to be a **kernel**: something to isolate processes, schedule tool calls, manage memory pressure, propagate signals across a call tree, persist checkpoints so a run can resume after a crash, mediate inter-process communication, and produce an auditable event stream. Above the kernel there is user space — shells, editors, IDEs, voice gateways, React apps. The kernel does not care which shell you pick; the shell cannot break the isolation the kernel provides.
17
+ We think agent software should be layered the way an operating system is. At the bottom there needs to be a **kernel**: something to isolate processes, schedule tool calls, manage memory pressure, propagate signals across a call tree, persist checkpoints so a run can resume after a crash, mediate inter-process communication, and produce an auditable event stream. Above the kernel there is user space — shells, editors, IDEs, voice gateways, web front-ends. The kernel does not care which shell you pick; the shell cannot break the isolation the kernel provides.
18
18
 
19
- **Namzu is the kernel.** It runs agents the way Unix runs processes. It does not render UI, it does not pick your database, it does not favor one LLM vendor. It gives you a surface — typed, versioned, documented — that any UI, any storage backend, and any model can plug into. The surface is small and stable; the guts underneath are deep.
19
+ **Namzu is the kernel.** It runs agents the way an operating system runs processes. It does not render UI, it does not pick your database, it does not favor one LLM vendor. It gives you a surface — typed, versioned, documented — that any UI, any storage backend, and any model can plug into. The surface is small and stable; the guts underneath are deep.
20
20
 
21
21
  ---
22
22
 
@@ -24,7 +24,7 @@ We think agent software should be layered like Unix. At the bottom there needs t
24
24
 
25
25
  Namzu is a single-process TypeScript kernel with the following responsibilities:
26
26
 
27
- - **Process execution and isolation.** Tools run inside OS-level sandboxes: Seatbelt (SBPL) on macOS, mount + PID namespaces on Linux. Deny-default file I/O, scoped network access, enforced resource limits. No Docker, no container runtime, no daemon, no sidecar.
27
+ - **Process execution and isolation.** Tools run outside the host process, under OS-level containment whose enforced controls vary by tier and the kernel states per tier which of filesystem, network and process isolation it actually enforces, refusing to start a run whose required control the host cannot supply rather than silently downgrading it. No container runtime, no daemon, no sidecar. See [The Boundary](#1-the-boundary-sandbox-sandbox) for the table.
28
28
  - **Agent lifecycle.** Parent/child agent spawn with depth tracking, budget splitting, and causal trace linkage. A supervisor can fork a subtree of agents and get their results back, with each child isolated from its siblings.
29
29
  - **Scheduling.** Per-run token, cost, wall-clock, and iteration budgets. Limit checker, task router (cheap model for compaction, expensive for coding), tool tiering (LLM learns to prefer cheaper tools first).
30
30
  - **Signals.** `AbortController` tree spanning parent and children. `cancel(taskId)` and `cancelAll(parentRunId)` propagate. Runs can be paused and resumed, aborted cleanly, and emit lifecycle events for every transition.
@@ -40,7 +40,7 @@ Namzu is a single-process TypeScript kernel with the following responsibilities:
40
40
  - **Human-in-the-loop.** Structured plan review, per-tool approval with destructiveness flags, typed decision contracts, checkpoint/resume across sessions.
41
41
  - **Plugin system.** Lifecycle-hooked plugin loader with MCP contributions, tool contributions, and manifest-driven resolution.
42
42
  - **Multi-tenant isolation from day one.** Connector registries, vaults, config, and stores are tenant-scoped. Two organizations can share a process without cross-contamination.
43
- - **Provider abstraction.** OpenRouter and AWS Bedrock today; the `Provider` interface is narrow enough that adding another vendor is an afternoon. BYOK everywhere, no hidden hot paths for any vendor.
43
+ - **Provider abstraction.** Seven drivers ship today, each its own package installed only if you use it, plus a scriptable mock pre-registered in the kernel. The `LLMProvider` interface is narrow enough that adding another is an afternoon. BYOK everywhere, no hidden hot paths for any vendor.
44
44
  - **Telemetry.** OpenTelemetry-native spans and metrics. Cost accounting (input tokens, output tokens, cached tokens, cache write tokens, cache discount) flows from the provider into per-run, per-tenant rollups.
45
45
  - **Prompt cache integration.** Hash-based system-prompt cache per thread, integrated with provider cache controls (OpenRouter `cacheControl` today, more planned), plus full cache telemetry in every run.
46
46
  - **Vault.** BYOK credentials and secrets, tenant-scoped, pluggable backend.
@@ -52,11 +52,11 @@ Every one of those bullets points at code that exists today in `src/`. The archi
52
52
 
53
53
  Equally important for scoping expectations:
54
54
 
55
- - **Not a chat SDK.** There are no React, Svelte, or Vue hooks, no generative UI components, no `useChat`. Your UI framework is your choice; the kernel hands you a typed event stream.
55
+ - **Not a chat SDK.** No front-end framework bindings, no generative UI components, no ready-made chat hook. Your UI framework is your choice; the kernel hands you a typed event stream.
56
56
  - **Not a hosted service.** There is no dashboard, no Namzu Cloud, no billing page. You run it in your own process.
57
- - **Not a deployment adapter.** No Next.js, Hono, Express, or Cloudflare Workers plumbing in the kernel. Those belong in separate packages or your own infra code.
57
+ - **Not a deployment adapter.** No web-framework or edge-runtime plumbing in the kernel. That belongs in separate packages or your own infra code.
58
58
  - **Not a dev studio.** No bundled playground UI. A playground that consumes the kernel's event protocol could exist as a separate tool; it would not live inside `@namzu/sdk`.
59
- - **Not a vector database.** RAG ships with a pluggable `VectorStore` interface, but the kernel does not embed pgvector or Pinecone. Bring your own.
59
+ - **Not a vector database.** RAG ships with a pluggable `VectorStore` interface; the kernel embeds no vector engine of its own. Bring your own.
60
60
  - **Not an LLM router service.** Task routing is an in-process policy, not a hosted service.
61
61
  - **Not a prompt management UI.** Personas are code-defined (YAML files in your repo), not database rows behind a web form.
62
62
 
@@ -106,9 +106,19 @@ Every folder under `src/` maps to a traditional OS concept. This section walks t
106
106
 
107
107
  ### 1. The Boundary: Sandbox (`sandbox/`)
108
108
 
109
- Sandboxing is the foundation. Tools do not execute in the host process; they execute inside an OS-enforced jail with deny-default file I/O and scoped network access. macOS uses Seatbelt profiles in SBPL format (the same mechanism Apple's own apps use). Linux uses lightweight mount + PID namespaces, without requiring Docker, systemd, or any container runtime. The `SandboxProvider` abstraction (`sandbox/factory.ts`, `sandbox/provider/`) means you can swap in a virtualization-backed or userspace-kernel-backed provider later without touching the rest of the kernel.
109
+ Tools do not execute in the host process. What that buys you **depends on the tier the host can supply, and the tiers do not all enforce the same controls** so the kernel keeps an honest table rather than a promise (`sandbox/isolation.ts`):
110
110
 
111
- The kernel enforces memory, timeout, and max-process limits on top of whatever the sandbox gives you. The goal: a rogue or hallucinated tool call should never wipe your filesystem or exfiltrate arbitrary data, even if the LLM tries very hard.
111
+ | Environment | Filesystem | Network | Process |
112
+ |---|---|---|---|
113
+ | `macos-seatbelt` | yes | yes | yes |
114
+ | `linux-namespace` | **no** | yes | yes |
115
+ | `basic` | no | no | no |
116
+
117
+ `linux-namespace` reports `filesystem: false` deliberately. It unshares the mount namespace but never remounts anything, so the child still sees the whole host filesystem — a private mount table is not confinement, and saying otherwise here would be the exact defect the table exists to prevent.
118
+
119
+ This matters because the failure it guards against is silent. If a run requires a control the host cannot enforce, `assertIsolation` **refuses to start it** rather than proceeding at a weaker tier: a security control that is accepted and then quietly not applied is worse than one that was never offered, because the caller stops looking. Use `isolationOf`, `missingIsolation` and `describeIsolation` to ask what you are actually getting before you rely on it.
120
+
121
+ The `SandboxProvider` abstraction (`sandbox/factory.ts`, `sandbox/provider/`) lets you supply a stronger provider without touching the rest of the kernel. The kernel enforces memory, timeout, and max-process limits on top of whatever the sandbox gives you.
112
122
 
113
123
  ### 2. Interprocess Communication: Bridge (`bridge/`) and Bus (`bus/`)
114
124
 
@@ -136,7 +146,7 @@ These exist because the moment you have more than one agent running in parallel
136
146
  - Emits `agent_pending` on the bus with parent/child/depth metadata
137
147
  - Forwards every child event to the parent's run listener so the supervisor sees what its subtree is doing
138
148
 
139
- When the parent is cancelled — by HITL, by a limit breach, or by an external signal — `cancelAll(parentRunId)` walks the subtree and aborts every descendant. This is the Unix `SIGKILL` to a process group.
149
+ When the parent is cancelled — by HITL, by a limit breach, or by an external signal — `cancelAll(parentRunId)` walks the subtree and aborts every descendant. This is the equivalent of signalling a whole process group.
140
150
 
141
151
  `manager/connector/` manages the lifecycle of external connectors (MCP servers, HTTP connectors). `manager/plan/lifecycle.ts` coordinates HITL plan review. `manager/run/persistence.ts` is the run-level persistence surface, and `manager/run/emergency.ts` is the emergency-save subsystem (see §9 below).
142
152
 
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=a-close-that-means-closed.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a-close-that-means-closed.test.d.ts","sourceRoot":"","sources":["../../../../src/connector/mcp/__tests__/a-close-that-means-closed.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,106 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { StdioTransport } from '../stdio.js';
3
+ /**
4
+ * `close()` sent SIGTERM and returned without waiting, so a resolved close
5
+ * meant "the signal is on its way", not "the child is gone". A caller that
6
+ * closed a transport and then deleted the child's working directory raced the
7
+ * exit and saw EBUSY — reported from a real integration, not inferred.
8
+ *
9
+ * A close that does not mean closed makes every teardown built on top of it a
10
+ * guess, and the guess is only wrong sometimes, which is the worst kind.
11
+ *
12
+ * These assert on the operating system rather than on the transport's own
13
+ * state: `process.kill(pid, 0)` throws ESRCH once the pid is gone, so the
14
+ * assertion cannot pass by the transport merely believing it closed.
15
+ */
16
+ /** The child the transport spawned, read before `close()` clears the field. */
17
+ function childOf(transport) {
18
+ const child = transport.process;
19
+ if (!child)
20
+ throw new Error('transport spawned no process');
21
+ return child;
22
+ }
23
+ function pidOf(transport) {
24
+ const pid = childOf(transport).pid;
25
+ if (pid === undefined)
26
+ throw new Error('transport spawned no process');
27
+ return pid;
28
+ }
29
+ function isAlive(pid) {
30
+ try {
31
+ process.kill(pid, 0);
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ describe('a close that means closed', () => {
39
+ it('does not resolve until the child is actually gone', async () => {
40
+ const transport = new StdioTransport({
41
+ type: 'stdio',
42
+ command: process.execPath,
43
+ args: ['-e', 'setInterval(() => {}, 1000)'],
44
+ });
45
+ await transport.connect();
46
+ const child = childOf(transport);
47
+ const pid = pidOf(transport);
48
+ expect(isAlive(pid)).toBe(true);
49
+ await transport.close();
50
+ // The reaped-ness of the child, not the liveness of the pid.
51
+ //
52
+ // `isAlive(pid)` alone is sound about the wrong thing: on Windows
53
+ // `kill('SIGTERM')` terminates the process synchronously, so the pid is
54
+ // already gone by the next line whether or not `close()` waited — the
55
+ // assertion passed under a deliberately reintroduced fire-and-forget
56
+ // kill, which is how this was caught. Node fills `exitCode`/`signalCode`
57
+ // only when it reaps the child and emits `exit`, a later tick, so these
58
+ // are non-null here exactly when `close()` awaited that event.
59
+ expect(child.exitCode !== null || child.signalCode !== null).toBe(true);
60
+ expect(isAlive(pid)).toBe(false);
61
+ });
62
+ it('returns rather than hanging when the command does not exist', async () => {
63
+ // A spawn that fails emits `error` and never `exit`. Waiting on `exit`
64
+ // alone would turn a bad command into a shutdown that never completes.
65
+ const transport = new StdioTransport({
66
+ type: 'stdio',
67
+ command: 'namzu-no-such-command-exists-here',
68
+ args: [],
69
+ });
70
+ await transport.connect();
71
+ await expect(transport.close()).resolves.toBeUndefined();
72
+ });
73
+ it('is safe to call twice', async () => {
74
+ const transport = new StdioTransport({
75
+ type: 'stdio',
76
+ command: process.execPath,
77
+ args: ['-e', 'setInterval(() => {}, 1000)'],
78
+ });
79
+ await transport.connect();
80
+ const pid = pidOf(transport);
81
+ await transport.close();
82
+ await transport.close();
83
+ expect(isAlive(pid)).toBe(false);
84
+ });
85
+ it('reports the exit to the handler registered for that session', async () => {
86
+ // The waiting must not swallow the notification the client depends on
87
+ // to learn the session ended.
88
+ const transport = new StdioTransport({
89
+ type: 'stdio',
90
+ command: process.execPath,
91
+ args: ['-e', 'setInterval(() => {}, 1000)'],
92
+ });
93
+ await transport.connect();
94
+ let closes = 0;
95
+ transport.onClose(() => {
96
+ closes++;
97
+ });
98
+ await transport.close();
99
+ // `close` on a ChildProcess follows `exit` by a tick once the stdio
100
+ // streams drain; give the loop that tick rather than asserting on a
101
+ // race.
102
+ await new Promise((resolve) => setTimeout(resolve, 50));
103
+ expect(closes).toBe(1);
104
+ });
105
+ });
106
+ //# sourceMappingURL=a-close-that-means-closed.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a-close-that-means-closed.test.js","sourceRoot":"","sources":["../../../../src/connector/mcp/__tests__/a-close-that-means-closed.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../../src/connector/mcp/stdio.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,iBAAiB,EACjB,uBAAuB,EACvB,YAAY,EACZ,MAAM,gCAAgC,CAAA;AAGvC,qBAAa,cAAe,YAAW,YAAY;IAWtC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVnC,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,eAAe,CAAkD;IACzE,OAAO,CAAC,aAAa,CAAwB;IAC7C,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,SAAS,CAAQ;IACzB,6DAA6D;IAC7D,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,GAAG,CAAQ;gBAEU,MAAM,EAAE,uBAAuB;IAItD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAuCxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB5B;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa;IAMf,IAAI,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQrD,SAAS,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,GAAG,IAAI;IAI9D,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIlC,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI;IAI9C,WAAW,IAAI,OAAO;IAItB,OAAO,CAAC,aAAa;CAerB"}
1
+ {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../../src/connector/mcp/stdio.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,iBAAiB,EACjB,uBAAuB,EACvB,YAAY,EACZ,MAAM,gCAAgC,CAAA;AAUvC,qBAAa,cAAe,YAAW,YAAY;IAWtC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVnC,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,eAAe,CAAkD;IACzE,OAAO,CAAC,aAAa,CAAwB;IAC7C,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,SAAS,CAAQ;IACzB,6DAA6D;IAC7D,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,GAAG,CAAQ;gBAEU,MAAM,EAAE,uBAAuB;IAItD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAuCxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkD5B;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa;IAMf,IAAI,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQrD,SAAS,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,GAAG,IAAI;IAI9D,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIlC,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI;IAI9C,WAAW,IAAI,OAAO;IAItB,OAAO,CAAC,aAAa;CAerB"}
@@ -1,5 +1,11 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { getRootLogger } from '../../utils/logger.js';
3
+ /**
4
+ * How long a child gets to honour SIGTERM before SIGKILL. Two seconds is
5
+ * long enough for a server flushing a response and short enough that a
6
+ * shutdown does not read as a hang.
7
+ */
8
+ const TERMINATE_GRACE_MS = 2_000;
3
9
  export class StdioTransport {
4
10
  config;
5
11
  process = null;
@@ -60,9 +66,40 @@ export class StdioTransport {
60
66
  }
61
67
  this.connected = false;
62
68
  this.exitPending = true;
63
- this.process.kill('SIGTERM');
69
+ const child = this.process;
64
70
  this.process = null;
65
71
  this.buffer = '';
72
+ // Resolve when the child is actually gone, not when the signal was
73
+ // sent. `kill()` returns as soon as the signal is delivered, so an
74
+ // awaited `close()` meant only "SIGTERM is on its way" — a caller that
75
+ // closed and then deleted the child's working directory raced the exit
76
+ // and saw EBUSY. A close that does not mean closed makes every
77
+ // teardown after it a guess.
78
+ //
79
+ // A spawn that never produced a process emits `error` and no `exit`,
80
+ // so both settle this, and two timers make a hang impossible: the
81
+ // first escalates to SIGKILL for a child ignoring SIGTERM, the second
82
+ // gives up waiting. Neither holds the event loop open.
83
+ if (child.pid === undefined)
84
+ return;
85
+ await new Promise((resolve) => {
86
+ let settled = false;
87
+ const finish = () => {
88
+ if (settled)
89
+ return;
90
+ settled = true;
91
+ clearTimeout(escalate);
92
+ clearTimeout(giveUp);
93
+ resolve();
94
+ };
95
+ child.once('exit', finish);
96
+ child.once('error', finish);
97
+ child.kill('SIGTERM');
98
+ const escalate = setTimeout(() => child.kill('SIGKILL'), TERMINATE_GRACE_MS);
99
+ const giveUp = setTimeout(finish, TERMINATE_GRACE_MS * 2);
100
+ escalate.unref?.();
101
+ giveUp.unref?.();
102
+ });
66
103
  // The handlers deliberately survive this call. `process.on('close')`
67
104
  // fires them when the process actually exits, and dropping them here
68
105
  // would leave `MCPClient` believing it is still connected — so its next
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../../src/connector/mcp/stdio.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../../src/connector/mcp/stdio.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=a-gateway-that-forgets-the-oldest.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a-gateway-that-forgets-the-oldest.test.d.ts","sourceRoot":"","sources":["../../../src/gateway/__tests__/a-gateway-that-forgets-the-oldest.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { LocalTaskGateway } from '../local.js';
3
+ /**
4
+ * The gateway's two ledgers — `trackedTaskIds` and `settledHandles` — had `add`
5
+ * and `set` and no removal anywhere. The doc called them "bounded by the number
6
+ * the gateway itself launched", which is true and is not a bound: a gateway
7
+ * built per run is bounded by that run, but `SupervisorAgentConfig.gateway`
8
+ * lets a host supply its own, and a long-lived host reusing one accumulates an
9
+ * id and a settled handle for every task it ever launched.
10
+ *
11
+ * Eviction is oldest-first and takes both together. Dropping a tracked id while
12
+ * keeping its handle — or the reverse — would make a task that ran read as one
13
+ * that never launched, which is precisely the defect the settled-handle map was
14
+ * added to fix.
15
+ */
16
+ const CAP = 1_000;
17
+ class CountingManager {
18
+ n = 0;
19
+ async sendMessage() {
20
+ this.n++;
21
+ return { taskId: `tsk_${this.n}`, state: 'running' };
22
+ }
23
+ async waitForCompletion() { }
24
+ getInstance() {
25
+ return undefined;
26
+ }
27
+ cancel() { }
28
+ }
29
+ function context() {
30
+ return {
31
+ parentRunId: 'run_p',
32
+ parentAgentId: 'sup',
33
+ parentAbortController: new AbortController(),
34
+ depth: 0,
35
+ budgetTracker: { total: 1_000_000, remaining: 1_000_000 },
36
+ tenantId: 'tnt_g',
37
+ sessionId: 'ses_g',
38
+ projectId: 'prj_g',
39
+ };
40
+ }
41
+ function tracked(gateway) {
42
+ return gateway.trackedTaskIds;
43
+ }
44
+ function settled(gateway) {
45
+ return gateway.settledHandles;
46
+ }
47
+ async function launch(gateway, count) {
48
+ for (let i = 0; i < count; i++) {
49
+ await gateway.createTask({
50
+ agentId: 'w',
51
+ prompt: 'go',
52
+ workingDirectory: '/tmp',
53
+ });
54
+ }
55
+ }
56
+ describe('a gateway that outlives its run forgets the oldest', () => {
57
+ it('keeps every task of a run that never reaches the cap', async () => {
58
+ // The control, and the case that must not change: a supervisor reading
59
+ // its listing at the end of a run sees everything it launched.
60
+ const gateway = new LocalTaskGateway(new CountingManager(), context());
61
+ await launch(gateway, 50);
62
+ expect(tracked(gateway).size).toBe(50);
63
+ });
64
+ it('stops growing once the cap is passed', async () => {
65
+ const gateway = new LocalTaskGateway(new CountingManager(), context());
66
+ await launch(gateway, CAP + 25);
67
+ expect(tracked(gateway).size).toBe(CAP);
68
+ });
69
+ it('drops the oldest, not the newest', async () => {
70
+ // A cap that evicted the most recent would keep the ledger small and
71
+ // answer every question wrongly.
72
+ const gateway = new LocalTaskGateway(new CountingManager(), context());
73
+ await launch(gateway, CAP + 3);
74
+ const ids = tracked(gateway);
75
+ expect(ids.has('tsk_1')).toBe(false);
76
+ expect(ids.has('tsk_3')).toBe(false);
77
+ expect(ids.has(`tsk_${CAP + 3}`)).toBe(true);
78
+ });
79
+ it('never keeps a settled handle for an id it has forgotten', async () => {
80
+ // The two must be evicted together. A handle whose id is gone is
81
+ // unreachable through `listTasks`, which walks the ids — so it would be
82
+ // retained memory serving nothing, and the leak would survive its own
83
+ // fix.
84
+ const gateway = new LocalTaskGateway(new CountingManager(), context());
85
+ await launch(gateway, 10);
86
+ const handles = settled(gateway);
87
+ for (let i = 1; i <= 10; i++)
88
+ handles.set(`tsk_${i}`, { id: i });
89
+ await launch(gateway, CAP + 5);
90
+ const ids = tracked(gateway);
91
+ for (const id of handles.keys()) {
92
+ expect({ id, trackedToo: ids.has(id) }).toEqual({ id, trackedToo: true });
93
+ }
94
+ });
95
+ });
96
+ //# sourceMappingURL=a-gateway-that-forgets-the-oldest.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a-gateway-that-forgets-the-oldest.test.js","sourceRoot":"","sources":["../../../src/gateway/__tests__/a-gateway-that-forgets-the-oldest.test.ts"],"names":[],"mappings":"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"}
@@ -59,6 +59,22 @@ export declare class LocalTaskGateway implements TaskGateway {
59
59
  * Called when the task settles, while the manager still holds it.
60
60
  */
61
61
  rememberSettled(taskId: TaskId): void;
62
+ /**
63
+ * Drop the oldest tasks once the ledger passes {@link GATEWAY_TASK_LEDGER_CAP}.
64
+ *
65
+ * A gateway constructed per run is bounded by that run and this never
66
+ * fires. But `SupervisorAgentConfig.gateway` lets a host supply its own,
67
+ * and a long-lived host reusing one accumulates an id and a settled handle
68
+ * per task it ever launched, for the life of the process — the doc above
69
+ * says "bounded by the number the gateway itself launched", which is true
70
+ * and is not a bound when the gateway outlives the run.
71
+ *
72
+ * Both collections are evicted **together and in insertion order**. Losing
73
+ * a tracked id while keeping its handle, or the reverse, would make a task
74
+ * that ran read as one that never launched — which is the exact defect the
75
+ * settled-handle map was added to fix, reintroduced by its own cleanup.
76
+ */
77
+ private forgetOldestBeyondCap;
62
78
  listTasks(): TaskHandle[];
63
79
  /**
64
80
  * Every event a child emits, reduced to "this one is still alive".
@@ -1 +1 @@
1
- {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/gateway/local.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EACX,iBAAiB,EACjB,oBAAoB,EACpB,WAAW,EACX,UAAU,EACV,MAAM,2BAA2B,CAAA;AAClC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAA;AAEnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAI9D,qBAAa,gBAAiB,YAAW,WAAW;IACnD,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,cAAc,CAAyB;IAE/C,OAAO,CAAC,WAAW,CAAC,CAA2E;IAE/F,OAAO,CAAC,mBAAmB,CAA+C;IAE1E;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,cAAc,CAAqC;IAE3D,OAAO,CAAC,oBAAoB,CAAmC;IAC/D,kCAAkC;IAClC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAsC;gBAGvE,YAAY,EAAE,oBAAoB,EAClC,WAAW,EAAE,gBAAgB,EAC7B,QAAQ,CAAC,EAAE,gBAAgB,EAC3B,WAAW,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,GAAG,sBAAsB,GAAG,gBAAgB,CAAC,EACvF,OAAO,CAAC,EAAE;QAAE,oBAAoB,CAAC,EAAE,oBAAoB,CAAA;KAAE;IAWpD,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC;IA6GjE;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,kBAAkB;IA0BpB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAShD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlE,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIhC,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAK/C;;;;OAIG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAKrC,SAAS,IAAI,UAAU,EAAE;IAgBzB;;;;;;;OAOG;IACH,cAAc,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAO9D,eAAe,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI;CAMnE"}
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/gateway/local.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EACX,iBAAiB,EACjB,oBAAoB,EACpB,WAAW,EACX,UAAU,EACV,MAAM,2BAA2B,CAAA;AAClC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAA;AAEnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAe9D,qBAAa,gBAAiB,YAAW,WAAW;IACnD,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,cAAc,CAAyB;IAE/C,OAAO,CAAC,WAAW,CAAC,CAA2E;IAE/F,OAAO,CAAC,mBAAmB,CAA+C;IAE1E;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,cAAc,CAAqC;IAE3D,OAAO,CAAC,oBAAoB,CAAmC;IAC/D,kCAAkC;IAClC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAsC;gBAGvE,YAAY,EAAE,oBAAoB,EAClC,WAAW,EAAE,gBAAgB,EAC7B,QAAQ,CAAC,EAAE,gBAAgB,EAC3B,WAAW,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,GAAG,sBAAsB,GAAG,gBAAgB,CAAC,EACvF,OAAO,CAAC,EAAE;QAAE,oBAAoB,CAAC,EAAE,oBAAoB,CAAA;KAAE;IAWpD,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC;IA8GjE;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,kBAAkB;IA0BpB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAShD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlE,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIhC,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAK/C;;;;OAIG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAKrC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,qBAAqB;IAS7B,SAAS,IAAI,UAAU,EAAE;IAgBzB;;;;;;;OAOG;IACH,cAAc,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAO9D,eAAe,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI;CAMnE"}
@@ -2,6 +2,16 @@ import { taskFailed } from '../tools/coordinator/outcome.js';
2
2
  import { createUserMessage } from '../types/message/index.js';
3
3
  import { toErrorMessage } from '../utils/error.js';
4
4
  import { getRootLogger } from '../utils/logger.js';
5
+ /**
6
+ * How many launched tasks a gateway remembers.
7
+ *
8
+ * High enough that no realistic single run reaches it — a fan-out is eight,
9
+ * a long supervisory run is dozens — so the listing a supervisor reads at the
10
+ * end of its run is always complete. It exists for the host that reuses one
11
+ * gateway across runs, where the alternative is a set and a map that grow for
12
+ * the life of the process.
13
+ */
14
+ const GATEWAY_TASK_LEDGER_CAP = 1_000;
5
15
  export class LocalTaskGateway {
6
16
  agentManager;
7
17
  taskContext;
@@ -114,6 +124,7 @@ export class LocalTaskGateway {
114
124
  });
115
125
  launched.id = task.taskId;
116
126
  this.trackedTaskIds.add(task.taskId);
127
+ this.forgetOldestBeyondCap();
117
128
  this.agentManager
118
129
  .waitForCompletion(task.taskId)
119
130
  .then(() => {
@@ -210,6 +221,30 @@ export class LocalTaskGateway {
210
221
  if (task)
211
222
  this.settledHandles.set(taskId, toHandle(task));
212
223
  }
224
+ /**
225
+ * Drop the oldest tasks once the ledger passes {@link GATEWAY_TASK_LEDGER_CAP}.
226
+ *
227
+ * A gateway constructed per run is bounded by that run and this never
228
+ * fires. But `SupervisorAgentConfig.gateway` lets a host supply its own,
229
+ * and a long-lived host reusing one accumulates an id and a settled handle
230
+ * per task it ever launched, for the life of the process — the doc above
231
+ * says "bounded by the number the gateway itself launched", which is true
232
+ * and is not a bound when the gateway outlives the run.
233
+ *
234
+ * Both collections are evicted **together and in insertion order**. Losing
235
+ * a tracked id while keeping its handle, or the reverse, would make a task
236
+ * that ran read as one that never launched — which is the exact defect the
237
+ * settled-handle map was added to fix, reintroduced by its own cleanup.
238
+ */
239
+ forgetOldestBeyondCap() {
240
+ while (this.trackedTaskIds.size > GATEWAY_TASK_LEDGER_CAP) {
241
+ const oldest = this.trackedTaskIds.values().next().value;
242
+ if (oldest === undefined)
243
+ return;
244
+ this.trackedTaskIds.delete(oldest);
245
+ this.settledHandles.delete(oldest);
246
+ }
247
+ }
213
248
  listTasks() {
214
249
  const handles = [];
215
250
  for (const taskId of this.trackedTaskIds) {
@@ -1 +1 @@
1
- {"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/gateway/local.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/gateway/local.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/sdk",
3
- "version": "14.0.3",
3
+ "version": "14.0.6",
4
4
  "description": "Open-source AI agent SDK with a built-in runtime. Nothing between you and your agents.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "type": "module",
@@ -0,0 +1,119 @@
1
+ import type { ChildProcess } from 'node:child_process'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { StdioTransport } from '../stdio.js'
5
+
6
+ /**
7
+ * `close()` sent SIGTERM and returned without waiting, so a resolved close
8
+ * meant "the signal is on its way", not "the child is gone". A caller that
9
+ * closed a transport and then deleted the child's working directory raced the
10
+ * exit and saw EBUSY — reported from a real integration, not inferred.
11
+ *
12
+ * A close that does not mean closed makes every teardown built on top of it a
13
+ * guess, and the guess is only wrong sometimes, which is the worst kind.
14
+ *
15
+ * These assert on the operating system rather than on the transport's own
16
+ * state: `process.kill(pid, 0)` throws ESRCH once the pid is gone, so the
17
+ * assertion cannot pass by the transport merely believing it closed.
18
+ */
19
+
20
+ /** The child the transport spawned, read before `close()` clears the field. */
21
+ function childOf(transport: StdioTransport): ChildProcess {
22
+ const child = (transport as unknown as { process: ChildProcess | null }).process
23
+ if (!child) throw new Error('transport spawned no process')
24
+ return child
25
+ }
26
+
27
+ function pidOf(transport: StdioTransport): number {
28
+ const pid = childOf(transport).pid
29
+ if (pid === undefined) throw new Error('transport spawned no process')
30
+ return pid
31
+ }
32
+
33
+ function isAlive(pid: number): boolean {
34
+ try {
35
+ process.kill(pid, 0)
36
+ return true
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ describe('a close that means closed', () => {
43
+ it('does not resolve until the child is actually gone', async () => {
44
+ const transport = new StdioTransport({
45
+ type: 'stdio',
46
+ command: process.execPath,
47
+ args: ['-e', 'setInterval(() => {}, 1000)'],
48
+ })
49
+ await transport.connect()
50
+ const child = childOf(transport)
51
+ const pid = pidOf(transport)
52
+ expect(isAlive(pid)).toBe(true)
53
+
54
+ await transport.close()
55
+
56
+ // The reaped-ness of the child, not the liveness of the pid.
57
+ //
58
+ // `isAlive(pid)` alone is sound about the wrong thing: on Windows
59
+ // `kill('SIGTERM')` terminates the process synchronously, so the pid is
60
+ // already gone by the next line whether or not `close()` waited — the
61
+ // assertion passed under a deliberately reintroduced fire-and-forget
62
+ // kill, which is how this was caught. Node fills `exitCode`/`signalCode`
63
+ // only when it reaps the child and emits `exit`, a later tick, so these
64
+ // are non-null here exactly when `close()` awaited that event.
65
+ expect(child.exitCode !== null || child.signalCode !== null).toBe(true)
66
+ expect(isAlive(pid)).toBe(false)
67
+ })
68
+
69
+ it('returns rather than hanging when the command does not exist', async () => {
70
+ // A spawn that fails emits `error` and never `exit`. Waiting on `exit`
71
+ // alone would turn a bad command into a shutdown that never completes.
72
+ const transport = new StdioTransport({
73
+ type: 'stdio',
74
+ command: 'namzu-no-such-command-exists-here',
75
+ args: [],
76
+ })
77
+ await transport.connect()
78
+
79
+ await expect(transport.close()).resolves.toBeUndefined()
80
+ })
81
+
82
+ it('is safe to call twice', async () => {
83
+ const transport = new StdioTransport({
84
+ type: 'stdio',
85
+ command: process.execPath,
86
+ args: ['-e', 'setInterval(() => {}, 1000)'],
87
+ })
88
+ await transport.connect()
89
+ const pid = pidOf(transport)
90
+
91
+ await transport.close()
92
+ await transport.close()
93
+
94
+ expect(isAlive(pid)).toBe(false)
95
+ })
96
+
97
+ it('reports the exit to the handler registered for that session', async () => {
98
+ // The waiting must not swallow the notification the client depends on
99
+ // to learn the session ended.
100
+ const transport = new StdioTransport({
101
+ type: 'stdio',
102
+ command: process.execPath,
103
+ args: ['-e', 'setInterval(() => {}, 1000)'],
104
+ })
105
+ await transport.connect()
106
+ let closes = 0
107
+ transport.onClose(() => {
108
+ closes++
109
+ })
110
+
111
+ await transport.close()
112
+ // `close` on a ChildProcess follows `exit` by a tick once the stdio
113
+ // streams drain; give the loop that tick rather than asserting on a
114
+ // race.
115
+ await new Promise((resolve) => setTimeout(resolve, 50))
116
+
117
+ expect(closes).toBe(1)
118
+ })
119
+ })
@@ -6,6 +6,13 @@ import type {
6
6
  } from '../../types/connector/index.js'
7
7
  import { type Logger, getRootLogger } from '../../utils/logger.js'
8
8
 
9
+ /**
10
+ * How long a child gets to honour SIGTERM before SIGKILL. Two seconds is
11
+ * long enough for a server flushing a response and short enough that a
12
+ * shutdown does not read as a hang.
13
+ */
14
+ const TERMINATE_GRACE_MS = 2_000
15
+
9
16
  export class StdioTransport implements MCPTransport {
10
17
  private process: ChildProcess | null = null
11
18
  private messageHandlers: Array<(message: MCPJsonRpcMessage) => void> = []
@@ -71,9 +78,39 @@ export class StdioTransport implements MCPTransport {
71
78
  }
72
79
  this.connected = false
73
80
  this.exitPending = true
74
- this.process.kill('SIGTERM')
81
+ const child = this.process
75
82
  this.process = null
76
83
  this.buffer = ''
84
+
85
+ // Resolve when the child is actually gone, not when the signal was
86
+ // sent. `kill()` returns as soon as the signal is delivered, so an
87
+ // awaited `close()` meant only "SIGTERM is on its way" — a caller that
88
+ // closed and then deleted the child's working directory raced the exit
89
+ // and saw EBUSY. A close that does not mean closed makes every
90
+ // teardown after it a guess.
91
+ //
92
+ // A spawn that never produced a process emits `error` and no `exit`,
93
+ // so both settle this, and two timers make a hang impossible: the
94
+ // first escalates to SIGKILL for a child ignoring SIGTERM, the second
95
+ // gives up waiting. Neither holds the event loop open.
96
+ if (child.pid === undefined) return
97
+ await new Promise<void>((resolve) => {
98
+ let settled = false
99
+ const finish = (): void => {
100
+ if (settled) return
101
+ settled = true
102
+ clearTimeout(escalate)
103
+ clearTimeout(giveUp)
104
+ resolve()
105
+ }
106
+ child.once('exit', finish)
107
+ child.once('error', finish)
108
+ child.kill('SIGTERM')
109
+ const escalate = setTimeout(() => child.kill('SIGKILL'), TERMINATE_GRACE_MS)
110
+ const giveUp = setTimeout(finish, TERMINATE_GRACE_MS * 2)
111
+ escalate.unref?.()
112
+ giveUp.unref?.()
113
+ })
77
114
  // The handlers deliberately survive this call. `process.on('close')`
78
115
  // fires them when the process actually exits, and dropping them here
79
116
  // would leave `MCPClient` believing it is still connected — so its next
@@ -0,0 +1,130 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { AgentManagerContract } from '../../types/agent/manager.js'
4
+ import type { AgentTask, AgentTaskContext } from '../../types/agent/task.js'
5
+ import type { AgentId, TaskId } from '../../types/ids/index.js'
6
+ import { LocalTaskGateway } from '../local.js'
7
+
8
+ /**
9
+ * The gateway's two ledgers — `trackedTaskIds` and `settledHandles` — had `add`
10
+ * and `set` and no removal anywhere. The doc called them "bounded by the number
11
+ * the gateway itself launched", which is true and is not a bound: a gateway
12
+ * built per run is bounded by that run, but `SupervisorAgentConfig.gateway`
13
+ * lets a host supply its own, and a long-lived host reusing one accumulates an
14
+ * id and a settled handle for every task it ever launched.
15
+ *
16
+ * Eviction is oldest-first and takes both together. Dropping a tracked id while
17
+ * keeping its handle — or the reverse — would make a task that ran read as one
18
+ * that never launched, which is precisely the defect the settled-handle map was
19
+ * added to fix.
20
+ */
21
+
22
+ const CAP = 1_000
23
+
24
+ class CountingManager {
25
+ private n = 0
26
+
27
+ async sendMessage(): Promise<AgentTask> {
28
+ this.n++
29
+ return { taskId: `tsk_${this.n}` as TaskId, state: 'running' } as AgentTask
30
+ }
31
+ async waitForCompletion(): Promise<void> {}
32
+ getInstance(): AgentTask | undefined {
33
+ return undefined
34
+ }
35
+ cancel(): void {}
36
+ }
37
+
38
+ function context(): AgentTaskContext {
39
+ return {
40
+ parentRunId: 'run_p' as never,
41
+ parentAgentId: 'sup',
42
+ parentAbortController: new AbortController(),
43
+ depth: 0,
44
+ budgetTracker: { total: 1_000_000, remaining: 1_000_000 },
45
+ tenantId: 'tnt_g' as never,
46
+ sessionId: 'ses_g' as never,
47
+ projectId: 'prj_g' as never,
48
+ } as unknown as AgentTaskContext
49
+ }
50
+
51
+ function tracked(gateway: LocalTaskGateway): Set<TaskId> {
52
+ return (gateway as unknown as { trackedTaskIds: Set<TaskId> }).trackedTaskIds
53
+ }
54
+
55
+ function settled(gateway: LocalTaskGateway): Map<TaskId, unknown> {
56
+ return (gateway as unknown as { settledHandles: Map<TaskId, unknown> }).settledHandles
57
+ }
58
+
59
+ async function launch(gateway: LocalTaskGateway, count: number): Promise<void> {
60
+ for (let i = 0; i < count; i++) {
61
+ await gateway.createTask({
62
+ agentId: 'w' as AgentId,
63
+ prompt: 'go',
64
+ workingDirectory: '/tmp',
65
+ } as never)
66
+ }
67
+ }
68
+
69
+ describe('a gateway that outlives its run forgets the oldest', () => {
70
+ it('keeps every task of a run that never reaches the cap', async () => {
71
+ // The control, and the case that must not change: a supervisor reading
72
+ // its listing at the end of a run sees everything it launched.
73
+ const gateway = new LocalTaskGateway(
74
+ new CountingManager() as unknown as AgentManagerContract,
75
+ context(),
76
+ )
77
+
78
+ await launch(gateway, 50)
79
+
80
+ expect(tracked(gateway).size).toBe(50)
81
+ })
82
+
83
+ it('stops growing once the cap is passed', async () => {
84
+ const gateway = new LocalTaskGateway(
85
+ new CountingManager() as unknown as AgentManagerContract,
86
+ context(),
87
+ )
88
+
89
+ await launch(gateway, CAP + 25)
90
+
91
+ expect(tracked(gateway).size).toBe(CAP)
92
+ })
93
+
94
+ it('drops the oldest, not the newest', async () => {
95
+ // A cap that evicted the most recent would keep the ledger small and
96
+ // answer every question wrongly.
97
+ const gateway = new LocalTaskGateway(
98
+ new CountingManager() as unknown as AgentManagerContract,
99
+ context(),
100
+ )
101
+
102
+ await launch(gateway, CAP + 3)
103
+
104
+ const ids = tracked(gateway)
105
+ expect(ids.has('tsk_1' as TaskId)).toBe(false)
106
+ expect(ids.has('tsk_3' as TaskId)).toBe(false)
107
+ expect(ids.has(`tsk_${CAP + 3}` as TaskId)).toBe(true)
108
+ })
109
+
110
+ it('never keeps a settled handle for an id it has forgotten', async () => {
111
+ // The two must be evicted together. A handle whose id is gone is
112
+ // unreachable through `listTasks`, which walks the ids — so it would be
113
+ // retained memory serving nothing, and the leak would survive its own
114
+ // fix.
115
+ const gateway = new LocalTaskGateway(
116
+ new CountingManager() as unknown as AgentManagerContract,
117
+ context(),
118
+ )
119
+ await launch(gateway, 10)
120
+ const handles = settled(gateway)
121
+ for (let i = 1; i <= 10; i++) handles.set(`tsk_${i}` as TaskId, { id: i })
122
+
123
+ await launch(gateway, CAP + 5)
124
+
125
+ const ids = tracked(gateway)
126
+ for (const id of handles.keys()) {
127
+ expect({ id, trackedToo: ids.has(id) }).toEqual({ id, trackedToo: true })
128
+ }
129
+ })
130
+ })
@@ -14,6 +14,17 @@ import type { RunEventListener } from '../types/run/events.js'
14
14
  import { toErrorMessage } from '../utils/error.js'
15
15
  import { getRootLogger } from '../utils/logger.js'
16
16
 
17
+ /**
18
+ * How many launched tasks a gateway remembers.
19
+ *
20
+ * High enough that no realistic single run reaches it — a fan-out is eight,
21
+ * a long supervisory run is dozens — so the listing a supervisor reads at the
22
+ * end of its run is always complete. It exists for the host that reuses one
23
+ * gateway across runs, where the alternative is a set and a map that grow for
24
+ * the life of the process.
25
+ */
26
+ const GATEWAY_TASK_LEDGER_CAP = 1_000
27
+
17
28
  export class LocalTaskGateway implements TaskGateway {
18
29
  private agentManager: AgentManagerContract
19
30
  private taskContext: AgentTaskContext
@@ -140,6 +151,7 @@ export class LocalTaskGateway implements TaskGateway {
140
151
 
141
152
  launched.id = task.taskId
142
153
  this.trackedTaskIds.add(task.taskId)
154
+ this.forgetOldestBeyondCap()
143
155
 
144
156
  this.agentManager
145
157
  .waitForCompletion(task.taskId)
@@ -242,6 +254,30 @@ export class LocalTaskGateway implements TaskGateway {
242
254
  if (task) this.settledHandles.set(taskId, toHandle(task))
243
255
  }
244
256
 
257
+ /**
258
+ * Drop the oldest tasks once the ledger passes {@link GATEWAY_TASK_LEDGER_CAP}.
259
+ *
260
+ * A gateway constructed per run is bounded by that run and this never
261
+ * fires. But `SupervisorAgentConfig.gateway` lets a host supply its own,
262
+ * and a long-lived host reusing one accumulates an id and a settled handle
263
+ * per task it ever launched, for the life of the process — the doc above
264
+ * says "bounded by the number the gateway itself launched", which is true
265
+ * and is not a bound when the gateway outlives the run.
266
+ *
267
+ * Both collections are evicted **together and in insertion order**. Losing
268
+ * a tracked id while keeping its handle, or the reverse, would make a task
269
+ * that ran read as one that never launched — which is the exact defect the
270
+ * settled-handle map was added to fix, reintroduced by its own cleanup.
271
+ */
272
+ private forgetOldestBeyondCap(): void {
273
+ while (this.trackedTaskIds.size > GATEWAY_TASK_LEDGER_CAP) {
274
+ const oldest = this.trackedTaskIds.values().next().value
275
+ if (oldest === undefined) return
276
+ this.trackedTaskIds.delete(oldest)
277
+ this.settledHandles.delete(oldest)
278
+ }
279
+ }
280
+
245
281
  listTasks(): TaskHandle[] {
246
282
  const handles: TaskHandle[] = []
247
283
  for (const taskId of this.trackedTaskIds) {