@h-rig/runtime 0.0.6-alpha.21 → 0.0.6-alpha.23

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.
Files changed (25) hide show
  1. package/dist/bin/rig-agent-dispatch.js +588 -28
  2. package/dist/src/control-plane/agent-wrapper.js +592 -28
  3. package/dist/src/control-plane/harness-main.js +142 -17
  4. package/dist/src/control-plane/hooks/completion-verification.js +142 -17
  5. package/dist/src/control-plane/native/harness-cli.js +142 -17
  6. package/dist/src/control-plane/native/pr-automation.js +142 -17
  7. package/dist/src/control-plane/native/pr-review-gate.js +142 -17
  8. package/dist/src/control-plane/native/run-ops.js +1 -1
  9. package/dist/src/control-plane/native/task-ops.js +142 -17
  10. package/dist/src/control-plane/native/verifier.js +142 -17
  11. package/dist/src/control-plane/pi-sessiond/bin.js +793 -0
  12. package/dist/src/control-plane/pi-sessiond/client.js +41 -0
  13. package/dist/src/control-plane/pi-sessiond/event-hub.js +59 -0
  14. package/dist/src/control-plane/pi-sessiond/extension-ui-context.js +198 -0
  15. package/dist/src/control-plane/pi-sessiond/launcher.js +163 -0
  16. package/dist/src/control-plane/pi-sessiond/server.js +802 -0
  17. package/dist/src/control-plane/pi-sessiond/session-service.js +540 -0
  18. package/dist/src/control-plane/pi-sessiond/types.js +1 -0
  19. package/dist/src/control-plane/runtime/index.js +17 -0
  20. package/dist/src/control-plane/runtime/isolation/home.js +17 -0
  21. package/dist/src/control-plane/runtime/isolation/index.js +17 -0
  22. package/dist/src/control-plane/runtime/isolation/runner.js +17 -0
  23. package/dist/src/control-plane/runtime/isolation.js +17 -0
  24. package/dist/src/control-plane/runtime/queue.js +17 -0
  25. package/package.json +7 -6
@@ -0,0 +1,802 @@
1
+ // @bun
2
+ // packages/runtime/src/control-plane/pi-sessiond/server.ts
3
+ import { randomBytes } from "crypto";
4
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
5
+ import { dirname, resolve } from "path";
6
+
7
+ // packages/runtime/src/control-plane/pi-sessiond/event-hub.ts
8
+ function isOpen(socket) {
9
+ return socket.readyState === (socket.OPEN ?? 1);
10
+ }
11
+
12
+ class RigPiSessionEventHub {
13
+ socketsBySession = new Map;
14
+ socketsByRun = new Map;
15
+ addSession(sessionId, socket) {
16
+ return addToMapSet(this.socketsBySession, sessionId, socket);
17
+ }
18
+ addRun(runId, socket) {
19
+ return addToMapSet(this.socketsByRun, runId, socket);
20
+ }
21
+ publishSession(sessionId, event) {
22
+ this.publishSet(this.socketsBySession.get(sessionId), event);
23
+ }
24
+ publishRun(runId, event) {
25
+ this.publishSet(this.socketsByRun.get(runId), event);
26
+ }
27
+ publish(sessionId, runId, event) {
28
+ this.publishSession(sessionId, event);
29
+ if (runId)
30
+ this.publishRun(runId, event);
31
+ }
32
+ publishSet(sockets, event) {
33
+ if (!sockets || sockets.size === 0)
34
+ return;
35
+ const payload = JSON.stringify(event);
36
+ for (const socket of [...sockets]) {
37
+ if (isOpen(socket)) {
38
+ socket.send(payload);
39
+ } else {
40
+ sockets.delete(socket);
41
+ }
42
+ }
43
+ }
44
+ }
45
+ function addToMapSet(map, key, socket) {
46
+ let sockets = map.get(key);
47
+ if (!sockets) {
48
+ sockets = new Set;
49
+ map.set(key, sockets);
50
+ }
51
+ sockets.add(socket);
52
+ let removed = false;
53
+ return () => {
54
+ if (removed)
55
+ return;
56
+ removed = true;
57
+ sockets?.delete(socket);
58
+ if (sockets?.size === 0)
59
+ map.delete(key);
60
+ };
61
+ }
62
+
63
+ // packages/runtime/src/control-plane/pi-sessiond/session-service.ts
64
+ import { randomUUID as randomUUID2 } from "crypto";
65
+ import { mkdirSync } from "fs";
66
+ import { join } from "path";
67
+ import {
68
+ AuthStorage,
69
+ createAgentSessionFromServices,
70
+ createAgentSessionRuntime,
71
+ createAgentSessionServices,
72
+ ModelRegistry,
73
+ SessionManager
74
+ } from "@earendil-works/pi-coding-agent";
75
+
76
+ // packages/runtime/src/control-plane/pi-sessiond/extension-ui-context.ts
77
+ import { randomUUID } from "crypto";
78
+ import {
79
+ initTheme
80
+ } from "@earendil-works/pi-coding-agent";
81
+ var headlessTheme = {
82
+ fg: (_color, text) => text,
83
+ bg: (_color, text) => text,
84
+ dim: (text) => text,
85
+ bold: (text) => text,
86
+ italic: (text) => text,
87
+ underline: (text) => text,
88
+ strikethrough: (text) => text,
89
+ reset: (text) => text,
90
+ getFgAnsi: () => "",
91
+ getBgAnsi: () => "",
92
+ name: "rig-headless"
93
+ };
94
+ function ensureThemeInitialized() {
95
+ try {
96
+ initTheme(undefined, false);
97
+ } catch {}
98
+ }
99
+
100
+ class RigPiExtensionUiBridge {
101
+ sessionId;
102
+ runId;
103
+ publish;
104
+ pending = new Map;
105
+ constructor(sessionId, runId, publish) {
106
+ this.sessionId = sessionId;
107
+ this.runId = runId;
108
+ this.publish = publish;
109
+ ensureThemeInitialized();
110
+ }
111
+ createContext() {
112
+ const dialog = (opts, defaultValue, request, parse) => {
113
+ if (opts?.signal?.aborted)
114
+ return Promise.resolve(defaultValue);
115
+ const id = randomUUID();
116
+ return new Promise((resolve) => {
117
+ let timeout;
118
+ const cleanup = () => {
119
+ if (timeout)
120
+ clearTimeout(timeout);
121
+ opts?.signal?.removeEventListener("abort", onAbort);
122
+ this.pending.delete(id);
123
+ };
124
+ const finish = (value) => {
125
+ cleanup();
126
+ resolve(value);
127
+ };
128
+ const onAbort = () => finish(defaultValue);
129
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
130
+ if (opts?.timeout)
131
+ timeout = setTimeout(() => finish(defaultValue), opts.timeout);
132
+ this.pending.set(id, {
133
+ timeout,
134
+ abort: onAbort,
135
+ resolve: (raw) => finish(parse(normalizeResponse(raw)))
136
+ });
137
+ this.publish({
138
+ type: "extension_ui_request",
139
+ sessionId: this.sessionId,
140
+ runId: this.runId,
141
+ request: { id, timeout: opts?.timeout, ...request }
142
+ });
143
+ });
144
+ };
145
+ return {
146
+ select: (title, options, opts) => dialog(opts, undefined, { method: "select", title, options }, (response) => {
147
+ if (response.cancelled)
148
+ return;
149
+ return typeof response.value === "string" ? response.value : undefined;
150
+ }),
151
+ confirm: (title, message, opts) => dialog(opts, false, { method: "confirm", title, message }, (response) => {
152
+ if (response.cancelled)
153
+ return false;
154
+ return response.confirmed === true || response.value === true;
155
+ }),
156
+ input: (title, placeholder, opts) => dialog(opts, undefined, { method: "input", title, placeholder }, (response) => {
157
+ if (response.cancelled)
158
+ return;
159
+ return typeof response.value === "string" ? response.value : undefined;
160
+ }),
161
+ notify: (message, type) => {
162
+ this.fire({ method: "notify", message, notifyType: type });
163
+ },
164
+ onTerminalInput: () => () => {},
165
+ setStatus: (key, text) => {
166
+ this.fire({ method: "setStatus", statusKey: key, statusText: text });
167
+ },
168
+ setWorkingMessage: (message) => {
169
+ this.fire({ method: "setWorkingMessage", message });
170
+ },
171
+ setWorkingVisible: (visible) => {
172
+ this.fire({ method: "setWorkingVisible", visible });
173
+ },
174
+ setWorkingIndicator: (options) => {
175
+ this.fire({ method: "setWorkingIndicator", options });
176
+ },
177
+ setHiddenThinkingLabel: (label) => {
178
+ this.fire({ method: "setHiddenThinkingLabel", label });
179
+ },
180
+ setWidget: (key, content, options) => {
181
+ if (content === undefined || Array.isArray(content)) {
182
+ this.fire({ method: "setWidget", widgetKey: key, widgetLines: content, widgetPlacement: options?.placement });
183
+ return;
184
+ }
185
+ this.fire({ method: "notify", message: `Extension widget '${key}' uses a component factory that Rig daemon mode cannot render.`, notifyType: "warning" });
186
+ },
187
+ setFooter: (factory) => {
188
+ if (factory !== undefined)
189
+ this.fire({ method: "notify", message: "Custom extension footers are not rendered in Rig daemon mode.", notifyType: "warning" });
190
+ },
191
+ setHeader: (factory) => {
192
+ if (factory !== undefined)
193
+ this.fire({ method: "notify", message: "Custom extension headers are not rendered in Rig daemon mode.", notifyType: "warning" });
194
+ },
195
+ setTitle: (title) => {
196
+ this.fire({ method: "setTitle", title });
197
+ },
198
+ custom: async () => {
199
+ this.fire({ method: "notify", message: "Custom extension UI components are not supported in Rig daemon mode.", notifyType: "warning" });
200
+ return;
201
+ },
202
+ pasteToEditor: (text) => {
203
+ this.fire({ method: "set_editor_text", text });
204
+ },
205
+ setEditorText: (text) => {
206
+ this.fire({ method: "set_editor_text", text });
207
+ },
208
+ getEditorText: () => "",
209
+ editor: (title, prefill) => dialog(undefined, undefined, { method: "editor", title, prefill }, (response) => {
210
+ if (response.cancelled)
211
+ return;
212
+ return typeof response.value === "string" ? response.value : undefined;
213
+ }),
214
+ addAutocompleteProvider: () => {},
215
+ setEditorComponent: (factory) => {
216
+ if (factory !== undefined)
217
+ this.fire({ method: "notify", message: "Custom editor components are not supported in Rig daemon mode.", notifyType: "warning" });
218
+ },
219
+ getEditorComponent: () => {
220
+ return;
221
+ },
222
+ get theme() {
223
+ return headlessTheme;
224
+ },
225
+ getAllThemes: () => [],
226
+ getTheme: () => {
227
+ return;
228
+ },
229
+ setTheme: () => ({ success: false, error: "Theme switching is not supported in Rig daemon mode" }),
230
+ getToolsExpanded: () => false,
231
+ setToolsExpanded: () => {}
232
+ };
233
+ }
234
+ respond(input) {
235
+ const pending = this.pending.get(input.requestId);
236
+ if (!pending)
237
+ return false;
238
+ pending.resolve(input);
239
+ return true;
240
+ }
241
+ cancelAll() {
242
+ for (const [id, pending] of this.pending.entries()) {
243
+ if (pending.timeout)
244
+ clearTimeout(pending.timeout);
245
+ pending.abort?.();
246
+ this.pending.delete(id);
247
+ pending.resolve({ requestId: id, cancelled: true });
248
+ }
249
+ }
250
+ fire(request) {
251
+ this.publish({
252
+ type: "extension_ui_request",
253
+ sessionId: this.sessionId,
254
+ runId: this.runId,
255
+ request: { id: randomUUID(), ...request }
256
+ });
257
+ }
258
+ }
259
+ function normalizeResponse(value) {
260
+ if (!value || typeof value !== "object" || Array.isArray(value))
261
+ return { requestId: "", cancelled: true };
262
+ const record = value;
263
+ return {
264
+ requestId: typeof record.requestId === "string" ? record.requestId : "",
265
+ value: record.value,
266
+ confirmed: typeof record.confirmed === "boolean" ? record.confirmed : undefined,
267
+ cancelled: record.cancelled === true
268
+ };
269
+ }
270
+
271
+ // packages/runtime/src/control-plane/pi-sessiond/session-service.ts
272
+ var BUILTIN_COMMANDS = [
273
+ { name: "session", description: "Show session info and stats", source: "builtin" },
274
+ { name: "name", description: "Set session display name", source: "builtin" },
275
+ { name: "compact", description: "Manually compact session context", source: "builtin" },
276
+ { name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes", source: "builtin" },
277
+ { name: "quit", description: "Detach from the current local Pi frontend", source: "builtin" }
278
+ ];
279
+
280
+ class RigPiSessionService {
281
+ hub;
282
+ config;
283
+ activeBySession = new Map;
284
+ sessionIdByRun = new Map;
285
+ heartbeat;
286
+ constructor(hub, config) {
287
+ this.hub = hub;
288
+ this.config = config;
289
+ this.heartbeat = setInterval(() => this.publishHeartbeats(), 2000);
290
+ }
291
+ activeCount() {
292
+ return this.activeBySession.size;
293
+ }
294
+ async dispose() {
295
+ clearInterval(this.heartbeat);
296
+ const sessions = [...this.activeBySession.values()];
297
+ this.activeBySession.clear();
298
+ this.sessionIdByRun.clear();
299
+ await Promise.all(sessions.map(async (active) => {
300
+ active.unsubscribe();
301
+ active.ui.cancelAll();
302
+ try {
303
+ await active.runtime.session.abort();
304
+ } catch {}
305
+ await active.runtime.dispose();
306
+ }));
307
+ }
308
+ async startSession(input) {
309
+ const existing = this.getByRun(input.runId);
310
+ if (existing)
311
+ return existing.metadata;
312
+ mkdirSync(input.agentDir, { recursive: true });
313
+ mkdirSync(input.sessionDir, { recursive: true });
314
+ const authStorage = AuthStorage.create(join(input.agentDir, "auth.json"));
315
+ const modelRegistry = ModelRegistry.create(authStorage, join(input.agentDir, "models.json"));
316
+ const createRuntime = async ({ cwd, agentDir, sessionManager: sessionManager2, sessionStartEvent }) => {
317
+ const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
318
+ const result = await createAgentSessionFromServices({ services, sessionManager: sessionManager2, sessionStartEvent });
319
+ return { ...result, services, diagnostics: services.diagnostics };
320
+ };
321
+ const sessionManager = SessionManager.create(input.cwd, input.sessionDir);
322
+ const runtime = await createAgentSessionRuntime(createRuntime, {
323
+ cwd: input.cwd,
324
+ agentDir: input.agentDir,
325
+ sessionManager,
326
+ sessionStartEvent: { type: "session_start", reason: "new" }
327
+ });
328
+ const now = new Date().toISOString();
329
+ const metadata = {
330
+ runId: input.runId,
331
+ sessionId: runtime.session.sessionId,
332
+ daemonId: this.config.daemonId,
333
+ cwd: input.cwd,
334
+ agentDir: input.agentDir,
335
+ sessionDir: input.sessionDir,
336
+ transport: "http-control-ws-events",
337
+ serverBasePath: `/api/runs/${encodeURIComponent(input.runId)}/pi`,
338
+ eventsPath: `/api/runs/${encodeURIComponent(input.runId)}/pi/events`,
339
+ createdAt: now,
340
+ updatedAt: now,
341
+ backend: {
342
+ kind: "worker-pi-sessiond",
343
+ version: this.config.version,
344
+ commit: this.config.commit,
345
+ pid: process.pid
346
+ }
347
+ };
348
+ const active = {
349
+ runId: input.runId,
350
+ runtime,
351
+ metadata,
352
+ unsubscribe: () => {},
353
+ ui: new RigPiExtensionUiBridge(runtime.session.sessionId, input.runId, (event) => this.publish(runtime.session.sessionId, input.runId, event))
354
+ };
355
+ await this.bindRuntime(active);
356
+ if (input.sessionName?.trim())
357
+ runtime.session.setSessionName(input.sessionName.trim());
358
+ this.activeBySession.set(runtime.session.sessionId, active);
359
+ this.sessionIdByRun.set(input.runId, runtime.session.sessionId);
360
+ this.publish(runtime.session.sessionId, input.runId, { type: "ready", metadata });
361
+ this.publishStatus(active);
362
+ this.publishActivity(active, "ready", "idle");
363
+ return metadata;
364
+ }
365
+ getByRun(runId) {
366
+ const sessionId = this.sessionIdByRun.get(runId);
367
+ return sessionId ? this.activeBySession.get(sessionId) : undefined;
368
+ }
369
+ getBySession(sessionId) {
370
+ return this.activeBySession.get(sessionId);
371
+ }
372
+ messages(sessionId) {
373
+ const active = this.requireActive(sessionId);
374
+ return [...active.runtime.session.messages];
375
+ }
376
+ status(sessionId) {
377
+ return this.statusFromActive(this.requireActive(sessionId));
378
+ }
379
+ commands(sessionId) {
380
+ const session = this.requireActive(sessionId).runtime.session;
381
+ const commands = [...BUILTIN_COMMANDS];
382
+ for (const command of session.extensionRunner.getRegisteredCommands()) {
383
+ commands.push({ name: command.invocationName, description: command.description, source: "extension" });
384
+ }
385
+ for (const template of session.promptTemplates) {
386
+ commands.push({ name: template.name, description: template.description, source: "prompt" });
387
+ }
388
+ for (const skill of session.resourceLoader.getSkills().skills) {
389
+ commands.push({ name: `skill:${skill.name}`, description: skill.description, source: "skill" });
390
+ }
391
+ return commands.sort((a, b) => a.name.localeCompare(b.name));
392
+ }
393
+ async prompt(sessionId, text, streamingBehavior) {
394
+ const active = this.requireActive(sessionId);
395
+ const session = active.runtime.session;
396
+ const behavior = session.isStreaming || session.isCompacting ? streamingBehavior ?? "steer" : undefined;
397
+ this.publishActivity(active, behavior === "followUp" ? "follow-up queued" : behavior === "steer" ? "steering queued" : "prompt accepted", "active");
398
+ session.prompt(text, behavior ? { streamingBehavior: behavior, source: "rpc" } : { source: "rpc" }).catch((error) => {
399
+ const message = error instanceof Error ? error.message : String(error);
400
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "error", message });
401
+ this.publishActivity(active, "prompt failed", "error", message);
402
+ });
403
+ this.publishStatus(active);
404
+ return { accepted: true };
405
+ }
406
+ async shell(sessionId, text) {
407
+ const active = this.requireActive(sessionId);
408
+ const session = active.runtime.session;
409
+ const excluded = text.startsWith("!!");
410
+ const command = (excluded ? text.slice(2) : text.startsWith("!") ? text.slice(1) : text).trim();
411
+ if (!command)
412
+ throw new Error("Usage: !<shell command>");
413
+ if (session.isBashRunning)
414
+ throw new Error("A bash command is already running");
415
+ this.publishActivity(active, "running bash", "active", command);
416
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "pi.ui_event", sessionId, runId: active.runId, event: { type: "shell.start", command, excludeFromContext: excluded } });
417
+ session.executeBash(command, (chunk) => {
418
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "pi.ui_event", sessionId, runId: active.runId, event: { type: "shell.chunk", chunk } });
419
+ this.publishActivity(active, "running bash", "active", command);
420
+ this.publishStatus(active);
421
+ }, { excludeFromContext: excluded }).then((result) => {
422
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "pi.ui_event", sessionId, runId: active.runId, event: { type: "shell.end", ...result } });
423
+ this.publishActivity(active, "bash complete", result.exitCode === 0 ? "idle" : "error", command);
424
+ this.publishStatus(active);
425
+ }).catch((error) => {
426
+ const message = error instanceof Error ? error.message : String(error);
427
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "pi.ui_event", sessionId, runId: active.runId, event: { type: "shell.end", output: message, isError: true } });
428
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "error", message });
429
+ this.publishActivity(active, "bash failed", "error", message);
430
+ this.publishStatus(active);
431
+ });
432
+ return { accepted: true };
433
+ }
434
+ async runCommand(sessionId, text) {
435
+ const active = this.requireActive(sessionId);
436
+ const trimmed = text.trim();
437
+ const [rawName = "", ...args] = trimmed.replace(/^\//, "").split(/\s+/);
438
+ const rest = args.join(" ").trim();
439
+ if (rawName === "session")
440
+ return { type: "done", message: formatSessionStats(active.runtime.session) };
441
+ if (rawName === "name") {
442
+ if (!rest)
443
+ return { type: "unsupported", message: "Usage: /name <session name>" };
444
+ active.runtime.session.setSessionName(rest);
445
+ this.publishStatus(active);
446
+ return { type: "done", message: `Session named: ${rest}` };
447
+ }
448
+ if (rawName === "compact") {
449
+ active.runtime.session.compact(rest || undefined).catch((error) => {
450
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "error", message: error instanceof Error ? error.message : String(error) });
451
+ });
452
+ return { type: "done", message: "Compaction started\u2026" };
453
+ }
454
+ if (rawName === "reload") {
455
+ await active.runtime.session.reload();
456
+ await this.bindRuntime(active);
457
+ return { type: "done", message: "Pi resources reloaded" };
458
+ }
459
+ if (rawName === "quit")
460
+ return { type: "done", message: "Detach locally with /detach or /quit." };
461
+ await this.prompt(sessionId, trimmed, active.runtime.session.isStreaming ? "steer" : undefined);
462
+ return { type: "done", message: `Accepted ${trimmed}` };
463
+ }
464
+ respondToCommand(_sessionId, _requestId, _value) {
465
+ return { type: "unsupported", message: "No pending command selection is active." };
466
+ }
467
+ respondToExtensionUi(sessionId, input) {
468
+ const active = this.requireActive(sessionId);
469
+ return { accepted: active.ui.respond(input) };
470
+ }
471
+ async abort(sessionId) {
472
+ const active = this.requireActive(sessionId);
473
+ active.runtime.session.clearQueue();
474
+ await active.runtime.session.abort();
475
+ this.publishActivity(active, "stopped", "idle");
476
+ this.publishStatus(active);
477
+ return { aborted: true };
478
+ }
479
+ async bindRuntime(active) {
480
+ active.unsubscribe();
481
+ const session = active.runtime.session;
482
+ active.ui.cancelAll();
483
+ active.ui = new RigPiExtensionUiBridge(session.sessionId, active.runId, (event) => this.publish(session.sessionId, active.runId, event));
484
+ active.metadata = { ...active.metadata, sessionId: session.sessionId, updatedAt: new Date().toISOString() };
485
+ this.sessionIdByRun.set(active.runId, session.sessionId);
486
+ this.activeBySession.set(session.sessionId, active);
487
+ active.runtime.setRebindSession(async () => this.bindRuntime(active));
488
+ await session.bindExtensions({
489
+ uiContext: active.ui.createContext(),
490
+ commandContextActions: {
491
+ waitForIdle: () => session.agent.waitForIdle(),
492
+ newSession: async (options) => active.runtime.newSession(options),
493
+ fork: async (entryId, options) => {
494
+ const result = await active.runtime.fork(entryId, options);
495
+ return { cancelled: result.cancelled };
496
+ },
497
+ navigateTree: async (targetId, options) => session.navigateTree(targetId, options),
498
+ switchSession: async (sessionPath, options) => active.runtime.switchSession(sessionPath, options),
499
+ reload: async () => {
500
+ await session.reload();
501
+ }
502
+ },
503
+ shutdownHandler: () => {
504
+ this.abort(session.sessionId);
505
+ },
506
+ onError: (error) => {
507
+ this.publish(session.sessionId, active.runId, { type: "error", message: error.error, detail: error });
508
+ }
509
+ });
510
+ active.unsubscribe = session.subscribe((event) => {
511
+ this.publish(session.sessionId, active.runId, { type: "pi.event", sessionId: session.sessionId, runId: active.runId, event });
512
+ this.publishActivityForEvent(active, event);
513
+ this.publishStatus(active);
514
+ });
515
+ }
516
+ publish(sessionId, runId, event) {
517
+ this.hub.publish(sessionId, runId, event);
518
+ }
519
+ publishStatus(active) {
520
+ const status = this.statusFromActive(active);
521
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "status.update", sessionId: active.runtime.session.sessionId, runId: active.runId, status });
522
+ }
523
+ publishActivity(active, label, phase, detail) {
524
+ const activity = {
525
+ sessionId: active.runtime.session.sessionId,
526
+ runId: active.runId,
527
+ phase,
528
+ label,
529
+ detail,
530
+ at: new Date().toISOString()
531
+ };
532
+ active.activity = activity;
533
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "activity.update", sessionId: active.runtime.session.sessionId, runId: active.runId, activity });
534
+ }
535
+ publishActivityForEvent(active, event) {
536
+ const type = event && typeof event === "object" && !Array.isArray(event) ? event.type : undefined;
537
+ if (type === "agent_start")
538
+ this.publishActivity(active, "agent running", "active");
539
+ else if (type === "agent_end")
540
+ this.publishActivity(active, "idle", "idle");
541
+ else if (type === "tool_execution_start")
542
+ this.publishActivity(active, `tool: ${String(event.toolName ?? "tool")}`, "active");
543
+ else if (type === "compaction_start")
544
+ this.publishActivity(active, "compacting", "active");
545
+ else if (type === "compaction_end")
546
+ this.publishActivity(active, "compaction complete", "idle");
547
+ }
548
+ publishHeartbeats() {
549
+ for (const active of this.activeBySession.values()) {
550
+ if (active.runtime.session.isStreaming || active.runtime.session.isCompacting || active.runtime.session.isBashRunning || active.runtime.session.pendingMessageCount > 0) {
551
+ this.publishStatus(active);
552
+ this.publishActivity(active, active.activity?.label ?? "active", "active", active.activity?.detail);
553
+ }
554
+ }
555
+ }
556
+ statusFromActive(active) {
557
+ const session = active.runtime.session;
558
+ return {
559
+ sessionId: session.sessionId,
560
+ runId: active.runId,
561
+ cwd: active.runtime.cwd,
562
+ sessionFile: session.sessionFile,
563
+ sessionName: session.sessionName,
564
+ model: session.model,
565
+ thinkingLevel: session.thinkingLevel,
566
+ isStreaming: session.isStreaming,
567
+ isCompacting: session.isCompacting,
568
+ isBashRunning: session.isBashRunning,
569
+ pendingMessageCount: session.pendingMessageCount,
570
+ messageCount: session.messages.length,
571
+ steeringMessages: session.getSteeringMessages(),
572
+ followUpMessages: session.getFollowUpMessages(),
573
+ contextUsage: session.getContextUsage(),
574
+ stats: session.getSessionStats()
575
+ };
576
+ }
577
+ requireActive(sessionId) {
578
+ const active = this.activeBySession.get(sessionId);
579
+ if (!active)
580
+ throw new Error("Pi session not found");
581
+ return active;
582
+ }
583
+ }
584
+ function formatSessionStats(session) {
585
+ const stats = session.getSessionStats();
586
+ return [
587
+ `Session: ${stats.sessionId}`,
588
+ `Messages: ${stats.totalMessages} (${stats.userMessages} user, ${stats.assistantMessages} assistant)`,
589
+ `Tool calls: ${stats.toolCalls}`,
590
+ `Tokens: \u2191${stats.tokens.input} \u2193${stats.tokens.output} total ${stats.tokens.total}`,
591
+ `Cost: $${stats.cost.toFixed(4)}`
592
+ ].join(`
593
+ `);
594
+ }
595
+ function createDaemonId() {
596
+ return `rig-pi-sessiond-${randomUUID2()}`;
597
+ }
598
+
599
+ // packages/runtime/src/control-plane/pi-sessiond/server.ts
600
+ async function runRigPiSessionDaemon(options = {}) {
601
+ const rootDir = resolve(options.rootDir || process.env.RIG_PI_SESSIOND_ROOT || process.cwd());
602
+ mkdirSync2(rootDir, { recursive: true });
603
+ const token = options.token || process.env.RIG_PI_SESSIOND_TOKEN || randomToken();
604
+ const host = options.host || process.env.RIG_PI_SESSIOND_HOST || "127.0.0.1";
605
+ const requestedPort = options.port ?? numberFromEnv(process.env.RIG_PI_SESSIOND_PORT) ?? 0;
606
+ const startedAt = new Date().toISOString();
607
+ const config = {
608
+ daemonId: options.daemonId || process.env.RIG_PI_SESSIOND_ID || createDaemonId(),
609
+ token,
610
+ host,
611
+ port: requestedPort,
612
+ rootDir,
613
+ startedAt,
614
+ version: options.version || process.env.RIG_VERSION || "dev",
615
+ commit: options.commit || process.env.RIG_GIT_COMMIT
616
+ };
617
+ const hub = new RigPiSessionEventHub;
618
+ const sessions = new RigPiSessionService(hub, config);
619
+ const server = Bun.serve({
620
+ hostname: host,
621
+ port: requestedPort,
622
+ idleTimeout: 255,
623
+ fetch: async (req, server2) => {
624
+ const url = new URL(req.url);
625
+ if (url.pathname === "/health" && req.method === "GET") {
626
+ return json(health(server2.port ?? config.port, config, sessions.activeCount()));
627
+ }
628
+ if (!isAuthorized(req, token))
629
+ return json({ ok: false, error: "Unauthorized" }, 401);
630
+ if (url.pathname === "/sessions" && req.method === "POST") {
631
+ const body = await readJson(req);
632
+ const runId = asString(body.runId);
633
+ const cwd = asString(body.cwd);
634
+ const agentDir = asString(body.agentDir);
635
+ const sessionDir = asString(body.sessionDir);
636
+ if (!runId || !cwd || !agentDir || !sessionDir)
637
+ return json({ ok: false, error: "runId, cwd, agentDir, and sessionDir are required" }, 400);
638
+ const metadata = await sessions.startSession({ runId, cwd, agentDir, sessionDir, sessionName: asString(body.sessionName) ?? undefined });
639
+ return json({ metadata });
640
+ }
641
+ const runEventsMatch = url.pathname.match(/^\/runs\/([^/]+)\/events$/);
642
+ if (runEventsMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
643
+ const upgraded = server2.upgrade(req, { data: { kind: "run-events", runId: decodeURIComponent(runEventsMatch[1]) } });
644
+ return upgraded ? new Response(null) : json({ ok: false, error: "WebSocket upgrade failed" }, 400);
645
+ }
646
+ const sessionEventsMatch = url.pathname.match(/^\/sessions\/([^/]+)\/events$/);
647
+ if (sessionEventsMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
648
+ const upgraded = server2.upgrade(req, { data: { kind: "session-events", sessionId: decodeURIComponent(sessionEventsMatch[1]) } });
649
+ return upgraded ? new Response(null) : json({ ok: false, error: "WebSocket upgrade failed" }, 400);
650
+ }
651
+ const sessionMatch = url.pathname.match(/^\/sessions\/([^/]+)(?:\/(.*))?$/);
652
+ if (!sessionMatch)
653
+ return json({ ok: false, error: "Not found" }, 404);
654
+ const sessionId = decodeURIComponent(sessionMatch[1]);
655
+ const action = sessionMatch[2] || "";
656
+ try {
657
+ if (action === "messages" && req.method === "GET")
658
+ return json({ messages: sessions.messages(sessionId) });
659
+ if (action === "status" && req.method === "GET")
660
+ return json({ status: sessions.status(sessionId) });
661
+ if (action === "commands" && req.method === "GET")
662
+ return json({ commands: sessions.commands(sessionId) });
663
+ if (action === "prompt" && req.method === "POST") {
664
+ const body = await readJson(req);
665
+ return json(await sessions.prompt(sessionId, requireText(body, "text"), optionalStreamingBehavior(body)));
666
+ }
667
+ if (action === "shell" && req.method === "POST") {
668
+ const body = await readJson(req);
669
+ return json(await sessions.shell(sessionId, requireText(body, "text")));
670
+ }
671
+ if (action === "commands/run" && req.method === "POST") {
672
+ const body = await readJson(req);
673
+ return json(await sessions.runCommand(sessionId, requireText(body, "text")));
674
+ }
675
+ if (action === "commands/respond" && req.method === "POST") {
676
+ const body = await readJson(req);
677
+ return json(sessions.respondToCommand(sessionId, requireText(body, "requestId"), body.value));
678
+ }
679
+ if (action === "extension-ui/respond" && req.method === "POST") {
680
+ const body = await readJson(req);
681
+ return json(sessions.respondToExtensionUi(sessionId, {
682
+ requestId: requireText(body, "requestId"),
683
+ value: body.value,
684
+ confirmed: typeof body.confirmed === "boolean" ? body.confirmed : undefined,
685
+ cancelled: body.cancelled === true
686
+ }));
687
+ }
688
+ if (action === "abort" && req.method === "POST")
689
+ return json(await sessions.abort(sessionId));
690
+ return json({ ok: false, error: "Not found" }, 404);
691
+ } catch (error) {
692
+ return json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
693
+ }
694
+ },
695
+ websocket: {
696
+ open(ws) {
697
+ const data = ws.data;
698
+ const cleanup = data.kind === "session-events" ? hub.addSession(data.sessionId, ws) : hub.addRun(data.runId, ws);
699
+ ws.__rigCleanup = cleanup;
700
+ },
701
+ message() {},
702
+ close(ws) {
703
+ ws.__rigCleanup?.();
704
+ }
705
+ }
706
+ });
707
+ config.port = server.port ?? requestedPort;
708
+ writeReadyFile(options.readyFile || process.env.RIG_PI_SESSIOND_READY_FILE, {
709
+ pid: process.pid,
710
+ host,
711
+ port: server.port ?? requestedPort,
712
+ token,
713
+ daemonId: config.daemonId,
714
+ rootDir,
715
+ startedAt,
716
+ version: config.version,
717
+ commit: config.commit
718
+ });
719
+ console.error(`[rig-pi-sessiond] listening on http://${host}:${server.port ?? requestedPort}`);
720
+ const shutdown = async () => {
721
+ await sessions.dispose();
722
+ server.stop(true);
723
+ process.exit(0);
724
+ };
725
+ process.once("SIGTERM", () => {
726
+ shutdown();
727
+ });
728
+ process.once("SIGINT", () => {
729
+ shutdown();
730
+ });
731
+ return await new Promise(() => {});
732
+ }
733
+ function health(port, config, activeSessions) {
734
+ return {
735
+ ok: true,
736
+ daemonId: config.daemonId,
737
+ version: config.version,
738
+ commit: config.commit,
739
+ pid: process.pid,
740
+ startedAt: config.startedAt,
741
+ activeSessions,
742
+ checkedAt: new Date().toISOString()
743
+ };
744
+ }
745
+ async function readJson(req) {
746
+ const value = await req.json().catch(() => ({}));
747
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
748
+ }
749
+ function json(payload, status = 200) {
750
+ return Response.json(payload, { status });
751
+ }
752
+ function isAuthorized(req, token) {
753
+ const url = new URL(req.url);
754
+ const queryToken = url.searchParams.get("token");
755
+ const header = req.headers.get("authorization") || "";
756
+ const match = header.match(/^Bearer\s+(.+)$/i);
757
+ return queryToken === token || match?.[1] === token;
758
+ }
759
+ function requireText(body, field) {
760
+ const value = body && typeof body === "object" && !Array.isArray(body) ? body[field] : undefined;
761
+ if (typeof value !== "string" || value.trim() === "")
762
+ throw new Error(`${field} is required`);
763
+ return value;
764
+ }
765
+ function optionalStreamingBehavior(body) {
766
+ const value = body && typeof body === "object" && !Array.isArray(body) ? body.streamingBehavior : undefined;
767
+ return value === "steer" || value === "followUp" ? value : undefined;
768
+ }
769
+ function asString(value) {
770
+ return typeof value === "string" && value.trim() ? value.trim() : null;
771
+ }
772
+ function numberFromEnv(value) {
773
+ if (!value?.trim())
774
+ return;
775
+ const parsed = Number(value);
776
+ return Number.isFinite(parsed) ? parsed : undefined;
777
+ }
778
+ function randomToken() {
779
+ return randomBytes(32).toString("hex");
780
+ }
781
+ function writeReadyFile(path, payload) {
782
+ if (!path)
783
+ return;
784
+ const resolved = resolve(path);
785
+ mkdirSync2(dirname(resolved), { recursive: true });
786
+ writeFileSync(resolved, `${JSON.stringify(payload, null, 2)}
787
+ `, "utf8");
788
+ }
789
+ function readDaemonReadyFile(path) {
790
+ if (!existsSync(path))
791
+ return null;
792
+ try {
793
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
794
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
795
+ } catch {
796
+ return null;
797
+ }
798
+ }
799
+ export {
800
+ runRigPiSessionDaemon,
801
+ readDaemonReadyFile
802
+ };