@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.
- package/dist/host-adapter.d.ts +2 -0
- package/dist/host-adapter.js +10 -1
- package/dist/runtime.d.ts +0 -10
- package/dist/runtime.js +32 -16
- package/package.json +2 -2
package/dist/host-adapter.d.ts
CHANGED
|
@@ -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
|
package/dist/host-adapter.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
38
|
+
"@hheei/pi-ext-core": "^0.6.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@earendil-works/pi-coding-agent": ">=1.3.0",
|