@inhuman.tools/harness-sdk 0.0.0-stage → 0.2.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/LICENSE.md +7 -0
- package/dist/client-agents-tree.d.ts +123 -0
- package/dist/client-agents-tree.d.ts.map +1 -0
- package/dist/client-agents-tree.js +504 -0
- package/dist/client-agents-tree.js.map +1 -0
- package/dist/client-agents.d.ts +50 -0
- package/dist/client-agents.d.ts.map +1 -0
- package/dist/client-agents.js +299 -0
- package/dist/client-agents.js.map +1 -0
- package/dist/client.d.ts +89 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +589 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/launch/continue-pick.d.ts +17 -0
- package/dist/launch/continue-pick.d.ts.map +1 -0
- package/dist/launch/continue-pick.js +20 -0
- package/dist/launch/continue-pick.js.map +1 -0
- package/dist/launch/index.d.ts +6 -0
- package/dist/launch/index.d.ts.map +1 -0
- package/dist/launch/index.js +6 -0
- package/dist/launch/index.js.map +1 -0
- package/dist/launch/model-flags.d.ts +44 -0
- package/dist/launch/model-flags.d.ts.map +1 -0
- package/dist/launch/model-flags.js +216 -0
- package/dist/launch/model-flags.js.map +1 -0
- package/dist/launch/streams.d.ts +15 -0
- package/dist/launch/streams.d.ts.map +1 -0
- package/dist/launch/streams.js +2 -0
- package/dist/launch/streams.js.map +1 -0
- package/dist/launch/term-continue.d.ts +13 -0
- package/dist/launch/term-continue.d.ts.map +1 -0
- package/dist/launch/term-continue.js +17 -0
- package/dist/launch/term-continue.js.map +1 -0
- package/dist/launch/term-session-key.d.ts +8 -0
- package/dist/launch/term-session-key.d.ts.map +1 -0
- package/dist/launch/term-session-key.js +76 -0
- package/dist/launch/term-session-key.js.map +1 -0
- package/dist/observable-abort-signal.d.ts +7 -0
- package/dist/observable-abort-signal.d.ts.map +1 -0
- package/dist/observable-abort-signal.js +56 -0
- package/dist/observable-abort-signal.js.map +1 -0
- package/dist/open-external.d.ts +2 -0
- package/dist/open-external.d.ts.map +1 -0
- package/dist/open-external.js +24 -0
- package/dist/open-external.js.map +1 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +16 -0
- package/dist/paths.js.map +1 -0
- package/dist/text/agent-cover.d.ts +43 -0
- package/dist/text/agent-cover.d.ts.map +1 -0
- package/dist/text/agent-cover.js +123 -0
- package/dist/text/agent-cover.js.map +1 -0
- package/dist/text/format-shell.d.ts +5 -0
- package/dist/text/format-shell.d.ts.map +1 -0
- package/dist/text/format-shell.js +53 -0
- package/dist/text/format-shell.js.map +1 -0
- package/dist/text/identity.d.ts +67 -0
- package/dist/text/identity.d.ts.map +1 -0
- package/dist/text/identity.js +433 -0
- package/dist/text/identity.js.map +1 -0
- package/dist/text/index.d.ts +14 -0
- package/dist/text/index.d.ts.map +1 -0
- package/dist/text/index.js +15 -0
- package/dist/text/index.js.map +1 -0
- package/dist/text/liveness.d.ts +62 -0
- package/dist/text/liveness.d.ts.map +1 -0
- package/dist/text/liveness.js +270 -0
- package/dist/text/liveness.js.map +1 -0
- package/dist/text/mcp-identity.d.ts +25 -0
- package/dist/text/mcp-identity.d.ts.map +1 -0
- package/dist/text/mcp-identity.js +133 -0
- package/dist/text/mcp-identity.js.map +1 -0
- package/dist/text/pretty-identity.d.ts +15 -0
- package/dist/text/pretty-identity.d.ts.map +1 -0
- package/dist/text/pretty-identity.js +112 -0
- package/dist/text/pretty-identity.js.map +1 -0
- package/dist/text/sugarcoat.d.ts +29 -0
- package/dist/text/sugarcoat.d.ts.map +1 -0
- package/dist/text/sugarcoat.js +148 -0
- package/dist/text/sugarcoat.js.map +1 -0
- package/dist/text/unwrap-program.d.ts +2 -0
- package/dist/text/unwrap-program.d.ts.map +1 -0
- package/dist/text/unwrap-program.js +137 -0
- package/dist/text/unwrap-program.js.map +1 -0
- package/dist/text/work-text.d.ts +72 -0
- package/dist/text/work-text.d.ts.map +1 -0
- package/dist/text/work-text.js +337 -0
- package/dist/text/work-text.js.map +1 -0
- package/dist/transport/unix.d.ts +19 -0
- package/dist/transport/unix.d.ts.map +1 -0
- package/dist/transport/unix.js +68 -0
- package/dist/transport/unix.js.map +1 -0
- package/package.json +79 -3
- package/src/client-agents-tree.ts +642 -0
- package/src/client-agents.ts +230 -0
- package/src/client.ts +686 -0
- package/src/index.ts +27 -0
- package/src/launch/continue-pick.ts +28 -0
- package/src/launch/index.ts +5 -0
- package/src/launch/model-flags.ts +273 -0
- package/src/launch/streams.ts +13 -0
- package/src/launch/term-continue.ts +31 -0
- package/src/launch/term-session-key.ts +70 -0
- package/src/observable-abort-signal.ts +66 -0
- package/src/open-external.ts +22 -0
- package/src/paths.ts +20 -0
- package/src/text/agent-cover.ts +136 -0
- package/src/text/format-shell.ts +52 -0
- package/src/text/identity.ts +473 -0
- package/src/text/index.ts +29 -0
- package/src/text/liveness.ts +322 -0
- package/src/text/mcp-identity.ts +148 -0
- package/src/text/pretty-identity.ts +120 -0
- package/src/text/sugarcoat.ts +165 -0
- package/src/text/unwrap-program.ts +133 -0
- package/src/text/work-text.ts +388 -0
- package/src/transport/unix.ts +84 -0
- package/README.md +0 -3
package/src/client.ts
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import { DefaultedMap } from "@here.build/collections";
|
|
5
|
+
import { PlexusAwareness } from "@here.build/plexus";
|
|
6
|
+
import { trace } from "@inhuman.tools/harness-diagnostics";
|
|
7
|
+
import {
|
|
8
|
+
KERNEL_DOC_GUID,
|
|
9
|
+
KernelPlexus,
|
|
10
|
+
ProjectPlexus,
|
|
11
|
+
publishKernelSeat,
|
|
12
|
+
type KernelRoot,
|
|
13
|
+
type ProjectRoot,
|
|
14
|
+
} from "@inhuman.tools/harness-model/kernel";
|
|
15
|
+
import {
|
|
16
|
+
CallJob,
|
|
17
|
+
delegatesPermissionMode,
|
|
18
|
+
deskPermissionSnap,
|
|
19
|
+
permissionDeskSessionId,
|
|
20
|
+
ProgrammaticJob,
|
|
21
|
+
projectWorkerPermissions,
|
|
22
|
+
SessionPlexus,
|
|
23
|
+
type SessionRoot,
|
|
24
|
+
type SessionTurn,
|
|
25
|
+
} from "@inhuman.tools/harness-model/session";
|
|
26
|
+
import {
|
|
27
|
+
awarenessBytes,
|
|
28
|
+
bindJsonRpc,
|
|
29
|
+
bindProviderAwareness,
|
|
30
|
+
destroyProvider,
|
|
31
|
+
encodeQueryAwareness,
|
|
32
|
+
MESSAGE_AWARENESS,
|
|
33
|
+
MESSAGE_QUERY_AWARENESS,
|
|
34
|
+
peekMessageType,
|
|
35
|
+
type JsonRpcPort,
|
|
36
|
+
} from "@inhuman.tools/harness-peer";
|
|
37
|
+
import {
|
|
38
|
+
revalidationDemandRefFromSpec,
|
|
39
|
+
type RevalidationDemandSource,
|
|
40
|
+
type RevalidationDemandSubject,
|
|
41
|
+
collectRevalidationDemandRefs,
|
|
42
|
+
publishRevalidationDemand,
|
|
43
|
+
publishRevalidationDemandFocus,
|
|
44
|
+
} from "@inhuman.tools/plexus-expectations";
|
|
45
|
+
import { comparer, computed, type IComputedValue, type IReactionDisposer, reaction } from "mobx";
|
|
46
|
+
import { WebsocketProvider } from "y-websocket";
|
|
47
|
+
import * as Y from "yjs";
|
|
48
|
+
|
|
49
|
+
import { ClientAgent, type AgentRosterRow, type AgentRosterScope } from "./client-agents.js";
|
|
50
|
+
import { daemonSocketPath } from "./paths.js";
|
|
51
|
+
import { openUnixRoom, RoomNotFound, type OpenUnixRoomOptions } from "./transport/unix.js";
|
|
52
|
+
|
|
53
|
+
function step(message: string): void {
|
|
54
|
+
trace("harness:client", message);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Kernel-plane JSON-RPC. No session, no project. */
|
|
58
|
+
export type KernelRpc = {
|
|
59
|
+
request(method: string, params?: unknown): Promise<unknown>;
|
|
60
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Kernel room with the live image. RPC is the same port. */
|
|
64
|
+
export type KernelHandle = KernelRpc & {
|
|
65
|
+
readonly plexus: KernelPlexus;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type SessionViewSlot = "peek" | "ask";
|
|
69
|
+
|
|
70
|
+
type SessionView = {
|
|
71
|
+
readonly sessionId: string;
|
|
72
|
+
readonly plexus: SessionPlexus;
|
|
73
|
+
readonly stop: (() => void) | null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** sessionId-only upgrade 404. Daemon-down is not this. */
|
|
77
|
+
export class RestoreNotFound extends Error {
|
|
78
|
+
constructor(readonly sessionId: string) {
|
|
79
|
+
super(`session not found: ${sessionId}`);
|
|
80
|
+
this.name = "RestoreNotFound";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class Client implements AsyncDisposable {
|
|
85
|
+
static start(kernelPath: string, projectPath: string, phase?: (label: string) => void): Promise<Client> {
|
|
86
|
+
return Client.#attach({ kernelPath, projectPath, phase });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static connect(
|
|
90
|
+
kernelPath: string,
|
|
91
|
+
projectPath: string,
|
|
92
|
+
sessionId: string,
|
|
93
|
+
phase?: (label: string) => void,
|
|
94
|
+
): Promise<Client> {
|
|
95
|
+
return Client.#attach({ kernelPath, projectPath, sessionId, phase });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static restore(kernelPath: string, sessionId: string, phase?: (label: string) => void): Promise<Client> {
|
|
99
|
+
return Client.#attach({ kernelPath, sessionId, restore: true, phase });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Kernel room only. Caller must `ensure()` the daemon first — except completion, which must not spawn. */
|
|
103
|
+
static async kernel(
|
|
104
|
+
kernelPath: string,
|
|
105
|
+
opts?: {
|
|
106
|
+
readonly reconnect?: boolean;
|
|
107
|
+
readonly syncTimeoutMs?: number;
|
|
108
|
+
readonly awarenessWaitMs?: number;
|
|
109
|
+
},
|
|
110
|
+
): Promise<KernelHandle> {
|
|
111
|
+
step(`client kernel ${kernelPath}`);
|
|
112
|
+
const kernel = await realpath(kernelPath);
|
|
113
|
+
const socketPath = daemonSocketPath(kernel);
|
|
114
|
+
const providers: WebsocketProvider[] = [];
|
|
115
|
+
const awarenessStops: Array<() => void> = [];
|
|
116
|
+
try {
|
|
117
|
+
const plexus = await Client.#open(
|
|
118
|
+
socketPath,
|
|
119
|
+
"kernel",
|
|
120
|
+
{},
|
|
121
|
+
new Y.Doc({ guid: KERNEL_DOC_GUID }),
|
|
122
|
+
(doc) => KernelPlexus.connect(doc),
|
|
123
|
+
providers,
|
|
124
|
+
awarenessStops,
|
|
125
|
+
opts == null
|
|
126
|
+
? undefined
|
|
127
|
+
: {
|
|
128
|
+
reconnect: opts.reconnect,
|
|
129
|
+
syncTimeoutMs: opts.syncTimeoutMs,
|
|
130
|
+
},
|
|
131
|
+
opts?.awarenessWaitMs,
|
|
132
|
+
);
|
|
133
|
+
const provider = providers.at(-1);
|
|
134
|
+
if (provider == null) throw new Error("client kernel: no provider");
|
|
135
|
+
const rpc = bindJsonRpc(provider);
|
|
136
|
+
return {
|
|
137
|
+
plexus,
|
|
138
|
+
request: (method, params) => rpc.request(method, params),
|
|
139
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
140
|
+
rpc.rejectAll(new Error("kernel rpc disposed"));
|
|
141
|
+
for (const stop of awarenessStops.splice(0)) stop();
|
|
142
|
+
for (const item of providers.splice(0)) destroyProvider(item);
|
|
143
|
+
plexus.destroy();
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
for (const stop of awarenessStops) stop();
|
|
148
|
+
for (const provider of providers) destroyProvider(provider);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** sessionId-only upgrade. False on 404; other failures throw. */
|
|
154
|
+
static async sessionExists(kernelPath: string, sessionId: string): Promise<boolean> {
|
|
155
|
+
const kernel = await realpath(kernelPath);
|
|
156
|
+
const socketPath = daemonSocketPath(kernel);
|
|
157
|
+
const doc = new Y.Doc({ guid: `harness:session:${sessionId}` });
|
|
158
|
+
try {
|
|
159
|
+
const provider = await openUnixRoom(socketPath, "session", { sessionId }, doc, {
|
|
160
|
+
reconnect: false,
|
|
161
|
+
});
|
|
162
|
+
destroyProvider(provider);
|
|
163
|
+
doc.destroy();
|
|
164
|
+
return true;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
doc.destroy();
|
|
167
|
+
if (error instanceof RoomNotFound) return false;
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static async #attach(opts: {
|
|
173
|
+
readonly kernelPath: string;
|
|
174
|
+
readonly projectPath?: string;
|
|
175
|
+
readonly sessionId?: string;
|
|
176
|
+
readonly restore?: boolean;
|
|
177
|
+
readonly phase?: (label: string) => void;
|
|
178
|
+
}): Promise<Client> {
|
|
179
|
+
const restore = opts.restore === true;
|
|
180
|
+
step(`client ${restore ? "restore" : opts.sessionId === undefined ? "start" : "connect"} ${opts.kernelPath}`);
|
|
181
|
+
const kernel = await realpath(opts.kernelPath);
|
|
182
|
+
const socketPath = daemonSocketPath(kernel);
|
|
183
|
+
step(`client sock ${socketPath}`);
|
|
184
|
+
const providers: WebsocketProvider[] = [];
|
|
185
|
+
const awarenessStops: Array<() => void> = [];
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
let project: string;
|
|
189
|
+
let session: string;
|
|
190
|
+
let sessionParams: Record<string, string>;
|
|
191
|
+
let unixOpts: OpenUnixRoomOptions | undefined;
|
|
192
|
+
if (restore) {
|
|
193
|
+
session = opts.sessionId ?? "";
|
|
194
|
+
if (session === "") throw new Error("client restore: missing sessionId");
|
|
195
|
+
sessionParams = { sessionId: session };
|
|
196
|
+
unixOpts = { reconnect: false };
|
|
197
|
+
project = "";
|
|
198
|
+
} else {
|
|
199
|
+
if (opts.projectPath == null) throw new Error("client attach: missing projectPath");
|
|
200
|
+
project = await realpath(opts.projectPath);
|
|
201
|
+
session = opts.sessionId ?? randomUUID();
|
|
202
|
+
sessionParams = { path: project, sessionId: session };
|
|
203
|
+
}
|
|
204
|
+
opts.phase?.("syncing the session...");
|
|
205
|
+
const sessionPlexus = await Client.#open(
|
|
206
|
+
socketPath,
|
|
207
|
+
"session",
|
|
208
|
+
sessionParams,
|
|
209
|
+
new Y.Doc({ guid: `harness:session:${session}` }),
|
|
210
|
+
(doc) => SessionPlexus.connect(doc),
|
|
211
|
+
providers,
|
|
212
|
+
awarenessStops,
|
|
213
|
+
unixOpts,
|
|
214
|
+
);
|
|
215
|
+
if (restore) {
|
|
216
|
+
const path = sessionPlexus.root.path;
|
|
217
|
+
if (path === "") throw new Error("client restore: session has no path");
|
|
218
|
+
project = await realpath(path);
|
|
219
|
+
}
|
|
220
|
+
const sessionProvider = providers.at(-1) ?? null;
|
|
221
|
+
opts.phase?.("syncing the kernel...");
|
|
222
|
+
const kernelPlexus = await Client.#open(
|
|
223
|
+
socketPath,
|
|
224
|
+
"kernel",
|
|
225
|
+
{},
|
|
226
|
+
new Y.Doc({ guid: KERNEL_DOC_GUID }),
|
|
227
|
+
(doc) => KernelPlexus.connect(doc),
|
|
228
|
+
providers,
|
|
229
|
+
awarenessStops,
|
|
230
|
+
undefined,
|
|
231
|
+
undefined,
|
|
232
|
+
() => opts.phase?.("reading kernel occupancy..."),
|
|
233
|
+
);
|
|
234
|
+
const kernelProvider = providers.at(-1) ?? null;
|
|
235
|
+
opts.phase?.("syncing the project...");
|
|
236
|
+
const projectPlexus = await Client.#open(
|
|
237
|
+
socketPath,
|
|
238
|
+
"project",
|
|
239
|
+
{ path: project },
|
|
240
|
+
new Y.Doc(),
|
|
241
|
+
(doc) => ProjectPlexus.connect(doc),
|
|
242
|
+
providers,
|
|
243
|
+
awarenessStops,
|
|
244
|
+
);
|
|
245
|
+
const client = new Client(
|
|
246
|
+
kernel,
|
|
247
|
+
project,
|
|
248
|
+
sessionPlexus.root.sessionId,
|
|
249
|
+
kernelPlexus,
|
|
250
|
+
projectPlexus,
|
|
251
|
+
sessionPlexus,
|
|
252
|
+
sessionProvider,
|
|
253
|
+
kernelProvider,
|
|
254
|
+
);
|
|
255
|
+
client.#providers.push(...providers);
|
|
256
|
+
client.#awarenessStops.push(...awarenessStops);
|
|
257
|
+
return client;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
for (const stop of awarenessStops) stop();
|
|
260
|
+
for (const provider of providers) destroyProvider(provider);
|
|
261
|
+
if (restore && error instanceof RoomNotFound) {
|
|
262
|
+
throw new RestoreNotFound(opts.sessionId ?? "");
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// HTTPServer upgrades only after the host is ready. y-websocket starts the handshake.
|
|
269
|
+
static async #open<T>(
|
|
270
|
+
socketPath: string,
|
|
271
|
+
room: string,
|
|
272
|
+
params: Record<string, string>,
|
|
273
|
+
doc: Y.Doc,
|
|
274
|
+
attach: (doc: Y.Doc) => T,
|
|
275
|
+
providers: WebsocketProvider[],
|
|
276
|
+
awarenessStops: Array<() => void>,
|
|
277
|
+
unixOptions?: OpenUnixRoomOptions,
|
|
278
|
+
awarenessWaitMs?: number,
|
|
279
|
+
onAwareness?: () => void,
|
|
280
|
+
): Promise<T> {
|
|
281
|
+
step(`client sync ${room}`);
|
|
282
|
+
const provider = await openUnixRoom(socketPath, room, params, doc, unixOptions);
|
|
283
|
+
providers.push(provider);
|
|
284
|
+
step(`client synced ${room}`);
|
|
285
|
+
const instance = attach(doc);
|
|
286
|
+
const hub = (instance as { awareness?: PlexusAwareness }).awareness;
|
|
287
|
+
if (hub !== undefined) {
|
|
288
|
+
awarenessStops.push(bindProviderAwareness(hub, provider));
|
|
289
|
+
if (room === "kernel") {
|
|
290
|
+
onAwareness?.();
|
|
291
|
+
await awaitAwarenessDump(provider, awarenessWaitMs ?? 2000);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return instance;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
readonly #providers: WebsocketProvider[] = [];
|
|
298
|
+
readonly #awarenessStops: Array<() => void> = [];
|
|
299
|
+
readonly #rpc: JsonRpcPort | null;
|
|
300
|
+
readonly #kernelRpc: JsonRpcPort | null;
|
|
301
|
+
#attentionStop: IReactionDisposer | null = null;
|
|
302
|
+
#revalidationDemandFocusStop: IReactionDisposer | null = null;
|
|
303
|
+
/** Intern table for `ClientAgent` rows. Not observable. */
|
|
304
|
+
readonly #agents = new DefaultedMap((id: string) => new ClientAgent(this, id));
|
|
305
|
+
/** Viewer intern for sneak-peek. Does not publish a kernel seat. */
|
|
306
|
+
#peek: SessionView | null = null;
|
|
307
|
+
/** Approval intern for one-shot / session-scope workers. Does not seat. */
|
|
308
|
+
#ask: SessionView | null = null;
|
|
309
|
+
readonly #viewGen = { peek: 0, ask: 0 };
|
|
310
|
+
#askDeskStop: IReactionDisposer | null = null;
|
|
311
|
+
|
|
312
|
+
constructor(
|
|
313
|
+
readonly kernelPath: string,
|
|
314
|
+
readonly projectPath: string,
|
|
315
|
+
readonly sessionId: string,
|
|
316
|
+
readonly kernelPlexus: KernelPlexus,
|
|
317
|
+
readonly projectPlexus: ProjectPlexus,
|
|
318
|
+
readonly sessionPlexus: SessionPlexus,
|
|
319
|
+
readonly sessionProvider: WebsocketProvider | null = null,
|
|
320
|
+
kernelProvider: WebsocketProvider | null = null,
|
|
321
|
+
) {
|
|
322
|
+
this.#rpc = sessionProvider === null ? null : bindJsonRpc(sessionProvider);
|
|
323
|
+
this.#kernelRpc = kernelProvider === null ? null : bindJsonRpc(kernelProvider);
|
|
324
|
+
publishKernelSeat(this.kernelPlexus.awareness, sessionId);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
get kernelRoot(): KernelRoot {
|
|
328
|
+
return this.kernelPlexus.root;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
get projectRoot(): ProjectRoot {
|
|
332
|
+
return this.projectPlexus.root;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
get sessionRoot(): SessionRoot {
|
|
336
|
+
return this.sessionPlexus.root;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Peeked worker tape. Null when no sneak-peek is interned. */
|
|
340
|
+
get peekPlexus(): SessionPlexus | null {
|
|
341
|
+
return this.#peek?.plexus ?? null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Asking worker tape for the host cover. Null when none is interned. */
|
|
345
|
+
get askPlexus(): SessionPlexus | null {
|
|
346
|
+
return this.#ask?.plexus ?? null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Local intern. Tests / already-open plexus. Does not own destroy. */
|
|
350
|
+
adoptPeek(plexus: SessionPlexus): void {
|
|
351
|
+
this.#adoptView("peek", plexus);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Local intern for the host cover. Tests / already-open plexus. */
|
|
355
|
+
adoptAsk(plexus: SessionPlexus): void {
|
|
356
|
+
this.#adoptView("ask", plexus);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Open the worker session room without seating. No-op on a local test
|
|
361
|
+
* Client (`sessionProvider == null`) — those use {@link adoptPeek}.
|
|
362
|
+
*/
|
|
363
|
+
watchPeek(sessionId: string): Promise<void> {
|
|
364
|
+
return this.#watchView("peek", sessionId);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Open the asking worker without seating. Local tests use {@link adoptAsk}.
|
|
369
|
+
*/
|
|
370
|
+
watchAsk(sessionId: string): Promise<void> {
|
|
371
|
+
return this.#watchView("ask", sessionId);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
releasePeek(): void {
|
|
375
|
+
this.#releaseView("peek");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
releaseAsk(): void {
|
|
379
|
+
this.#releaseView("ask");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#adoptView(slot: SessionViewSlot, plexus: SessionPlexus): void {
|
|
383
|
+
this.#releaseView(slot);
|
|
384
|
+
this.#setView(slot, { sessionId: plexus.root.sessionId, plexus, stop: null });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#releaseView(slot: SessionViewSlot): void {
|
|
388
|
+
this.#viewGen[slot] += 1;
|
|
389
|
+
const view = slot === "peek" ? this.#peek : this.#ask;
|
|
390
|
+
this.#setView(slot, null);
|
|
391
|
+
view?.stop?.();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#setView(slot: SessionViewSlot, view: SessionView | null): void {
|
|
395
|
+
if (slot === "peek") this.#peek = view;
|
|
396
|
+
else {
|
|
397
|
+
this.#ask = view;
|
|
398
|
+
this.#bindAskDesk();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Host cover intern: project remainder inputs when this session is the desk. */
|
|
403
|
+
#bindAskDesk(): void {
|
|
404
|
+
this.#askDeskStop?.();
|
|
405
|
+
this.#askDeskStop = null;
|
|
406
|
+
const worker = this.#ask?.plexus.root;
|
|
407
|
+
if (worker == null) return;
|
|
408
|
+
this.#askDeskStop = reaction(
|
|
409
|
+
() => {
|
|
410
|
+
if (!delegatesPermissionMode(worker)) return null;
|
|
411
|
+
const deskId = permissionDeskSessionId(worker, this.kernelRoot);
|
|
412
|
+
if (deskId !== "" && deskId !== this.sessionId) return null;
|
|
413
|
+
return deskPermissionSnap(this.sessionRoot);
|
|
414
|
+
},
|
|
415
|
+
(snap) => {
|
|
416
|
+
if (snap == null) return;
|
|
417
|
+
projectWorkerPermissions(worker, this.sessionRoot);
|
|
418
|
+
},
|
|
419
|
+
{ equals: comparer.structural, fireImmediately: true },
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async #watchView(slot: SessionViewSlot, sessionId: string): Promise<void> {
|
|
424
|
+
const id = sessionId.trim();
|
|
425
|
+
const current = slot === "peek" ? this.#peek : this.#ask;
|
|
426
|
+
if (id === "" || current?.sessionId === id) return;
|
|
427
|
+
this.#releaseView(slot);
|
|
428
|
+
if (this.sessionProvider == null) return;
|
|
429
|
+
const gen = this.#viewGen[slot];
|
|
430
|
+
const kernel = await realpath(this.kernelPath);
|
|
431
|
+
const providers: WebsocketProvider[] = [];
|
|
432
|
+
const awarenessStops: Array<() => void> = [];
|
|
433
|
+
const drop = (): void => {
|
|
434
|
+
for (const stop of awarenessStops.splice(0)) stop();
|
|
435
|
+
for (const provider of providers.splice(0)) destroyProvider(provider);
|
|
436
|
+
};
|
|
437
|
+
try {
|
|
438
|
+
const plexus = await Client.#open(
|
|
439
|
+
daemonSocketPath(kernel),
|
|
440
|
+
"session",
|
|
441
|
+
{ sessionId: id },
|
|
442
|
+
new Y.Doc({ guid: `harness:session:${id}` }),
|
|
443
|
+
(doc) => SessionPlexus.connect(doc),
|
|
444
|
+
providers,
|
|
445
|
+
awarenessStops,
|
|
446
|
+
{ reconnect: false },
|
|
447
|
+
);
|
|
448
|
+
if (gen !== this.#viewGen[slot]) {
|
|
449
|
+
drop();
|
|
450
|
+
plexus.destroy();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
this.#setView(slot, {
|
|
454
|
+
sessionId: id,
|
|
455
|
+
plexus,
|
|
456
|
+
stop: () => {
|
|
457
|
+
drop();
|
|
458
|
+
plexus.destroy();
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
} catch (error) {
|
|
462
|
+
drop();
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
@computed.struct
|
|
468
|
+
get botsRoster(): readonly AgentRosterRow[] {
|
|
469
|
+
return [
|
|
470
|
+
...rosterRows("session", this.sessionRoot.botsRoster),
|
|
471
|
+
...rosterRows("project", this.projectRoot.botsRoster),
|
|
472
|
+
...rosterRows("global", this.kernelRoot.botsRoster),
|
|
473
|
+
];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
@computed
|
|
477
|
+
get agentCalls(): readonly CallJob[] {
|
|
478
|
+
const out: CallJob[] = [];
|
|
479
|
+
const seen = new Set<CallJob>();
|
|
480
|
+
const take = (job: CallJob): void => {
|
|
481
|
+
if (seen.has(job) || job.workerSessionId.trim() === "") return;
|
|
482
|
+
seen.add(job);
|
|
483
|
+
out.push(job);
|
|
484
|
+
};
|
|
485
|
+
for (const turn of this.sessionRoot.liveTurns) {
|
|
486
|
+
for (const job of turnCallJobs(turn)) take(job);
|
|
487
|
+
}
|
|
488
|
+
return out;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
@computed
|
|
492
|
+
get programs(): readonly ProgrammaticJob[] {
|
|
493
|
+
const out: ProgrammaticJob[] = [];
|
|
494
|
+
const seen = new Set<ProgrammaticJob>();
|
|
495
|
+
for (const turn of this.sessionRoot.liveTurns) {
|
|
496
|
+
for (const node of turn.openBackgroundWork) {
|
|
497
|
+
if (!(node instanceof ProgrammaticJob) || seen.has(node)) continue;
|
|
498
|
+
seen.add(node);
|
|
499
|
+
out.push(node);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
@computed.struct
|
|
506
|
+
get #workerIds(): readonly string[] {
|
|
507
|
+
const ids: string[] = [];
|
|
508
|
+
const seen = new Set<string>();
|
|
509
|
+
const add = (id: string): void => {
|
|
510
|
+
if (id === "" || seen.has(id)) return;
|
|
511
|
+
seen.add(id);
|
|
512
|
+
ids.push(id);
|
|
513
|
+
};
|
|
514
|
+
for (const row of this.botsRoster) add(row.sessionId);
|
|
515
|
+
for (const call of this.agentCalls) {
|
|
516
|
+
if (!call.completed) add(call.workerSessionId.trim());
|
|
517
|
+
}
|
|
518
|
+
return ids;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
@computed
|
|
522
|
+
get agents(): readonly ClientAgent[] {
|
|
523
|
+
return this.#workerIds.map((id) => this.#agents.get(id));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
@computed
|
|
527
|
+
get sessionAgents(): readonly ClientAgent[] {
|
|
528
|
+
return this.agents.filter((agent) => agent.scope === "session");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
@computed
|
|
532
|
+
get projectAgents(): readonly ClientAgent[] {
|
|
533
|
+
return this.agents.filter((agent) => agent.scope === "project");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
@computed
|
|
537
|
+
get globalAgents(): readonly ClientAgent[] {
|
|
538
|
+
return this.agents.filter((agent) => agent.scope === "global");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
@computed
|
|
542
|
+
get oneShotAgents(): readonly ClientAgent[] {
|
|
543
|
+
return this.agents.filter((agent) => agent.oneShot);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Ambient looking: specs currently in a list. Tracked as a MobX computed —
|
|
548
|
+
* occupancy retracts when the getter stops returning them. Kernel-plane:
|
|
549
|
+
* interned connections outlive a session wrap.
|
|
550
|
+
*/
|
|
551
|
+
setComputedAttention(fn: () => RevalidationDemandSource): void {
|
|
552
|
+
this.#attentionStop?.();
|
|
553
|
+
this.#attentionStop = reaction(
|
|
554
|
+
() => collectRevalidationDemandRefs(fn()),
|
|
555
|
+
(refs) => publishRevalidationDemand(this.kernelPlexus.awareness, refs),
|
|
556
|
+
{ equals: comparer.structural, fireImmediately: true },
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Focused looking: the one-of-a-kind spec in the UI. Tracked as a MobX
|
|
562
|
+
* computed — occupancy retracts when the getter returns null. Kernel-plane:
|
|
563
|
+
* interned connections outlive a session wrap.
|
|
564
|
+
*/
|
|
565
|
+
setComputedAttentionFocus(fn: () => RevalidationDemandSubject | null | undefined): void {
|
|
566
|
+
this.#revalidationDemandFocusStop?.();
|
|
567
|
+
this.#revalidationDemandFocusStop = reaction(
|
|
568
|
+
() => {
|
|
569
|
+
const spec = fn();
|
|
570
|
+
return spec == null ? null : revalidationDemandRefFromSpec(spec);
|
|
571
|
+
},
|
|
572
|
+
(ref) => publishRevalidationDemandFocus(this.kernelPlexus.awareness, ref),
|
|
573
|
+
{ equals: comparer.structural, fireImmediately: true },
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
get lost(): boolean {
|
|
578
|
+
return this.#rpc === null || this.#rpc.lost;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
request(method: string, params: unknown = {}): Promise<unknown> {
|
|
582
|
+
if (this.#rpc === null) {
|
|
583
|
+
return Promise.reject(new Error("client: session socket is not open"));
|
|
584
|
+
}
|
|
585
|
+
return this.#rpc.request(method, params);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** Kernel-plane JSON-RPC. Kernel/project docs are lenses; writes go through this. */
|
|
589
|
+
kernelRequest(method: string, params: unknown = {}): Promise<unknown> {
|
|
590
|
+
if (this.#kernelRpc === null) {
|
|
591
|
+
return Promise.reject(new Error("client: kernel socket is not open"));
|
|
592
|
+
}
|
|
593
|
+
return this.#kernelRpc.request(method, params);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
597
|
+
this.releasePeek();
|
|
598
|
+
this.releaseAsk();
|
|
599
|
+
this.#askDeskStop?.();
|
|
600
|
+
this.#askDeskStop = null;
|
|
601
|
+
this.#attentionStop?.();
|
|
602
|
+
this.#attentionStop = null;
|
|
603
|
+
this.#revalidationDemandFocusStop?.();
|
|
604
|
+
this.#revalidationDemandFocusStop = null;
|
|
605
|
+
publishKernelSeat(this.kernelPlexus.awareness, null);
|
|
606
|
+
publishRevalidationDemand(this.kernelPlexus.awareness, []);
|
|
607
|
+
publishRevalidationDemandFocus(this.kernelPlexus.awareness, null);
|
|
608
|
+
for (const stop of this.#awarenessStops.splice(0)) stop();
|
|
609
|
+
this.#rpc?.rejectAll(new Error("client disposed"));
|
|
610
|
+
this.#kernelRpc?.rejectAll(new Error("client disposed"));
|
|
611
|
+
for (const provider of this.#providers.splice(0)) {
|
|
612
|
+
destroyProvider(provider);
|
|
613
|
+
}
|
|
614
|
+
this.#agents.clear();
|
|
615
|
+
this.kernelPlexus.destroy();
|
|
616
|
+
this.projectPlexus.destroy();
|
|
617
|
+
this.sessionPlexus.destroy();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const turnCallJobsCache = new WeakMap<SessionTurn, IComputedValue<readonly CallJob[]>>();
|
|
622
|
+
|
|
623
|
+
function turnCallJobs(turn: SessionTurn): readonly CallJob[] {
|
|
624
|
+
let box = turnCallJobsCache.get(turn);
|
|
625
|
+
if (box === undefined) {
|
|
626
|
+
box = computed(
|
|
627
|
+
() => {
|
|
628
|
+
const out: CallJob[] = [];
|
|
629
|
+
const seen = new Set<CallJob>();
|
|
630
|
+
const take = (job: CallJob): void => {
|
|
631
|
+
if (seen.has(job) || job.workerSessionId.trim() === "") return;
|
|
632
|
+
seen.add(job);
|
|
633
|
+
out.push(job);
|
|
634
|
+
};
|
|
635
|
+
for (const chunk of turn.doneWork) {
|
|
636
|
+
if (chunk instanceof CallJob) take(chunk);
|
|
637
|
+
}
|
|
638
|
+
for (const chunk of turn.queue) {
|
|
639
|
+
if (chunk instanceof CallJob) take(chunk);
|
|
640
|
+
}
|
|
641
|
+
for (const node of turn.subtreeWork) {
|
|
642
|
+
if (node instanceof CallJob) take(node);
|
|
643
|
+
}
|
|
644
|
+
return out;
|
|
645
|
+
},
|
|
646
|
+
{ equals: comparer.shallow },
|
|
647
|
+
);
|
|
648
|
+
turnCallJobsCache.set(turn, box);
|
|
649
|
+
}
|
|
650
|
+
return box.get();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function rosterRows(scope: AgentRosterScope, record: Record<string, string>): AgentRosterRow[] {
|
|
654
|
+
const out: AgentRosterRow[] = [];
|
|
655
|
+
for (const [name, sessionId] of Object.entries(record)) {
|
|
656
|
+
const id = sessionId.trim();
|
|
657
|
+
if (name.trim() === "" || id === "") continue;
|
|
658
|
+
out.push({ scope, name, sessionId: id });
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Join snapshot can arrive before bind; QUERY fetches occupancy for `--continue`. */
|
|
664
|
+
async function awaitAwarenessDump(provider: WebsocketProvider, ms = 2000): Promise<void> {
|
|
665
|
+
const socket = provider.ws;
|
|
666
|
+
if (socket == null || socket.readyState !== socket.OPEN) return;
|
|
667
|
+
await new Promise<void>((resolve) => {
|
|
668
|
+
let done = false;
|
|
669
|
+
const finish = (): void => {
|
|
670
|
+
if (done) return;
|
|
671
|
+
done = true;
|
|
672
|
+
clearTimeout(timer);
|
|
673
|
+
socket.removeEventListener("message", onMessage);
|
|
674
|
+
resolve();
|
|
675
|
+
};
|
|
676
|
+
const timer = setTimeout(finish, ms);
|
|
677
|
+
const onMessage = (event: { data: unknown }): void => {
|
|
678
|
+
const bytes = awarenessBytes(event.data);
|
|
679
|
+
if (bytes == null) return;
|
|
680
|
+
const type = peekMessageType(bytes);
|
|
681
|
+
if (type === MESSAGE_AWARENESS || type === MESSAGE_QUERY_AWARENESS) finish();
|
|
682
|
+
};
|
|
683
|
+
socket.addEventListener("message", onMessage);
|
|
684
|
+
socket.send(encodeQueryAwareness());
|
|
685
|
+
});
|
|
686
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export {
|
|
2
|
+
mintProjectDocGuid as mintProjectDocGuid,
|
|
3
|
+
PROJECT_DOC_KIND as PROJECT_DOC_KIND,
|
|
4
|
+
ProjectPlexus as ProjectPlexus,
|
|
5
|
+
} from "@inhuman.tools/harness-model/kernel";
|
|
6
|
+
export * from "./paths.js";
|
|
7
|
+
export { Client as Client, RestoreNotFound as RestoreNotFound, type KernelRpc as KernelRpc } from "./client.js";
|
|
8
|
+
export { hostAskEscalateSessionId as hostAskEscalateSessionId } from "./client-agents.js";
|
|
9
|
+
export {
|
|
10
|
+
delegatesPermissionMode as delegatesPermissionMode,
|
|
11
|
+
deskPermissionSnap as deskPermissionSnap,
|
|
12
|
+
permissionDeskSessionId as permissionDeskSessionId,
|
|
13
|
+
projectWorkerPermissions as projectWorkerPermissions,
|
|
14
|
+
type DeskPermissionSnap as DeskPermissionSnap,
|
|
15
|
+
} from "@inhuman.tools/harness-model/session";
|
|
16
|
+
export {
|
|
17
|
+
SESSION_FACE_ASK as SESSION_FACE_ASK,
|
|
18
|
+
flattenAgentDrawer as flattenAgentDrawer,
|
|
19
|
+
mentionedAgentCall as mentionedAgentCall,
|
|
20
|
+
projectAgentDrawer as projectAgentDrawer,
|
|
21
|
+
projectAgentMeter as projectAgentMeter,
|
|
22
|
+
projectRecentSessions as projectRecentSessions,
|
|
23
|
+
type AgentDrawerRow as AgentDrawerRow,
|
|
24
|
+
} from "./client-agents-tree.js";
|
|
25
|
+
export { openUnixRoom as openUnixRoom } from "./transport/unix.js";
|
|
26
|
+
export * from "./observable-abort-signal.js";
|
|
27
|
+
export * from "./open-external.js";
|