antechamber 1.0.34 → 1.0.36

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
@@ -7,6 +7,19 @@ and this project uses [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.36] - 2026-11-03
11
+
12
+ ### Added
13
+
14
+ - Clean up after the agent on macOS. Disposal now records the agent's process group and re-identifies each member
15
+ before signalling it, so child processes that outlive the agent are stopped instead of being left behind. Linux
16
+ keeps reading `/proc`; macOS reads `/bin/ps`, because Node has neither `/proc` nor pidfd there. A process that has
17
+ already exited (zombie) never counts as a survivor.
18
+
19
+ ## [1.0.35] - 2026-11-03
20
+
21
+ - test: resolve fixture paths with fileURLToPath (#86)
22
+
10
23
  ## [1.0.34] - 2026-11-02
11
24
 
12
25
  ### Fixed
package/dist/pi-rpc.d.ts CHANGED
@@ -9,6 +9,8 @@
9
9
  */
10
10
  import { EventEmitter } from "node:events";
11
11
  import { type LifecycleClock } from "./lifecycle.js";
12
+ import { type ProcessTable } from "./proc-table.js";
13
+ export type { ProcessIdentity, ProcessTable } from "./proc-table.js";
12
14
  export interface PiRpcCommand {
13
15
  id?: string;
14
16
  type: string;
@@ -30,17 +32,6 @@ export declare class UpstreamProtocolError extends Error {
30
32
  * earlyoom take a workload before the adapter, whose death stops every session at once.
31
33
  */
32
34
  export declare const AGENT_OOM_SCORE_ADJ_MARGIN = 300;
33
- /** A process's group and kernel start time: with the PID, the identity that survives PID reuse checks. */
34
- interface ProcessIdentity {
35
- readonly pid: number;
36
- readonly group: number;
37
- readonly start: string;
38
- }
39
- /** Reads process identities so disposal can tell owned survivors from a reused PID. */
40
- export interface ProcessTable {
41
- identity(pid: number): ProcessIdentity | undefined;
42
- groupMembers(group: number): ProcessIdentity[];
43
- }
44
35
  /** Why the agent stopped without antechamber disposing it on purpose (eviction, delete, shutdown). */
45
36
  export type PiStopReason = {
46
37
  readonly kind: "timeout";
@@ -143,4 +134,3 @@ export declare class PiRpcClient extends EventEmitter {
143
134
  notify(cmd: Omit<PiRpcCommand, "id">): void;
144
135
  dispose(): Promise<void>;
145
136
  }
146
- export {};
package/dist/pi-rpc.js CHANGED
@@ -11,10 +11,11 @@ import { spawn } from "node:child_process";
11
11
  import { StringDecoder } from "node:string_decoder";
12
12
  import { EventEmitter } from "node:events";
13
13
  import { randomUUID } from "node:crypto";
14
- import { readdirSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { readFileSync, writeFileSync } from "node:fs";
15
15
  import { constants as osConstants } from "node:os";
16
16
  import { systemClock } from "./lifecycle.js";
17
17
  import { agentBinary, envSetting } from "./config.js";
18
+ import { createProcessTable } from "./proc-table.js";
18
19
  export const RPC_COMMAND_TIMEOUT_MS = 60_000;
19
20
  /**
20
21
  * `compact` answers only when the summary is written. OmO bounds one compaction by
@@ -50,34 +51,7 @@ function rankAboveAdapterForOom(pid) {
50
51
  console.error(`[pi-rpc] could not raise the agent's OOM score: ${error instanceof Error ? error.message : error}`);
51
52
  }
52
53
  }
53
- // Linux /proc. Any read failure (gone, no permission, no /proc) means the identity cannot be
54
- // verified, and an unverifiable process is never signalled: fail closed.
55
- const procTable = {
56
- identity(pid) {
57
- let stat;
58
- try {
59
- stat = readFileSync(`/proc/${pid}/stat`, "utf8");
60
- }
61
- catch {
62
- return undefined;
63
- }
64
- const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
65
- return { pid, group: Number(fields[2]), start: fields[19] ?? "" };
66
- },
67
- groupMembers(group) {
68
- let names;
69
- try {
70
- names = readdirSync("/proc");
71
- }
72
- catch {
73
- return [];
74
- }
75
- return names
76
- .filter((name) => /^\d+$/.test(name))
77
- .flatMap((name) => procTable.identity(Number(name)) ?? [])
78
- .filter((member) => member.group === group);
79
- },
80
- };
54
+ const procTable = createProcessTable();
81
55
  function duration(ms) {
82
56
  if (ms % 60_000 === 0)
83
57
  return `${ms / 60_000} min`;
@@ -0,0 +1,16 @@
1
+ import type { ProcessTable } from "./proc-table.js";
2
+ /**
3
+ * macOS process table, backed by `/bin/ps`. Used by disposal to identify owned survivors, so it must
4
+ * report the same three facts Linux reads from `/proc/<pid>/stat`: PID, process group, and a start
5
+ * time that survives PID reuse checks. `ps` exposes the latter as `lstart` (second granularity).
6
+ */
7
+ /** Runs `/bin/ps` with the given arguments and returns stdout. Injected so tests never spawn. */
8
+ export type PsRunner = (args: readonly string[]) => string;
9
+ /**
10
+ * `execFileSync` with an absolute binary: no shell, and no PATH lookup that a user-controlled
11
+ * directory could shadow. `-ww` disables width truncation; the column set is fixed so every line
12
+ * has the same shape. LC_ALL is pinned because `lstart` is rendered in the current locale: two
13
+ * calls must agree with each other even if the user changes the system language mid-session.
14
+ */
15
+ export declare function runPs(args: readonly string[]): string;
16
+ export declare function createDarwinProcTable(run?: PsRunner): ProcessTable;
@@ -0,0 +1,78 @@
1
+ import { execFileSync } from "node:child_process";
2
+ const PS_TIMEOUT_MS = 2_000;
3
+ const PS_MAX_BUFFER = 8 * 1024 * 1024;
4
+ const PS_COLUMNS = "pid=,pgid=,stat=,lstart=";
5
+ /** `pid pgid stat lstart`, where lstart is "Sun Nov 1 15:55:48 2026" and may end in padding. */
6
+ const PS_LINE = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/;
7
+ /**
8
+ * `execFileSync` with an absolute binary: no shell, and no PATH lookup that a user-controlled
9
+ * directory could shadow. `-ww` disables width truncation; the column set is fixed so every line
10
+ * has the same shape. LC_ALL is pinned because `lstart` is rendered in the current locale: two
11
+ * calls must agree with each other even if the user changes the system language mid-session.
12
+ */
13
+ export function runPs(args) {
14
+ return execFileSync("/bin/ps", args, {
15
+ encoding: "utf8",
16
+ timeout: PS_TIMEOUT_MS,
17
+ maxBuffer: PS_MAX_BUFFER,
18
+ env: { ...process.env, LC_ALL: "C", LANG: "C" },
19
+ });
20
+ }
21
+ function parseLine(line) {
22
+ const match = PS_LINE.exec(line);
23
+ if (match === null)
24
+ return undefined;
25
+ const pid = Number(match[1]);
26
+ const group = Number(match[2]);
27
+ const state = match[3];
28
+ const start = match[4];
29
+ if (!Number.isInteger(pid) || !Number.isInteger(group) || state === undefined || start === undefined)
30
+ return undefined;
31
+ if (start === "")
32
+ return undefined;
33
+ return { pid, group, start, state };
34
+ }
35
+ /**
36
+ * A zombie has already exited; only its parent is left to reap it. Counting it as a survivor would
37
+ * make disposal wait out the full grace window for a process that can no longer run, so `Z` entries
38
+ * are treated as gone in both methods, as `session-hosts` already does on Linux.
39
+ */
40
+ function alive(entry) {
41
+ return !entry.state.startsWith("Z");
42
+ }
43
+ function toIdentity(entry) {
44
+ return { pid: entry.pid, group: entry.group, start: entry.start };
45
+ }
46
+ export function createDarwinProcTable(run = runPs) {
47
+ return {
48
+ identity(pid) {
49
+ if (!Number.isInteger(pid) || pid <= 0)
50
+ return undefined;
51
+ let output;
52
+ try {
53
+ output = run(["-ww", "-o", PS_COLUMNS, "-p", String(pid)]);
54
+ }
55
+ catch {
56
+ // The process is gone, or ps could not read it. An unverifiable identity fails closed.
57
+ return undefined;
58
+ }
59
+ const entry = parseLine(output.trim());
60
+ if (entry === undefined || entry.pid !== pid || !alive(entry))
61
+ return undefined;
62
+ return toIdentity(entry);
63
+ },
64
+ groupMembers(group) {
65
+ let output;
66
+ try {
67
+ output = run(["-ww", "-o", PS_COLUMNS, "-A"]);
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ return output.split("\n").flatMap((line) => {
73
+ const entry = parseLine(line);
74
+ return entry !== undefined && entry.group === group && alive(entry) ? [toIdentity(entry)] : [];
75
+ });
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,17 @@
1
+ /** A process's group and kernel start time: with the PID, the identity that survives PID reuse checks. */
2
+ export interface ProcessIdentity {
3
+ readonly pid: number;
4
+ readonly group: number;
5
+ readonly start: string;
6
+ }
7
+ /** Reads process identities so disposal can tell owned survivors from a reused PID. */
8
+ export interface ProcessTable {
9
+ identity(pid: number): ProcessIdentity | undefined;
10
+ groupMembers(group: number): ProcessIdentity[];
11
+ }
12
+ /**
13
+ * The process table for this platform. Linux reads `/proc`; macOS reads `/bin/ps`, because Node has no
14
+ * `/proc` and no pidfd there. Every other platform keeps the Linux table so a missing `/proc` fails
15
+ * closed (an identity is never reported that cannot be verified).
16
+ */
17
+ export declare function createProcessTable(platform?: NodeJS.Platform): ProcessTable;
@@ -0,0 +1,38 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { createDarwinProcTable } from "./proc-table-darwin.js";
3
+ // Linux /proc. Any read failure (gone, no permission, no /proc) means the identity cannot be
4
+ // verified, and an unverifiable process is never signalled: fail closed.
5
+ const linuxProcTable = {
6
+ identity(pid) {
7
+ let stat;
8
+ try {
9
+ stat = readFileSync(`/proc/${pid}/stat`, "utf8");
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
15
+ return { pid, group: Number(fields[2]), start: fields[19] ?? "" };
16
+ },
17
+ groupMembers(group) {
18
+ let names;
19
+ try {
20
+ names = readdirSync("/proc");
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ return names
26
+ .filter((name) => /^\d+$/.test(name))
27
+ .flatMap((name) => linuxProcTable.identity(Number(name)) ?? [])
28
+ .filter((member) => member.group === group);
29
+ },
30
+ };
31
+ /**
32
+ * The process table for this platform. Linux reads `/proc`; macOS reads `/bin/ps`, because Node has no
33
+ * `/proc` and no pidfd there. Every other platform keeps the Linux table so a missing `/proc` fails
34
+ * closed (an identity is never reported that cannot be verified).
35
+ */
36
+ export function createProcessTable(platform = process.platform) {
37
+ return platform === "darwin" ? createDarwinProcTable() : linuxProcTable;
38
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-compat",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "OpenChamber extension: displays OmO tool calls the way OmO means them when OpenChamber runs on antechamber",
5
5
  "private": true,
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-graph",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "OpenChamber extension: live, interactive graph of the OmO sub-agents behind the current session",
5
5
  "private": true,
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-work",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "OpenChamber 2.x Work Status section: OmO todo phases, monitors, eval cells and background work of the current session",
5
5
  "private": true,
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "OpenCode-compatible HTTP/SSE server that lets OpenChamber drive the pi coding agent (and OmO) over RPC",
5
5
  "type": "module",
6
6
  "license": "MIT",