pi-sessions 0.9.0 → 0.11.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.
Files changed (42) hide show
  1. package/README.md +29 -20
  2. package/dist/session-messaging/broker/client-frame.js +62 -0
  3. package/dist/session-messaging/broker/process.js +234 -0
  4. package/dist/shared/session-broker/framing.js +59 -0
  5. package/dist/shared/session-broker/socket-path.js +19 -0
  6. package/extensions/pi-sessions.ts +14 -1
  7. package/extensions/session-auto-title/generate.ts +56 -66
  8. package/extensions/session-auto-title/install.ts +2 -0
  9. package/extensions/session-auto-title/runs.ts +50 -0
  10. package/extensions/session-handoff/board-view-model.ts +2 -4
  11. package/extensions/session-handoff/bootstrap.ts +14 -16
  12. package/extensions/session-handoff/extract.ts +34 -16
  13. package/extensions/session-handoff/install.ts +1 -1
  14. package/extensions/session-handoff/kickoff.ts +4 -3
  15. package/extensions/session-handoff/launch-target.ts +1 -1
  16. package/extensions/session-handoff/metadata.ts +72 -23
  17. package/extensions/session-messaging/broker/client-frame.ts +113 -0
  18. package/extensions/session-messaging/broker/process.ts +12 -21
  19. package/extensions/session-messaging/broker/spawn.ts +12 -7
  20. package/extensions/session-messaging/pi/reachable-contract.ts +59 -0
  21. package/extensions/session-messaging/pi/reachable-presenter.ts +49 -0
  22. package/extensions/session-messaging/pi/reachable-tool.ts +190 -0
  23. package/extensions/session-messaging/pi/reachable-view-model.ts +59 -0
  24. package/extensions/session-messaging/pi/renderer.ts +1 -1
  25. package/extensions/session-messaging/pi/tools.ts +8 -8
  26. package/extensions/session-search/hooks.ts +45 -52
  27. package/extensions/session-search/install.ts +3 -158
  28. package/extensions/session-search/reindex.ts +3 -3
  29. package/extensions/session-search/renderer.ts +1 -32
  30. package/extensions/session-search/tool-contract.ts +1 -21
  31. package/extensions/shared/model-runtime.ts +8 -1
  32. package/extensions/shared/session-broker/client.ts +3 -4
  33. package/extensions/shared/session-broker/framing.ts +6 -7
  34. package/extensions/shared/session-index/lineage.ts +6 -2
  35. package/extensions/shared/session-index/sqlite.ts +44 -17
  36. package/extensions/shared/settings.ts +25 -9
  37. package/extensions/shared/tmux.ts +22 -0
  38. package/extensions/subagents/classify.ts +6 -0
  39. package/extensions/subagents/install.ts +22 -137
  40. package/extensions/subagents/report-message-renderer.ts +2 -2
  41. package/extensions/subagents/settle.ts +182 -0
  42. package/package.json +18 -26
@@ -228,7 +228,7 @@ function syncSessionFileWithDb(
228
228
  const stat = statSync(sessionFile);
229
229
 
230
230
  if (baseline && isIndexCurrent(baseline, stat, sessionOrigin)) {
231
- db.transaction(() => writeHookSyncMetadata(db, eventType)).immediate();
231
+ db.transaction(() => writeHookSyncMetadata(db, eventType));
232
232
  return true;
233
233
  }
234
234
 
@@ -279,11 +279,6 @@ function isIndexCurrent(
279
279
  );
280
280
  }
281
281
 
282
- // All write transactions are immediate: they read before they write, and a
283
- // deferred transaction whose snapshot goes stale fails with SQLITE_BUSY on the
284
- // read-to-write upgrade without ever invoking the busy handler. Immediate mode
285
- // takes the write lock at BEGIN, where busy_timeout queues us behind concurrent
286
- // writers from other pi processes.
287
282
  function applyFullSync(
288
283
  db: SessionIndexDatabase,
289
284
  extracted: ExtractedSessionRecord,
@@ -312,7 +307,7 @@ function applyFullSync(
312
307
  }
313
308
 
314
309
  writeHookSyncMetadata(db, eventType);
315
- }).immediate();
310
+ });
316
311
  }
317
312
 
318
313
  function applyTailSync(
@@ -322,60 +317,58 @@ function applyTailSync(
322
317
  eventType: string,
323
318
  sessionOrigin?: SessionOrigin,
324
319
  ): boolean {
325
- return db
326
- .transaction((): boolean => {
327
- // Another process may have advanced the index between our baseline read
328
- // and this transaction; the tail deltas would then double-count.
329
- const current = getSessionRowByPath(db, baseline.sessionPath);
330
- if (
331
- !current ||
332
- current.sessionId !== baseline.sessionId ||
333
- current.indexedFileSize !== baseline.indexedFileSize
334
- ) {
335
- return false;
336
- }
320
+ return db.transaction((): boolean => {
321
+ // Another process may have advanced the index between our baseline read
322
+ // and this transaction; the tail deltas would then double-count.
323
+ const current = getSessionRowByPath(db, baseline.sessionPath);
324
+ if (
325
+ !current ||
326
+ current.sessionId !== baseline.sessionId ||
327
+ current.indexedFileSize !== baseline.indexedFileSize
328
+ ) {
329
+ return false;
330
+ }
337
331
 
338
- const scan = tail.scan;
339
- upsertSession(db, buildTailSessionRow(baseline, tail, sessionOrigin), "hook");
340
-
341
- if (scan.sessionName !== undefined && scan.sessionName !== baseline.sessionName) {
342
- clearSessionChunksBySourceKind(db, baseline.sessionId, "session_name");
343
- if (scan.sessionName && scan.sessionNameEntryId) {
344
- insertTextChunk(db, {
345
- sessionId: baseline.sessionId,
346
- ...createSessionNameChunk(
347
- scan.sessionName,
348
- scan.sessionNameTs ?? baseline.startedAt,
349
- scan.sessionNameEntryId,
350
- ),
351
- });
352
- }
353
- }
332
+ const scan = tail.scan;
333
+ upsertSession(db, buildTailSessionRow(baseline, tail, sessionOrigin), "hook");
354
334
 
355
- if (!baseline.handoffGoal && scan.handoffMetadata) {
356
- const { entryId, ts, metadata } = scan.handoffMetadata;
335
+ if (scan.sessionName !== undefined && scan.sessionName !== baseline.sessionName) {
336
+ clearSessionChunksBySourceKind(db, baseline.sessionId, "session_name");
337
+ if (scan.sessionName && scan.sessionNameEntryId) {
357
338
  insertTextChunk(db, {
358
339
  sessionId: baseline.sessionId,
359
- entryId,
360
- entryType: "custom",
361
- ts,
362
- sourceKind: "handoff_goal",
363
- text: metadata.goal,
340
+ ...createSessionNameChunk(
341
+ scan.sessionName,
342
+ scan.sessionNameTs ?? baseline.startedAt,
343
+ scan.sessionNameEntryId,
344
+ ),
364
345
  });
365
346
  }
347
+ }
366
348
 
367
- for (const chunk of scan.chunks) {
368
- insertTextChunk(db, { sessionId: baseline.sessionId, ...chunk });
369
- }
349
+ if (!baseline.handoffGoal && scan.handoffMetadata) {
350
+ const { entryId, ts, metadata } = scan.handoffMetadata;
351
+ insertTextChunk(db, {
352
+ sessionId: baseline.sessionId,
353
+ entryId,
354
+ entryType: "custom",
355
+ ts,
356
+ sourceKind: "handoff_goal",
357
+ text: metadata.goal,
358
+ });
359
+ }
370
360
 
371
- for (const fileTouch of scan.fileTouches) {
372
- insertSessionFileTouch(db, { sessionId: baseline.sessionId, ...fileTouch });
373
- }
361
+ for (const chunk of scan.chunks) {
362
+ insertTextChunk(db, { sessionId: baseline.sessionId, ...chunk });
363
+ }
374
364
 
375
- writeHookSyncMetadata(db, eventType);
376
- return true;
377
- })
378
- .immediate();
365
+ for (const fileTouch of scan.fileTouches) {
366
+ insertSessionFileTouch(db, { sessionId: baseline.sessionId, ...fileTouch });
367
+ }
368
+
369
+ writeHookSyncMetadata(db, eventType);
370
+ return true;
371
+ });
379
372
  }
380
373
 
381
374
  function buildTailSessionRow(
@@ -1,24 +1,14 @@
1
- import {
2
- type ExtensionAPI,
3
- type ExtensionContext,
4
- SessionManager,
5
- } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
2
  import { Type } from "typebox";
7
- import { isSessionStarting } from "../session-handoff/metadata.ts";
8
- import type { MessagingHandle } from "../session-messaging/install.ts";
9
3
  import type { IndexHandle } from "../shared/composition.ts";
10
- import { formatError } from "../shared/errors.ts";
11
4
  import { stripSearchSnippetMarkers } from "../shared/search-snippet.ts";
12
5
  import {
13
- getSessionById,
14
6
  type SearchSessionsParams,
15
7
  type SessionSearchEvidence,
16
8
  searchSessions,
17
9
  withSessionIndex,
18
10
  } from "../shared/session-index/index.ts";
19
11
  import type { SessionSettings } from "../shared/settings.ts";
20
- import type { SubagentState } from "../subagents/classify.ts";
21
- import type { SubagentRoster } from "../subagents/roster.ts";
22
12
  import { renderSessionSearchResult } from "./renderer.ts";
23
13
  import type {
24
14
  SessionSearchResult,
@@ -31,8 +21,6 @@ const DEFAULT_SESSION_SEARCH_LIMIT = 6;
31
21
  export interface SearchInstallDeps {
32
22
  settings: SessionSettings;
33
23
  index: IndexHandle;
34
- messaging?: MessagingHandle | undefined;
35
- roster?: SubagentRoster | undefined;
36
24
  }
37
25
 
38
26
  export function installSearch(pi: ExtensionAPI, deps: SearchInstallDeps): void {
@@ -46,7 +34,6 @@ export function installSearch(pi: ExtensionAPI, deps: SearchInstallDeps): void {
46
34
  promptGuidelines: [
47
35
  "Omit queries in session_search to list matching sessions chronologically.",
48
36
  "After session_search finds a session id, switch to session_ask for questions about it.",
49
- 'To identify your own subagents, use session_search with relationScope: "branch".',
50
37
  ],
51
38
  parameters: Type.Object({
52
39
  query: Type.Optional(
@@ -105,22 +92,11 @@ export function installSearch(pi: ExtensionAPI, deps: SearchInstallDeps): void {
105
92
  },
106
93
  ),
107
94
  ),
108
- live: Type.Optional(
109
- Type.Boolean({
110
- description: "When true, only return currently active sessions",
111
- }),
112
- ),
113
95
  kind: Type.Optional(
114
96
  Type.Union([Type.Literal("user"), Type.Literal("subagent")], {
115
97
  description: "Filter by session kind",
116
98
  }),
117
99
  ),
118
- relationScope: Type.Optional(
119
- Type.Union([Type.Literal("branch"), Type.Literal("tree")], {
120
- description:
121
- "Restrict results to subagents launched from the active branch or anywhere in this session's conversation tree",
122
- }),
123
- ),
124
100
  limit: Type.Optional(
125
101
  Type.Number({
126
102
  description: "Number of matches to return",
@@ -142,35 +118,9 @@ export function installSearch(pi: ExtensionAPI, deps: SearchInstallDeps): void {
142
118
  details: progressDetails,
143
119
  });
144
120
 
145
- const scope = await resolveSearchScope(params, deps);
146
-
147
121
  return withSessionIndex(indexPath, { mode: "read", required: true }, ({ db, status }) => {
148
- const results = searchSessions(
149
- db,
150
- buildSearchParams(params, ctx, scope.includeSessionIds),
151
- ).map((result): SessionSearchResult => {
152
- const annotation = scope.annotations.get(result.sessionId);
153
- if (!annotation) {
154
- return result;
155
- }
156
- if ("depth" in annotation) {
157
- return {
158
- ...result,
159
- state: annotation.state,
160
- depth: annotation.depth,
161
- onActiveBranch: annotation.onActiveBranch,
162
- };
163
- }
164
- return { ...result, state: annotation.state };
165
- });
166
- const details: SessionSearchToolDetails = {
167
- params,
168
- status,
169
- results,
170
- ...(scope.total === undefined
171
- ? {}
172
- : { scope: { matched: results.length, total: scope.total } }),
173
- };
122
+ const results = searchSessions(db, buildSearchParams(params, ctx));
123
+ const details: SessionSearchToolDetails = { params, status, results };
174
124
  return {
175
125
  content: [
176
126
  {
@@ -189,7 +139,6 @@ export function installSearch(pi: ExtensionAPI, deps: SearchInstallDeps): void {
189
139
  function buildSearchParams(
190
140
  params: SessionSearchToolParams,
191
141
  ctx: ExtensionContext,
192
- liveSessionIds?: string[] | undefined,
193
142
  ): SearchSessionsParams {
194
143
  const currentSessionId = ctx.sessionManager.getSessionId();
195
144
 
@@ -204,119 +153,15 @@ function buildSearchParams(
204
153
  kind: params.kind,
205
154
  sort: params.sort,
206
155
  limit: params.limit ?? DEFAULT_SESSION_SEARCH_LIMIT,
207
- includeSessionIds: liveSessionIds,
208
156
  relativeToSessionId: currentSessionId,
209
157
  };
210
158
  }
211
159
 
212
- interface StartingSearchResultAnnotation {
213
- state: "starting";
214
- }
215
-
216
- interface RelatedSubagentSearchResultAnnotation {
217
- state: SubagentState;
218
- depth: number;
219
- onActiveBranch: boolean;
220
- }
221
-
222
- type SearchResultAnnotation =
223
- | StartingSearchResultAnnotation
224
- | RelatedSubagentSearchResultAnnotation;
225
-
226
- interface ResolvedSearchScope {
227
- includeSessionIds: string[] | undefined;
228
- annotations: ReadonlyMap<string, SearchResultAnnotation>;
229
- total: number | undefined;
230
- }
231
-
232
- async function resolveSearchScope(
233
- params: SessionSearchToolParams,
234
- deps: SearchInstallDeps,
235
- ): Promise<ResolvedSearchScope> {
236
- if (params.relationScope) {
237
- if (!deps.roster) {
238
- throw new Error("Subagent relations are unavailable because subagents are not active.");
239
- }
240
- try {
241
- const roster = await deps.roster.resolve(params.relationScope);
242
- const entries = params.live
243
- ? roster.entries.filter((entry) => entry.managedLive)
244
- : roster.entries;
245
- const annotations = new Map<string, SearchResultAnnotation>();
246
- for (const entry of roster.entries) {
247
- annotations.set(entry.sessionId, {
248
- state: entry.state,
249
- depth: entry.depth,
250
- onActiveBranch: entry.onActiveBranch,
251
- });
252
- }
253
- return {
254
- includeSessionIds: entries.map((entry) => entry.sessionId),
255
- annotations,
256
- total: roster.total,
257
- };
258
- } catch (error) {
259
- throw new Error(`Subagent relation scope is unavailable: ${formatError(error)}`);
260
- }
261
- }
262
-
263
- if (!params.live) {
264
- return {
265
- includeSessionIds: undefined,
266
- annotations: new Map(),
267
- total: undefined,
268
- };
269
- }
270
-
271
- const liveSessionIds = await getLiveSessionIds(deps.messaging);
272
- return {
273
- includeSessionIds: liveSessionIds,
274
- annotations: getStartingSessionAnnotations(liveSessionIds, deps.index.path),
275
- total: undefined,
276
- };
277
- }
278
-
279
- function getStartingSessionAnnotations(
280
- sessionIds: readonly string[],
281
- indexPath: string,
282
- ): ReadonlyMap<string, SearchResultAnnotation> {
283
- return withSessionIndex(indexPath, { mode: "read", required: true }, ({ db }) => {
284
- const annotations = new Map<string, SearchResultAnnotation>();
285
- for (const sessionId of sessionIds) {
286
- const session = getSessionById(db, sessionId);
287
- if (!session) {
288
- continue;
289
- }
290
- try {
291
- if (isSessionStarting(SessionManager.open(session.sessionPath).getBranch())) {
292
- annotations.set(sessionId, { state: "starting" });
293
- }
294
- } catch {}
295
- }
296
- return annotations;
297
- });
298
- }
299
-
300
- async function getLiveSessionIds(messaging: MessagingHandle | undefined): Promise<string[]> {
301
- if (!messaging) {
302
- throw new Error(
303
- "Session messaging is not active; live session search requires session messaging.",
304
- );
305
- }
306
-
307
- try {
308
- return await messaging.listSessions();
309
- } catch (error) {
310
- throw new Error(`Session messaging is unavailable: ${formatError(error)}`);
311
- }
312
- }
313
-
314
160
  function formatSearchResultsForModel(details: SessionSearchToolDetails): string {
315
161
  return JSON.stringify(
316
162
  {
317
163
  params: details.params,
318
164
  status: details.status,
319
- ...(details.scope ? { scope: details.scope } : {}),
320
165
  results: details.results.map(formatSearchResultForModel),
321
166
  },
322
167
  null,
@@ -34,7 +34,7 @@ export async function rebuildSessionIndex(options: ReindexOptions): Promise<Rein
34
34
  db.transaction(() => {
35
35
  dropIndexTables(db);
36
36
  initializeSchema(db);
37
- }).immediate();
37
+ });
38
38
 
39
39
  let sessionCount = 0;
40
40
  let chunkCount = 0;
@@ -44,7 +44,7 @@ export async function rebuildSessionIndex(options: ReindexOptions): Promise<Rein
44
44
  continue;
45
45
  }
46
46
 
47
- db.transaction(() => indexSession(db, extracted)).immediate();
47
+ db.transaction(() => indexSession(db, extracted));
48
48
  sessionCount += 1;
49
49
  chunkCount += extracted.chunks.length;
50
50
  }
@@ -53,7 +53,7 @@ export async function rebuildSessionIndex(options: ReindexOptions): Promise<Rein
53
53
  rebuildSessionLineageRelations(db);
54
54
  setMetadata(db, "indexed_at", new Date().toISOString());
55
55
  setMetadata(db, "session_source", "SessionManager.listAll()");
56
- }).immediate();
56
+ });
57
57
 
58
58
  return { sessionCount, chunkCount, indexPath };
59
59
  } finally {
@@ -11,8 +11,6 @@ import {
11
11
  } from "../shared/search-snippet.ts";
12
12
  import { formatSessionTitleOrShortId } from "../shared/session-ui.ts";
13
13
  import type {
14
- AnnotatedSearchResult,
15
- RelatedSubagentSearchResult,
16
14
  SessionSearchResult,
17
15
  SessionSearchToolDetails,
18
16
  SessionSearchToolParams,
@@ -51,11 +49,6 @@ export function renderSessionSearchResult(
51
49
  }
52
50
 
53
51
  const lines = formatSessionSearchContextLines(details.params, theme);
54
- if (details.scope) {
55
- lines.push(
56
- theme.fg("dim", `scope: ${details.scope.matched} matched • ${details.scope.total} total`),
57
- );
58
- }
59
52
  if (details.results.length === 0) {
60
53
  if (lines.length > 0) lines.push("");
61
54
  lines.push(theme.fg("warning", "No matching sessions found."));
@@ -85,9 +78,7 @@ function formatSessionSearchContextLines(
85
78
  filters.push(`files.touched: ${params.files.touched.join(", ")}`);
86
79
  if (params.files?.changed?.length)
87
80
  filters.push(`files.changed: ${params.files.changed.join(", ")}`);
88
- if (params.live) filters.push("live: true");
89
81
  if (params.kind) filters.push(`kind: ${params.kind}`);
90
- if (params.relationScope) filters.push(`relationScope: ${params.relationScope}`);
91
82
  if (params.sort) filters.push(`sort: ${params.sort}`);
92
83
  if (params.time?.after?.trim()) filters.push(`after: ${params.time.after.trim()}`);
93
84
  if (params.time?.before?.trim()) filters.push(`before: ${params.time.before.trim()}`);
@@ -114,9 +105,7 @@ function formatSessionSearchPanelResults(
114
105
  const lines = visibleResults.flatMap((result, index) => {
115
106
  const location = formatSearchResultLocation(result.cwd);
116
107
  const relation = result.relation ? ` ${theme.fg("dim", `[${result.relation}]`)}` : "";
117
- const annotation = formatSearchResultAnnotation(result);
118
- const state = annotation.length > 0 ? ` ${theme.fg("dim", `[${annotation.join(" • ")}]`)}` : "";
119
- const heading = `${index + 1}. ${theme.bold(formatSearchResultLabel(result))}${relation}${state}${location ? ` ${theme.fg("dim", `(${location})`)}` : ""}`;
108
+ const heading = `${index + 1}. ${theme.bold(formatSearchResultLabel(result))}${relation}${location ? ` ${theme.fg("dim", `(${location})`)}` : ""}`;
120
109
  const snippets = params?.query ? formatSearchSnippets(result).slice(0, 3) : [];
121
110
  return snippets.length > 0
122
111
  ? [heading, ...snippets.map((snippet) => theme.fg("dim", ` - ${snippet}`))]
@@ -130,26 +119,6 @@ function formatSessionSearchPanelResults(
130
119
  return lines;
131
120
  }
132
121
 
133
- function formatSearchResultAnnotation(result: SessionSearchResult): string[] {
134
- if (!isAnnotatedSearchResult(result)) {
135
- return [];
136
- }
137
- if (!isRelatedSubagentSearchResult(result)) {
138
- return [result.state];
139
- }
140
- return [result.state, `depth ${result.depth}`, ...(result.onActiveBranch ? [] : ["history"])];
141
- }
142
-
143
- function isAnnotatedSearchResult(result: SessionSearchResult): result is AnnotatedSearchResult {
144
- return "state" in result;
145
- }
146
-
147
- function isRelatedSubagentSearchResult(
148
- result: AnnotatedSearchResult,
149
- ): result is RelatedSubagentSearchResult {
150
- return "depth" in result;
151
- }
152
-
153
122
  function formatSearchResultLabel(result: SessionSearchResult): string {
154
123
  return formatSessionTitleOrShortId(result.sessionName, result.sessionId);
155
124
  }
@@ -4,7 +4,6 @@ import type {
4
4
  SessionIndexStatus,
5
5
  SessionKind,
6
6
  } from "../shared/session-index/index.ts";
7
- import type { SubagentState } from "../subagents/classify.ts";
8
7
 
9
8
  export interface SessionSearchToolParams {
10
9
  query?: string;
@@ -20,32 +19,13 @@ export interface SessionSearchToolParams {
20
19
  };
21
20
  sort?: SearchSort;
22
21
  limit?: number;
23
- live?: boolean;
24
22
  kind?: SessionKind;
25
- relationScope?: "branch" | "tree";
26
23
  }
27
24
 
28
- export interface SessionSearchScopeDetails {
29
- matched: number;
30
- total: number;
31
- }
32
-
33
- export interface StartingSearchResult extends SearchSessionResult {
34
- state: "starting";
35
- }
36
-
37
- export interface RelatedSubagentSearchResult extends SearchSessionResult {
38
- state: SubagentState;
39
- depth: number;
40
- onActiveBranch: boolean;
41
- }
42
-
43
- export type AnnotatedSearchResult = StartingSearchResult | RelatedSubagentSearchResult;
44
- export type SessionSearchResult = SearchSessionResult | AnnotatedSearchResult;
25
+ export type SessionSearchResult = SearchSessionResult;
45
26
 
46
27
  export interface SessionSearchToolDetails {
47
28
  params?: SessionSearchToolParams | undefined;
48
29
  results: SessionSearchResult[];
49
30
  status?: SessionIndexStatus | undefined;
50
- scope?: SessionSearchScopeDetails | undefined;
51
31
  }
@@ -7,13 +7,20 @@ export type ModelRuntimeProvider = (modelRegistry: ModelRegistry) => Promise<Mod
7
7
  // ModelRuntime, but ExtensionContext only exposes the ModelRegistry compatibility facade,
8
8
  // which keeps its runtime private. Until pi exposes the runtime to extensions, build a
9
9
  // fresh one per use and mirror the registered providers (e.g. pi-claude-bridge) so their
10
- // configs — including live streamSimple closures — carry over by reference.
10
+ // live stream closures carry over by reference. Extensions register either a native
11
+ // Provider or a legacy provider config, and the facade exposes each through its own
12
+ // accessor while listing both id sets together.
11
13
  export async function createSessionModelRuntime(
12
14
  modelRegistry: ModelRegistry,
13
15
  ): Promise<ModelRuntime> {
14
16
  const modelRuntime = await ModelRuntime.create({ allowModelNetwork: false });
15
17
 
16
18
  for (const providerId of modelRegistry.getRegisteredProviderIds()) {
19
+ const native = modelRegistry.getRegisteredNativeProvider(providerId);
20
+ if (native) {
21
+ modelRuntime.registerNativeProvider(native);
22
+ continue;
23
+ }
17
24
  const config = modelRegistry.getRegisteredProviderConfig(providerId);
18
25
  if (config) {
19
26
  modelRuntime.registerProvider(providerId, config);
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { EventEmitter } from "node:events";
3
3
  import net from "node:net";
4
+ import { parseTypeBoxValue } from "../typebox.ts";
4
5
  import { readFrames, writeFrame } from "./framing.ts";
5
6
  import {
6
7
  BROKER_FRAME_SCHEMA,
@@ -176,10 +177,8 @@ export class SessionMessagingClient extends EventEmitter {
176
177
 
177
178
  private async readSocket(socket: net.Socket): Promise<void> {
178
179
  try {
179
- for await (const frame of readFrames(
180
- socket,
181
- BROKER_FRAME_SCHEMA,
182
- "Invalid session messaging broker frame",
180
+ for await (const frame of readFrames(socket, (value) =>
181
+ parseTypeBoxValue(BROKER_FRAME_SCHEMA, value, "Invalid session messaging broker frame"),
183
182
  )) {
184
183
  this.handleFrame(frame);
185
184
  }
@@ -1,7 +1,5 @@
1
1
  import type { Socket } from "node:net";
2
2
  import { createInterface } from "node:readline";
3
- import type { Static, TSchema } from "typebox";
4
- import { parseTypeBoxValue } from "../typebox.ts";
5
3
 
6
4
  const MAX_FRAME_BYTES = 256 * 1024;
7
5
 
@@ -9,11 +7,12 @@ export function writeFrame<T>(socket: Socket, frame: T): void {
9
7
  socket.write(`${JSON.stringify(frame)}\n`);
10
8
  }
11
9
 
12
- export async function* readFrames<T extends TSchema>(
10
+ // The parser is injected so the detached broker can run with no dependencies
11
+ // installed alongside it, while Pi-side callers keep using TypeBox schemas.
12
+ export async function* readFrames<T>(
13
13
  socket: Socket,
14
- schema: T,
15
- context: string,
16
- ): AsyncGenerator<Static<T>> {
14
+ parseFrame: (value: unknown) => T,
15
+ ): AsyncGenerator<T> {
17
16
  const reader = createInterface({ input: socket, crlfDelay: Number.POSITIVE_INFINITY });
18
17
  const lines: string[] = [];
19
18
  const waiters: Array<() => void> = [];
@@ -64,7 +63,7 @@ export async function* readFrames<T extends TSchema>(
64
63
  throw parseError instanceof Error ? parseError : new Error(String(parseError));
65
64
  }
66
65
 
67
- yield parseTypeBoxValue(schema, parsed, context);
66
+ yield parseFrame(parsed);
68
67
  }
69
68
  } finally {
70
69
  reader.close();
@@ -239,6 +239,7 @@ function buildSessionGraph(db: SessionIndexDatabase): SessionGraph {
239
239
  "Invalid session graph rows",
240
240
  );
241
241
 
242
+ const sessionIds = new Set(rows.map((row) => row.sessionId));
242
243
  const pathToId = new Map(rows.map((row) => [row.sessionPath, row.sessionId]));
243
244
  const nodes = new Map<string, SessionGraphNode>(
244
245
  rows.map((row) => [
@@ -246,8 +247,11 @@ function buildSessionGraph(db: SessionIndexDatabase): SessionGraph {
246
247
  {
247
248
  ...row,
248
249
  resolvedParentSessionId:
249
- row.parentSessionId ??
250
- (row.parentSessionPath ? pathToId.get(row.parentSessionPath) : undefined),
250
+ row.parentSessionId && sessionIds.has(row.parentSessionId)
251
+ ? row.parentSessionId
252
+ : row.parentSessionPath
253
+ ? pathToId.get(row.parentSessionPath)
254
+ : undefined,
251
255
  },
252
256
  ]),
253
257
  );