antechamber 0.2.15 → 0.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/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project uses [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.17] - 2026-10-17
11
+
12
+ ### Fixed
13
+
14
+ - Keep files in the session diff when the agent reports their absolute path through a symlinked workspace, such as
15
+ a macOS temporary directory under `/var`.
16
+ - Make `antechamber doctor` skip running-server records whose process has exited on macOS, not only on Linux.
17
+ - Stop a fast subagent task from staying busy on macOS when its task record changes before the file watcher is
18
+ live.
19
+
20
+ ## [0.2.16] - 2026-10-17
21
+
22
+ ### Fixed
23
+
24
+ - Keep deleted subagent sessions deleted. They came back when their parent's history was read again or when the
25
+ adapter restarted, because OmO still holds their task record.
26
+ - Keep subagent sessions archived after an adapter restart. Archiving a parent with its subagents left the
27
+ subagents active as orphans once the adapter restarted.
28
+
10
29
  ## [0.2.15] - 2026-10-17
11
30
 
12
31
  ### Fixed
@@ -7,6 +7,16 @@ import { readRunningServers } from "../running-servers.js";
7
7
  import { isNewerVersion } from "../self-update.js";
8
8
  import { ADAPTER_VERSION } from "../version.js";
9
9
  import { autoUpdateLauncherPath, autoUpdateLauncherTarget, createContext, isPackageBinary, readSettings, resolveBinary, settingsPath, } from "./context.js";
10
+ /** EPERM means the process exists under another user. */
11
+ function processAlive(pid) {
12
+ try {
13
+ process.kill(pid, 0);
14
+ return true;
15
+ }
16
+ catch (error) {
17
+ return error instanceof Error && "code" in error && error.code === "EPERM";
18
+ }
19
+ }
10
20
  /** Checks capabilities with access/stat only, never creating state or config files. */
11
21
  export function doctor(options = {}, ctx = createContext(), latestVersion) {
12
22
  const checks = [];
@@ -53,7 +63,7 @@ export function doctor(options = {}, ctx = createContext(), latestVersion) {
53
63
  else
54
64
  add("version", "PASS", ADAPTER_VERSION);
55
65
  // A released fix is live only once OpenChamber relaunches the adapter.
56
- const running = readRunningServers(stateDir, ctx.fs, (pid) => ctx.platform === "linux" ? ctx.fs.existsSync(`/proc/${pid}`) : true);
66
+ const running = readRunningServers(stateDir, ctx.fs, processAlive);
57
67
  if (running.length === 0)
58
68
  add("running", "INFO", `no running antechamber server recorded in ${stateDir}`);
59
69
  for (const server of running) {
@@ -26,7 +26,10 @@ async function git(directory, args) {
26
26
  /** A removed file may be absent, but neither traversal nor symlinks may escape. */
27
27
  async function confinedFile(directory, path) {
28
28
  const root = await realpath(directory);
29
- const absolute = resolve(root, path);
29
+ // Map paths spelled through a symlinked workspace (macOS /var -> /private/var) onto the canonical root.
30
+ const lexical = relative(resolve(directory), resolve(directory, path));
31
+ const insideLexical = lexical !== ".." && !lexical.startsWith(`..${sep}`) && !isAbsolute(lexical);
32
+ const absolute = insideLexical ? resolve(root, lexical) : resolve(root, path);
30
33
  const rel = relative(root, absolute);
31
34
  if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
32
35
  return undefined;
@@ -35,6 +35,8 @@ export type TitleSource = "user" | "native" | "fallback" | "generated";
35
35
  export interface SessionRegistryEntry {
36
36
  /** Created by this connector, rather than discovered from another OmO client. */
37
37
  connectorOwned?: true;
38
+ /** A child the user deleted; kept so its task record or parent history cannot list it again. */
39
+ deleted?: true;
38
40
  tree?: SessionTreeState;
39
41
  parentID?: string | undefined;
40
42
  parentMessageID?: string;
@@ -346,6 +346,8 @@ function registryEntry(value) {
346
346
  return false;
347
347
  if (value.connectorOwned !== undefined && value.connectorOwned !== true)
348
348
  return false;
349
+ if (value.deleted !== undefined && value.deleted !== true)
350
+ return false;
349
351
  if (value.tree !== undefined && !sessionTreeState(value.tree))
350
352
  return false;
351
353
  if (value.parentID !== undefined && typeof value.parentID !== "string")
package/dist/sessions.js CHANGED
@@ -344,7 +344,7 @@ export class SessionManager {
344
344
  this.emit(s.info.directory, { type: "session.deleted", properties: { info: s.info } });
345
345
  await this.lifecycle.dormant(s);
346
346
  const piPath = this.subagents.owns(sessionId) || this.monitors.owns(sessionId) ? undefined : (s.piPath ?? s.restored?.piPath);
347
- this.subagents.remove(sessionId);
347
+ const child = this.subagents.remove(sessionId);
348
348
  this.monitors.remove(sessionId);
349
349
  if (piPath !== undefined) {
350
350
  try {
@@ -355,7 +355,9 @@ export class SessionManager {
355
355
  throw error;
356
356
  }
357
357
  }
358
- this.store.remove(sessionId);
358
+ // A deleted child keeps its tombstone; OmO still holds the task record that would list it again.
359
+ if (!child)
360
+ this.store.remove(sessionId);
359
361
  return true;
360
362
  }
361
363
  /**
@@ -391,7 +393,9 @@ export class SessionManager {
391
393
  delete s.info.time.archived; // 0 = unarchive
392
394
  }
393
395
  s.info.time.updated = Date.now();
394
- this.persistMeta(s);
396
+ // Child sessions have no pi path of their own, so persistMeta would skip them.
397
+ if (!this.subagents.save(s.info.id))
398
+ this.persistMeta(s);
395
399
  this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
396
400
  return s.info;
397
401
  }
@@ -1318,6 +1322,8 @@ export class SessionManager {
1318
1322
  /** Resolve a bare session id (e.g. deep link after restart) via known directories. */
1319
1323
  resolveRestored(sessionId) {
1320
1324
  const reg = this.store.get(sessionId);
1325
+ if (reg?.deleted)
1326
+ return undefined;
1321
1327
  if (reg && existsSync(reg.piPath)) {
1322
1328
  return this.adopt({
1323
1329
  piSessionId: reg.piSessionId,
@@ -78,12 +78,16 @@ export declare class SubagentBridge {
78
78
  private publishRefresh;
79
79
  private saveArchive;
80
80
  private fallback;
81
+ /** Save a rename or archive made through the session routes; false when `id` is not a child. */
82
+ save(id: string): boolean;
81
83
  private persist;
82
84
  private emitPart;
83
85
  private closeWatchers;
84
86
  finishCalls(parentID: string, cancelled?: boolean): void;
85
87
  stopParent(parentID: string): void;
86
- remove(id: string): void;
88
+ /** Forget a deleted child and leave a registry tombstone; false when `id` is not a child. */
89
+ remove(id: string): boolean;
90
+ private deleted;
87
91
  dispose(): void;
88
92
  }
89
93
  export {};
package/dist/subagents.js CHANGED
@@ -6,6 +6,9 @@ import { syncSessionCost } from "./pi-event-translation.js";
6
6
  import { ocIdFor, SessionPersistenceError } from "./session-store.js";
7
7
  import { mapTool, resultText, toolMappers } from "./tool-map.js";
8
8
  import { ADAPTER_VERSION } from "./version.js";
9
+ // macOS watches directories through FSEvents, which goes live asynchronously after
10
+ // watch() returns; a task record replaced inside that window raises no event.
11
+ const FSEVENTS_STARTUP_RECHECK_MS = 1_000;
9
12
  function record(value) {
10
13
  return value !== null && typeof value === "object" && !Array.isArray(value);
11
14
  }
@@ -319,6 +322,8 @@ export class SubagentBridge {
319
322
  const id = metadata.taskID;
320
323
  const details = record(metadata.task) ? metadata.task : {};
321
324
  let child = this.children.get(ocIdFor(id));
325
+ if (!child && this.deleted(ocIdFor(id)))
326
+ continue;
322
327
  if (!child) {
323
328
  child = this.create(parent, {
324
329
  id,
@@ -340,6 +345,7 @@ export class SubagentBridge {
340
345
  }
341
346
  }
342
347
  create(parent, task) {
348
+ const archived = this.store.get(ocIdFor(task.id))?.archived;
343
349
  const session = this.managedSession({
344
350
  id: ocIdFor(task.id),
345
351
  parentID: parent.info.id,
@@ -347,7 +353,7 @@ export class SubagentBridge {
347
353
  directory: parent.info.directory,
348
354
  title: typeof task.input.description === "string" ? task.input.description : task.id,
349
355
  version: parent.info.version,
350
- time: { ...parent.info.time },
356
+ time: { ...parent.info.time, ...(archived !== undefined ? { archived } : {}) },
351
357
  });
352
358
  const child = {
353
359
  session,
@@ -373,7 +379,7 @@ export class SubagentBridge {
373
379
  const entry = this.store.get(id);
374
380
  if (!entry)
375
381
  return this.removed.has(id) ? undefined : this.adopt(id);
376
- if (!entry.task || !entry.parentID)
382
+ if (entry.deleted || !entry.task || !entry.parentID)
377
383
  return undefined;
378
384
  const child = this.create(this.detachedParent(entry.parentID, entry.directory, entry), entry.task);
379
385
  child.session.info.title = entry.title ?? child.session.info.title;
@@ -503,6 +509,12 @@ export class SubagentBridge {
503
509
  child.watchers.delete(dir);
504
510
  });
505
511
  child.watchers.set(dir, watcher);
512
+ if (process.platform === "darwin") {
513
+ setTimeout(() => {
514
+ if (child.watchers.get(dir) === watcher)
515
+ this.refresh(child, true);
516
+ }, FSEVENTS_STARTUP_RECHECK_MS).unref();
517
+ }
506
518
  }
507
519
  refresh(child, publish, useRecord = true) {
508
520
  const archive = this.archive(child.task.id);
@@ -682,6 +694,14 @@ export class SubagentBridge {
682
694
  });
683
695
  return this.parse(child.session, `${request}\n${response}\n`);
684
696
  }
697
+ /** Save a rename or archive made through the session routes; false when `id` is not a child. */
698
+ save(id) {
699
+ const child = this.children.get(id);
700
+ if (!child)
701
+ return false;
702
+ this.persist(child);
703
+ return true;
704
+ }
685
705
  persist(child) {
686
706
  const { info } = child.session;
687
707
  try {
@@ -691,6 +711,7 @@ export class SubagentBridge {
691
711
  directory: info.directory,
692
712
  title: info.title,
693
713
  parentID: info.parentID,
714
+ ...(info.time.archived !== undefined ? { archived: info.time.archived } : {}),
694
715
  task: child.task,
695
716
  created: info.time.created,
696
717
  updated: info.time.updated,
@@ -746,14 +767,29 @@ export class SubagentBridge {
746
767
  this.closeWatchers(child);
747
768
  }
748
769
  }
770
+ /** Forget a deleted child and leave a registry tombstone; false when `id` is not a child. */
749
771
  remove(id) {
750
772
  const child = this.children.get(id);
751
773
  if (!child)
752
- return;
774
+ return false;
753
775
  this.closeWatchers(child);
754
776
  child.parts.clear();
755
777
  this.children.delete(id);
756
778
  this.removed.add(id);
779
+ const { info } = child.session;
780
+ this.store.set(id, {
781
+ deleted: true,
782
+ piSessionId: child.session.piSessionId ?? child.task.id,
783
+ piPath: this.archive(child.task.id),
784
+ directory: info.directory,
785
+ ...(info.parentID !== undefined ? { parentID: info.parentID } : {}),
786
+ created: info.time.created,
787
+ updated: info.time.updated,
788
+ });
789
+ return true;
790
+ }
791
+ deleted(id) {
792
+ return this.removed.has(id) || this.store.get(id)?.deleted === true;
757
793
  }
758
794
  dispose() {
759
795
  for (const child of this.children.values()) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-graph",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
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",