@hheei/pi-subagents 0.4.0 → 0.5.1

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.
@@ -36,6 +36,8 @@ export interface HostObservation {
36
36
  readonly identity: HostAttachmentIdentity;
37
37
  readonly alive: boolean;
38
38
  readonly known: boolean;
39
+ /** True when the underlying host container (tab, pane, or surface) no longer exists. */
40
+ readonly containerGone?: boolean;
39
41
  /**
40
42
  * Whether a human is looking at this panel. Absent when the host cannot say, which includes
41
43
  * both hosts without the capability and a lookup that failed just now; the caller must treat
@@ -138,10 +138,13 @@ export function createHerdrHostAdapter(options) {
138
138
  const listed = herdrListingSucceeded(tabs) && herdrListingSucceeded(workspaces)
139
139
  ? herdrPanelFocused(tabs.stdout, workspaces.stdout, target.tabId)
140
140
  : undefined;
141
+ const code = herdrErrorCode(process.stderr) ?? herdrErrorCode(process.stdout);
142
+ const containerGone = code === "pane_not_found" || code === "tab_not_found";
141
143
  const alive = herdrPaneAlive(process);
142
144
  return {
143
145
  process,
144
146
  ...(alive === undefined ? {} : { alive }),
147
+ ...(containerGone ? { containerGone: true } : {}),
145
148
  ...(listed === undefined ? {} : { focused: listed }),
146
149
  };
147
150
  },
@@ -187,7 +190,12 @@ export function createCmuxHostAdapter(options) {
187
190
  const process = await runner.run("cmux", ["--json", "list-panels"], { timeoutMs });
188
191
  if (process.timedOut || process.exitCode !== 0)
189
192
  return { process };
190
- return { process, alive: containsString(process.stdout, attachmentId) };
193
+ const alive = containsString(process.stdout, attachmentId);
194
+ return {
195
+ process,
196
+ alive,
197
+ ...(!alive ? { containerGone: true } : {}),
198
+ };
191
199
  },
192
200
  close: () => runner.run("cmux", ["--json", "close-surface", "--surface", attachmentId], {
193
201
  timeoutMs,
@@ -207,6 +215,7 @@ function hostAttachment(options) {
207
215
  // the caller drop the panel and the runtime evidence, so it is only ever said on purpose.
208
216
  alive: result.alive === true,
209
217
  known: result.alive !== undefined,
218
+ ...(result.containerGone ? { containerGone: true } : {}),
210
219
  ...(focused === undefined ? {} : { focused }),
211
220
  detail: result.process.stderr || result.process.stdout,
212
221
  };
package/dist/runtime.d.ts CHANGED
@@ -46,19 +46,9 @@ export declare function launchChild(options: LaunchChildOptions): Promise<Launch
46
46
  export interface OpenChildPanelOptions extends LaunchChildOptions {
47
47
  readonly host: HostAdapter;
48
48
  }
49
- /**
50
- * What a launch left behind. `failure` is what the caller reports. A handle is kept when the host
51
- * still has a tab, so stop can close it. It does not by itself forbid another send.
52
- */
53
49
  export interface LaunchOutcome<T> {
54
50
  readonly handle?: T;
55
51
  readonly failure?: string;
56
52
  }
57
- /**
58
- * Opens one child in a host panel and waits for it to connect. The host owns this process, so the
59
- * parent holds no runtime handle for it; a panel that never yields a bridge is closed again instead
60
- * of being left as a process nobody can control.
61
- */
62
53
  export declare function openChildPanel(options: OpenChildPanelOptions): Promise<LaunchOutcome<HostAttachment>>;
63
- /** Creates the runtime directory if needed; the socket itself is 0600 in a 0700 directory. */
64
54
  export declare function prepareRuntimeDirectory(diagnose?: (message: string) => void): Promise<string>;
package/dist/runtime.js CHANGED
@@ -127,8 +127,6 @@ export async function launchChild(options) {
127
127
  }
128
128
  await runtime.terminate();
129
129
  if (runtime.alive) {
130
- // The process outlived termination, so it is not gone and the caller keeps the handle: it is
131
- // the only thing that can try again.
132
130
  await bindProcess(options.registry, record.subagentId, runtime.pid);
133
131
  return {
134
132
  handle: runtime,
@@ -139,11 +137,6 @@ export async function launchChild(options) {
139
137
  await commitLease(options.registry, record.subagentId, { type: "runtime-gone", outcome: "clear" }, undefined, plan.runtimeIdentity);
140
138
  return { failure: `Child ${record.subagentId} never connected to the parent bridge` };
141
139
  }
142
- /**
143
- * Opens one child in a host panel and waits for it to connect. The host owns this process, so the
144
- * parent holds no runtime handle for it; a panel that never yields a bridge is closed again instead
145
- * of being left as a process nobody can control.
146
- */
147
140
  export async function openChildPanel(options) {
148
141
  const record = options.record;
149
142
  const plan = planChildRuntime(record, options.tokens, "panel");
@@ -153,28 +146,51 @@ export async function openChildPanel(options) {
153
146
  attachment = await options.host.open(plan.spec);
154
147
  }
155
148
  catch (error) {
156
- // Opening is not atomic: the host may have created the panel and started the process before
157
- // failing to report it. The token is kept so a child that does come up can still be adopted,
158
- // and the caller is told the runtime is unconfirmed instead of that nothing was started.
159
149
  return {
160
150
  failure: `Child ${record.subagentId} could not be opened in a ${options.host.kind} panel (${errorMessage(error)}); a panel may have been created, so close it if it is still there`,
161
151
  };
162
152
  }
153
+ const earlyExitAbort = new AbortController();
154
+ const combinedSignal = options.signal
155
+ ? AbortSignal.any([options.signal, earlyExitAbort.signal])
156
+ : earlyExitAbort.signal;
157
+ let childAppeared = false;
158
+ let earlyExitObserved;
159
+ const probeInterval = setInterval(async () => {
160
+ try {
161
+ const obs = await attachment.observe();
162
+ if (!obs.known)
163
+ return;
164
+ if (obs.containerGone) {
165
+ earlyExitObserved = obs;
166
+ earlyExitAbort.abort();
167
+ return;
168
+ }
169
+ if (obs.alive) {
170
+ childAppeared = true;
171
+ }
172
+ else if (childAppeared) {
173
+ earlyExitObserved = obs;
174
+ earlyExitAbort.abort();
175
+ }
176
+ }
177
+ catch { }
178
+ }, 200);
163
179
  let connected;
164
180
  try {
165
- connected = await options.waitForBridge(record.subagentId, options.signal);
181
+ connected = await options.waitForBridge(record.subagentId, combinedSignal);
166
182
  }
167
183
  catch (error) {
168
184
  options.diagnose?.(`bridge wait failed: ${errorMessage(error)}`);
169
185
  connected = false;
170
186
  }
187
+ finally {
188
+ clearInterval(probeInterval);
189
+ }
171
190
  if (connected) {
172
191
  await bindPanel(options.registry, record.subagentId, options.host.kind, attachment.identity.attachmentId);
173
192
  return { handle: attachment };
174
193
  }
175
- // The child never dialed in, so the panel is closed again. A host that cannot confirm it is gone
176
- // keeps the handle, so stop can try the close again, and the token, so a child that does come up
177
- // can still be adopted. The handle does not block a later send.
178
194
  const cleanup = await attachment.cleanup();
179
195
  const observed = await attachment.observe();
180
196
  if (!(observed.known && !observed.alive)) {
@@ -186,9 +202,9 @@ export async function openChildPanel(options) {
186
202
  }
187
203
  options.tokens.forget(plan.runtimeIdentity);
188
204
  await commitLease(options.registry, record.subagentId, { type: "runtime-gone", outcome: "clear" }, undefined, plan.runtimeIdentity);
189
- return { failure: `Child ${record.subagentId} never connected to the parent bridge` };
205
+ const detail = earlyExitObserved?.detail ? ` (${earlyExitObserved.detail.trim()})` : "";
206
+ return { failure: `Child ${record.subagentId} never connected to the parent bridge${detail}` };
190
207
  }
191
- /** Creates the runtime directory if needed; the socket itself is 0600 in a 0700 directory. */
192
208
  export async function prepareRuntimeDirectory(diagnose) {
193
209
  const directory = runtimeDirectory();
194
210
  await mkdir(directory, { recursive: true, mode: 0o700 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hheei/pi-subagents",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Independent background Pi subagent sessions with durable identity and RPC control.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,7 +35,7 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@hheei/pi-ext-core": "^0.5.0"
38
+ "@hheei/pi-ext-core": "^0.6.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@earendil-works/pi-coding-agent": ">=1.3.0",