@fitchmultz/pi-subagents 0.50.0 → 0.52.0
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 +63 -2
- package/README.md +36 -5
- package/dist/agent/child-runtime.js +80 -0
- package/dist/agent/contracts.js +15 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +212 -192
- package/dist/agent/journal-tail.js +115 -156
- package/dist/agent/launch.js +20 -8
- package/dist/agent/legacy.js +65 -23
- package/dist/agent/notifications.js +138 -47
- package/dist/agent/observation-state.js +246 -0
- package/dist/agent/observation-view.js +258 -0
- package/dist/agent/observation.js +350 -0
- package/dist/agent/observe-card.js +243 -0
- package/dist/agent/observe-chrome.js +85 -0
- package/dist/agent/observe-client.js +285 -0
- package/dist/agent/observe-layout.js +65 -0
- package/dist/agent/observe-reading.js +84 -0
- package/dist/agent/observe-screen.js +400 -96
- package/dist/agent/observe-transcript.js +382 -0
- package/dist/agent/observe.js +250 -43
- package/dist/agent/parking.js +253 -0
- package/dist/agent/process.js +228 -28
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +229 -126
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +47 -12
- package/dist/agent/tool-schemas.js +20 -0
- package/dist/agent/typebox.js +10 -5
- package/dist/agent/usage.js +0 -115
- package/dist/agent/view.js +46 -16
- package/dist/agent/wake.js +99 -0
- package/dist/build-info.js +1 -1
- package/dist/intercom-client.d.ts +26 -0
- package/dist/intercom-client.js +45 -0
- package/dist/pi-intercom/agent-dir.d.ts +1 -0
- package/dist/pi-intercom/bridge-session.js +2 -1
- package/dist/pi-intercom/broker/broker.js +62 -43
- package/dist/pi-intercom/broker/client-last.d.ts +24 -0
- package/dist/pi-intercom/broker/client-last.js +42 -5
- package/dist/pi-intercom/broker/client-park.d.ts +9 -0
- package/dist/pi-intercom/broker/client-park.js +21 -0
- package/dist/pi-intercom/broker/client.d.ts +59 -0
- package/dist/pi-intercom/broker/client.js +55 -29
- package/dist/pi-intercom/broker/connection.d.ts +29 -0
- package/dist/pi-intercom/broker/framing.d.ts +17 -0
- package/dist/pi-intercom/broker/framing.js +10 -7
- package/dist/pi-intercom/broker/guard.js +23 -0
- package/dist/pi-intercom/broker/last.d.ts +30 -0
- package/dist/pi-intercom/broker/last.js +87 -40
- package/dist/pi-intercom/broker/parked.js +107 -0
- package/dist/pi-intercom/broker/paths.d.ts +13 -0
- package/dist/pi-intercom/broker/paths.js +35 -12
- package/dist/pi-intercom/broker/pid.js +5 -1
- package/dist/pi-intercom/broker/server-connection.js +20 -1
- package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
- package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
- package/dist/pi-intercom/broker/targets.d.ts +9 -0
- package/dist/pi-intercom/connection.js +71 -20
- package/dist/pi-intercom/identity.js +6 -5
- package/dist/pi-intercom/inbound-delivery.js +16 -60
- package/dist/pi-intercom/inbound-journal.js +83 -6
- package/dist/pi-intercom/inbound-record.js +18 -1
- package/dist/pi-intercom/inbound.js +22 -37
- package/dist/pi-intercom/index.js +41 -0
- package/dist/pi-intercom/last-message.js +4 -16
- package/dist/pi-intercom/lifecycle.js +88 -3
- package/dist/pi-intercom/outbound.js +5 -15
- package/dist/pi-intercom/reply-tracker.js +7 -6
- package/dist/pi-intercom/reply-wait.js +2 -2
- package/dist/pi-intercom/session-targets.d.ts +24 -0
- package/dist/pi-intercom/tool-registration.js +4 -1
- package/dist/pi-intercom/tool-results.js +5 -7
- package/dist/pi-intercom/tool-schemas.js +12 -26
- package/dist/pi-intercom/types.d.ts +212 -0
- package/dist/pi-intercom/types.js +54 -3
- package/dist/pi-intercom/validation.d.ts +12 -0
- package/dist/pi-intercom/validation.js +3 -0
- package/dist/shared/bounded-json.js +151 -0
- package/dist/shared/compact-cards.js +66 -0
- package/dist/shared/journal-frames.js +3 -6
- package/dist/shared/json-projection.js +267 -129
- package/dist/shared/native-journal.js +123 -56
- package/dist/shared/parking.js +12 -0
- package/dist/shared/post-exit-stdio-guard.js +120 -7
- package/dist/shared/tui-action.js +29 -0
- package/dist/shared/unknown.d.ts +9 -0
- package/docs/code-quality.md +5 -3
- package/docs/development.md +42 -3
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +228 -7
- package/docs/performance.md +118 -14
- package/docs/reference.md +168 -20
- package/docs/retired-history-maintenance.md +252 -0
- package/package.json +28 -15
- package/scripts/archive-retired-history.mjs +464 -0
- package/scripts/build.mjs +48 -28
- package/scripts/measure-size.mjs +1 -1
- package/scripts/native-package-smoke.mjs +153 -12
- package/scripts/package-smoke.mjs +80 -11
- package/scripts/prepare.mjs +2 -2
- package/scripts/real-pi-live.mjs +377 -0
- package/scripts/real-pi-processes.mjs +6 -1
- package/scripts/real-pi-smoke.mjs +6 -282
- package/scripts/run-tests.mjs +4 -2
- package/scripts/typescript.mjs +35 -0
- package/skills/pi-intercom/SKILL.md +2 -1
- package/skills/pi-subagents/SKILL.md +23 -5
- package/src/agent/child-runtime.ts +105 -0
- package/src/agent/contracts.ts +18 -34
- package/src/agent/host.ts +39 -20
- package/src/agent/index.ts +234 -207
- package/src/agent/journal-tail.ts +141 -153
- package/src/agent/launch.ts +33 -9
- package/src/agent/legacy.ts +85 -31
- package/src/agent/notifications.ts +192 -67
- package/src/agent/observation-state.ts +277 -0
- package/src/agent/observation-view.ts +267 -0
- package/src/agent/observation.ts +409 -0
- package/src/agent/observe-card.ts +304 -0
- package/src/agent/observe-chrome.ts +170 -0
- package/src/agent/observe-client.ts +318 -0
- package/src/agent/observe-layout.ts +79 -0
- package/src/agent/observe-reading.ts +97 -0
- package/src/agent/observe-screen.ts +443 -129
- package/src/agent/observe-transcript.ts +434 -0
- package/src/agent/observe.ts +304 -71
- package/src/agent/parking.ts +341 -0
- package/src/agent/process.ts +280 -37
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +227 -149
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +44 -11
- package/src/agent/tool-schemas.ts +33 -0
- package/src/agent/typebox.ts +12 -7
- package/src/agent/usage.ts +0 -130
- package/src/agent/view.ts +70 -20
- package/src/agent/wake.ts +107 -0
- package/src/intercom-client.ts +108 -0
- package/src/pi-intercom/bridge-session.ts +2 -2
- package/src/pi-intercom/broker/broker.ts +64 -53
- package/src/pi-intercom/broker/client-last.ts +65 -12
- package/src/pi-intercom/broker/client-park.ts +23 -0
- package/src/pi-intercom/broker/client.ts +63 -45
- package/src/pi-intercom/broker/framing.ts +11 -8
- package/src/pi-intercom/broker/guard.ts +33 -0
- package/src/pi-intercom/broker/last.ts +131 -43
- package/src/pi-intercom/broker/parked.ts +130 -0
- package/src/pi-intercom/broker/paths.ts +39 -12
- package/src/pi-intercom/broker/pid.ts +6 -1
- package/src/pi-intercom/broker/server-connection.ts +27 -2
- package/src/pi-intercom/connection.ts +103 -39
- package/src/pi-intercom/identity.ts +8 -5
- package/src/pi-intercom/inbound-delivery.ts +20 -66
- package/src/pi-intercom/inbound-journal.ts +103 -7
- package/src/pi-intercom/inbound-record.ts +21 -4
- package/src/pi-intercom/inbound.ts +32 -49
- package/src/pi-intercom/index.ts +44 -0
- package/src/pi-intercom/last-message.ts +11 -20
- package/src/pi-intercom/lifecycle.ts +117 -14
- package/src/pi-intercom/outbound.ts +7 -16
- package/src/pi-intercom/reply-tracker.ts +8 -6
- package/src/pi-intercom/reply-wait.ts +3 -2
- package/src/pi-intercom/runtime-types.ts +0 -1
- package/src/pi-intercom/tool-registration.ts +4 -1
- package/src/pi-intercom/tool-results.ts +10 -9
- package/src/pi-intercom/tool-schemas.ts +12 -44
- package/src/pi-intercom/types.ts +115 -4
- package/src/pi-intercom/validation.ts +4 -0
- package/src/shared/bounded-json.ts +162 -0
- package/src/shared/compact-cards.ts +118 -0
- package/src/shared/journal-frames.ts +3 -19
- package/src/shared/json-projection.ts +267 -147
- package/src/shared/native-journal.ts +157 -75
- package/src/shared/parking.ts +22 -0
- package/src/shared/post-exit-stdio-guard.ts +165 -7
- package/src/shared/tui-action.ts +39 -0
- package/tsconfig.client.json +8 -0
- package/tsconfig.json +1 -0
- package/src/shared/stream-json.d.ts +0 -10
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { AgentSession, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { parkedSchema, type Launch, type Parked } from "./contracts.ts";
|
|
6
|
+
import { errorText, loadSDK, type WorkingState } from "./host.ts";
|
|
7
|
+
import { conversation, latest, launchAt, list, read, runDirectory, write } from "./storage.ts";
|
|
8
|
+
import { alive, shells, wakeActor } from "./process.ts";
|
|
9
|
+
import {
|
|
10
|
+
isHistoryRequest,
|
|
11
|
+
PARK,
|
|
12
|
+
PARKED_HISTORY,
|
|
13
|
+
WAKE,
|
|
14
|
+
type ParkRequest,
|
|
15
|
+
} from "../shared/parking.ts";
|
|
16
|
+
import { nativePeerId } from "../pi-intercom/session-targets.ts";
|
|
17
|
+
import type { NativeHistory } from "../pi-intercom/last-message.ts";
|
|
18
|
+
|
|
19
|
+
type SDK = Awaited<ReturnType<typeof loadSDK>>;
|
|
20
|
+
type Restore = NonNullable<SDK["readWorkingSession"]>;
|
|
21
|
+
export type Runtime = Awaited<ReturnType<SDK["createAgentSessionRuntime"]>>;
|
|
22
|
+
type SessionManager = Parameters<SDK["createAgentSessionRuntime"]>[1]["sessionManager"];
|
|
23
|
+
interface WorkingHold {
|
|
24
|
+
readonly state: WorkingState;
|
|
25
|
+
readonly sleepReady: boolean;
|
|
26
|
+
readonly blockers: readonly string[];
|
|
27
|
+
readonly assertHeld: () => void;
|
|
28
|
+
readonly release: () => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/** Fork-only native cut and strict final exit; official Pi children stay resident. */
|
|
31
|
+
type Parkable = AgentSession & {
|
|
32
|
+
readonly acquireWorkingSession: (options: {
|
|
33
|
+
boundary: "settled";
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
}) => Promise<WorkingHold>;
|
|
36
|
+
};
|
|
37
|
+
/** The runner's view of its own work; parking never admits or runs commands itself. */
|
|
38
|
+
interface Resident {
|
|
39
|
+
readonly pi: ExtensionAPI | undefined;
|
|
40
|
+
readonly busy: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* False once queued commands, work, stopping or a viewer appeared. Never admits: a prompt
|
|
43
|
+
* started under the native hold fails, so the runner admits only after release.
|
|
44
|
+
*/
|
|
45
|
+
readonly quiet: () => boolean;
|
|
46
|
+
readonly errors: string[];
|
|
47
|
+
/** Aborts preparation once the owner stops or dies. */
|
|
48
|
+
readonly signal: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
interface Finalizing {
|
|
51
|
+
readonly setFinalization: (
|
|
52
|
+
finalization: (session: AgentSession) => Promise<void>,
|
|
53
|
+
options: { strictShutdown: boolean },
|
|
54
|
+
) => void;
|
|
55
|
+
}
|
|
56
|
+
function parkable(session: AgentSession): session is Parkable {
|
|
57
|
+
return typeof Reflect.get(session, "acquireWorkingSession") === "function";
|
|
58
|
+
}
|
|
59
|
+
function finalizing(runtime: Runtime): runtime is Runtime & Finalizing {
|
|
60
|
+
return typeof Reflect.get(runtime, "setFinalization") === "function";
|
|
61
|
+
}
|
|
62
|
+
function fence(pi: ExtensionAPI, park: boolean): Promise<boolean> {
|
|
63
|
+
let fenced = Promise.resolve(false);
|
|
64
|
+
pi.events.emit(PARK, {
|
|
65
|
+
park,
|
|
66
|
+
reply: (parked) => {
|
|
67
|
+
fenced = parked;
|
|
68
|
+
},
|
|
69
|
+
} satisfies ParkRequest);
|
|
70
|
+
return fenced;
|
|
71
|
+
}
|
|
72
|
+
async function cut(
|
|
73
|
+
pi: ExtensionAPI,
|
|
74
|
+
session: Parkable,
|
|
75
|
+
{ quiet, errors, signal }: Omit<Resident, "pi" | "busy">,
|
|
76
|
+
): Promise<WorkingHold | undefined> {
|
|
77
|
+
try {
|
|
78
|
+
// Save handlers may legitimately append entries; only the pre-cut leaf proves idleness.
|
|
79
|
+
if ((await fence(pi, true)) && quiet() && session.isIdle) {
|
|
80
|
+
return await session.acquireWorkingSession({ boundary: "settled", signal });
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
errors.push(errorText(error));
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const PARK_AFTER_MS = 5 * 60_000;
|
|
88
|
+
const digest = (file: string): string =>
|
|
89
|
+
createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
90
|
+
|
|
91
|
+
/** Parked authority names this session and exactly the saved generation's state bytes. */
|
|
92
|
+
function exact(
|
|
93
|
+
restore: Restore,
|
|
94
|
+
parked: Parked,
|
|
95
|
+
state: string,
|
|
96
|
+
launch: Launch,
|
|
97
|
+
): WorkingState | undefined {
|
|
98
|
+
if (
|
|
99
|
+
parked.sessionId !== launch.sessionId ||
|
|
100
|
+
parked.state !== state ||
|
|
101
|
+
digest(state) !== parked.digest
|
|
102
|
+
) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const saved = restore(state);
|
|
106
|
+
return saved.sessionFile === launch.sessionFile &&
|
|
107
|
+
saved.header.id === launch.sessionId &&
|
|
108
|
+
saved.leafId === parked.leafId
|
|
109
|
+
? saved
|
|
110
|
+
: undefined;
|
|
111
|
+
}
|
|
112
|
+
/** This child's exact parked generation, if any; mismatched authority restores nothing. */
|
|
113
|
+
export function parkedState(
|
|
114
|
+
dir: string,
|
|
115
|
+
launch: Launch,
|
|
116
|
+
restore: Restore | undefined,
|
|
117
|
+
): { readonly state: string; readonly saved: WorkingState } | undefined {
|
|
118
|
+
const parked = read(path.join(dir, "parked.json"), parkedSchema);
|
|
119
|
+
if (!parked) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
if (!restore) {
|
|
123
|
+
throw new Error("Parked working state needs the Pi fork that saved it; nothing was reopened.");
|
|
124
|
+
}
|
|
125
|
+
// Native working state requires a canonical parent, not a symlinked alias like macOS /var.
|
|
126
|
+
const state = path.join(fs.realpathSync(dir), `working-${parked.token}.json`);
|
|
127
|
+
const saved = exact(restore, parked, state, launch);
|
|
128
|
+
if (!saved) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"Parked working state is not this child's saved generation; nothing was reopened.",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return { state, saved };
|
|
134
|
+
}
|
|
135
|
+
/** One actor generation's idle parking and exact restore; commands stay with the runner. */
|
|
136
|
+
export class Parking {
|
|
137
|
+
saved?: WorkingState;
|
|
138
|
+
private restored?: string;
|
|
139
|
+
private since = 0;
|
|
140
|
+
private leaf?: string | null;
|
|
141
|
+
private readonly base: string;
|
|
142
|
+
private readonly launch: Launch;
|
|
143
|
+
private readonly token: string;
|
|
144
|
+
private readonly sdk: SDK;
|
|
145
|
+
constructor(base: string, launch: Launch, token: string, sdk: SDK) {
|
|
146
|
+
this.base = base;
|
|
147
|
+
this.launch = launch;
|
|
148
|
+
this.token = token;
|
|
149
|
+
this.sdk = sdk;
|
|
150
|
+
}
|
|
151
|
+
private get dir(): string {
|
|
152
|
+
return runDirectory(this.base, this.launch.rootId);
|
|
153
|
+
}
|
|
154
|
+
private get file(): string {
|
|
155
|
+
return path.join(this.dir, "parked.json");
|
|
156
|
+
}
|
|
157
|
+
/** Parked authority is exact state or nothing; never a journal-only reopen. */
|
|
158
|
+
open(): SessionManager {
|
|
159
|
+
const { sdk, launch } = this;
|
|
160
|
+
const { openWorkingSession: restore } = sdk;
|
|
161
|
+
const parked = parkedState(this.dir, launch, restore && sdk.readWorkingSession);
|
|
162
|
+
if (!parked || !restore) {
|
|
163
|
+
return sdk.SessionManager.open(launch.sessionFile, undefined, launch.cwd);
|
|
164
|
+
}
|
|
165
|
+
this.saved = parked.saved;
|
|
166
|
+
this.restored = parked.state;
|
|
167
|
+
return restore(parked.saved);
|
|
168
|
+
}
|
|
169
|
+
/** The ready restored generation owns this state now; a later park writes a new cut. */
|
|
170
|
+
adopt(): void {
|
|
171
|
+
if (this.restored !== undefined) {
|
|
172
|
+
fs.rmSync(this.file);
|
|
173
|
+
fs.rmSync(this.restored, { force: true });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** True once this generation has exited its runtime with or without parked authority. */
|
|
177
|
+
async park(runtime: Runtime, actor: Resident): Promise<boolean> {
|
|
178
|
+
if (
|
|
179
|
+
!actor.pi ||
|
|
180
|
+
!this.due(runtime.session, actor.busy) ||
|
|
181
|
+
shells(this.base, this.launch, this.token).some(alive)
|
|
182
|
+
) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const hold = await this.prepare(runtime, actor.pi, actor);
|
|
186
|
+
if (hold) {
|
|
187
|
+
await this.commit(runtime, hold, actor.errors);
|
|
188
|
+
}
|
|
189
|
+
return hold !== undefined;
|
|
190
|
+
}
|
|
191
|
+
/** Final idle: no run, turn, journal change or viewer since the period began. */
|
|
192
|
+
private due(session: AgentSession, busy: boolean): boolean {
|
|
193
|
+
const leaf = session.sessionManager.getLeafId();
|
|
194
|
+
if (busy || !session.isIdle || leaf !== this.leaf) {
|
|
195
|
+
this.leaf = leaf;
|
|
196
|
+
this.since = performance.now();
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return performance.now() - this.since >= PARK_AFTER_MS;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Fence broker input, then hold a settled sleep-ready cut of the same quiet idle leaf. Any
|
|
203
|
+
* failure or raced command releases, unfences and keeps this actor resident.
|
|
204
|
+
*/
|
|
205
|
+
private async prepare(
|
|
206
|
+
runtime: Runtime,
|
|
207
|
+
pi: ExtensionAPI,
|
|
208
|
+
{ quiet, errors, signal }: Resident,
|
|
209
|
+
): Promise<WorkingHold | undefined> {
|
|
210
|
+
const { session } = runtime;
|
|
211
|
+
this.since = performance.now();
|
|
212
|
+
if (!parkable(session) || !this.sdk.writeWorkingSession || !finalizing(runtime)) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
// A launcher that enqueues after this marker waits for exit, then wakes the saved state.
|
|
216
|
+
const marker = path.join(this.dir, "actor.lock", "parking.json");
|
|
217
|
+
write(marker, { at: Date.now() });
|
|
218
|
+
const hold = await cut(pi, session, {
|
|
219
|
+
quiet: () => quiet() && session.sessionManager.getLeafId() === this.leaf,
|
|
220
|
+
errors,
|
|
221
|
+
signal,
|
|
222
|
+
});
|
|
223
|
+
// Revalidate after the awaited fence and save, immediately before the irreversible commit.
|
|
224
|
+
if (hold?.sleepReady === true && quiet()) {
|
|
225
|
+
return hold;
|
|
226
|
+
}
|
|
227
|
+
await hold?.release();
|
|
228
|
+
await fence(pi, false);
|
|
229
|
+
fs.rmSync(marker, { force: true });
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
/** Commit: native strict exit, then capture; failure leaves no parked authority. */
|
|
233
|
+
private async commit(runtime: Runtime, hold: WorkingHold, errors: string[]): Promise<void> {
|
|
234
|
+
const save = this.sdk.writeWorkingSession;
|
|
235
|
+
if (save && finalizing(runtime)) {
|
|
236
|
+
runtime.setFinalization((final) => this.capture(final, save), { strictShutdown: true });
|
|
237
|
+
}
|
|
238
|
+
// No await between release and dispose: native final exit fences new turns in this tick.
|
|
239
|
+
const released = hold.release();
|
|
240
|
+
const disposed = runtime.dispose();
|
|
241
|
+
try {
|
|
242
|
+
await Promise.all([released, disposed]);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
errors.push(errorText(error));
|
|
245
|
+
}
|
|
246
|
+
if (read(this.file, parkedSchema)?.token !== this.token) {
|
|
247
|
+
// Truthful boundary: retained input stays queued and nothing restarts implicitly.
|
|
248
|
+
write(path.join(this.dir, "park-failed.json"), { token: this.token, errors });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
private async capture(
|
|
252
|
+
session: AgentSession,
|
|
253
|
+
save: (file: string, state: WorkingState) => void,
|
|
254
|
+
): Promise<void> {
|
|
255
|
+
if (!parkable(session)) {
|
|
256
|
+
throw new Error("Native working state is unavailable.");
|
|
257
|
+
}
|
|
258
|
+
const hold = await session.acquireWorkingSession({ boundary: "settled" });
|
|
259
|
+
try {
|
|
260
|
+
if (!hold.sleepReady) {
|
|
261
|
+
throw new Error(`Final working state is not sleep-ready: ${hold.blockers.join("; ")}`);
|
|
262
|
+
}
|
|
263
|
+
const state = path.join(fs.realpathSync(this.dir), `working-${this.token}.json`);
|
|
264
|
+
save(state, hold.state);
|
|
265
|
+
hold.assertHeld();
|
|
266
|
+
write(this.file, {
|
|
267
|
+
token: this.token,
|
|
268
|
+
state,
|
|
269
|
+
digest: digest(state),
|
|
270
|
+
sessionId: this.launch.sessionId,
|
|
271
|
+
leafId: hold.state.leafId,
|
|
272
|
+
});
|
|
273
|
+
} finally {
|
|
274
|
+
await hold.release();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
interface Runs {
|
|
279
|
+
readonly base: string;
|
|
280
|
+
readonly owner: string;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The broker names a parked child by native peer ID; only this owner's conversations match. */
|
|
284
|
+
function ownedRun(runs: Runs | undefined, peer: unknown): Launch | undefined {
|
|
285
|
+
const launch =
|
|
286
|
+
runs && typeof peer === "string"
|
|
287
|
+
? list(runs.base)
|
|
288
|
+
.map((id) => launchAt(runs.base, id, runs.owner))
|
|
289
|
+
.find((candidate) => nativePeerId(candidate.sessionId) === peer)
|
|
290
|
+
: undefined;
|
|
291
|
+
return launch && runs && latest(runs.base, launch);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The captured leaf's root-to-leaf path, as a live session manager reports it. */
|
|
295
|
+
function branchOf(state: WorkingState): ReturnType<NativeHistory["getBranch"]> {
|
|
296
|
+
const entries = new Map(state.entries.map((entry) => [entry.id, entry]));
|
|
297
|
+
const branch = [];
|
|
298
|
+
for (
|
|
299
|
+
let entry = entries.get(state.leafId ?? "");
|
|
300
|
+
entry !== undefined;
|
|
301
|
+
entry = entries.get(entry.parentId ?? "")
|
|
302
|
+
) {
|
|
303
|
+
branch.unshift(entry);
|
|
304
|
+
}
|
|
305
|
+
return branch;
|
|
306
|
+
}
|
|
307
|
+
async function history(runs: Runs | undefined, peer: string): Promise<NativeHistory | undefined> {
|
|
308
|
+
const launch = ownedRun(runs, peer);
|
|
309
|
+
const restore = launch && (await loadSDK(launch.sdk)).readWorkingSession;
|
|
310
|
+
const state =
|
|
311
|
+
runs && launch && parkedState(conversation(runs.base, launch), launch, restore)?.saved;
|
|
312
|
+
return state && { getBranch: () => branchOf(state) };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Wakes this owner's parked children for broker input and reads their saved history.
|
|
317
|
+
* `runs` is undefined while this session owns no children.
|
|
318
|
+
*/
|
|
319
|
+
export function serveParked(
|
|
320
|
+
pi: ExtensionAPI,
|
|
321
|
+
runs: () => Runs | undefined,
|
|
322
|
+
signal: AbortSignal,
|
|
323
|
+
): void {
|
|
324
|
+
pi.events.on(WAKE, (peer) => {
|
|
325
|
+
// On failure, retained input stays queued at the broker until the next main registration.
|
|
326
|
+
(async () => {
|
|
327
|
+
const current = runs();
|
|
328
|
+
const launch = ownedRun(current, peer);
|
|
329
|
+
if (current && launch) {
|
|
330
|
+
await wakeActor(current.base, launch, signal);
|
|
331
|
+
}
|
|
332
|
+
})().catch((error: unknown) => {
|
|
333
|
+
console.error(`Agent wake: ${errorText(error)}`);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
pi.events.on(PARKED_HISTORY, (request) => {
|
|
337
|
+
if (isHistoryRequest(request)) {
|
|
338
|
+
request.reply(history(runs(), request.peer));
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|