@hachej/boring-agent 0.1.105 → 0.1.108

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 (34) hide show
  1. package/dist/{chunk-K25PK5EC.js → chunk-3JFPS2O6.js} +1 -0
  2. package/dist/{chunk-JNEKOVMJ.js → chunk-6OANSUEI.js} +1 -1
  3. package/dist/{chunk-4IQ4HHBG.js → chunk-7XAOVFSN.js} +7 -0
  4. package/dist/{chunk-F2OJI2LX.js → chunk-GS4ADE34.js} +863 -358
  5. package/dist/{chunk-LFN45QDZ.js → chunk-KDXIXVF6.js} +5076 -3640
  6. package/dist/{chunk-6DRHERVK.js → chunk-MVKXLMH3.js} +44 -2
  7. package/dist/chunk-YTUZFKB3.js +78 -0
  8. package/dist/core/index.d.ts +5 -5
  9. package/dist/core/index.js +2 -2
  10. package/dist/{createHarness-GMBZZPTN.js → createHarness-AHWSC36D.js} +3 -3
  11. package/dist/{errors-DH7C01Pm.d.ts → errors-CEU4iujR.d.ts} +59 -4
  12. package/dist/{file-search-qcD5zItr.d.ts → file-search-BGUHnW9o.d.ts} +3 -3
  13. package/dist/front/index.d.ts +36 -7
  14. package/dist/front/index.js +1491 -727
  15. package/dist/front/styles.css +57 -0
  16. package/dist/{harness-Dcd08yKr.d.ts → harness-Ct0e4RWc.d.ts} +6 -3
  17. package/dist/{lease-DffrPrQn.d.ts → lease-nASWv2tU.d.ts} +1 -1
  18. package/dist/{piChatEvent-iNSXiFQq.d.ts → piChatEvent-BUWIzw5l.d.ts} +67 -36
  19. package/dist/{piChatSessionService-DQSQ_kWZ.d.ts → piChatSessionService-nvroRAVk.d.ts} +13 -11
  20. package/dist/server/agent-host/testing/gatewayConformance.d.ts +6 -5
  21. package/dist/server/agent-host/testing/gatewayConformance.js +57 -8
  22. package/dist/server/index.d.ts +167 -212
  23. package/dist/server/index.js +13 -9
  24. package/dist/server/worker/index.js +7 -7
  25. package/dist/shared/index.d.ts +34 -28
  26. package/dist/shared/index.js +8 -8
  27. package/dist/{types-BTsdaAQ2.d.ts → types-B7-PN5jV.d.ts} +47 -14
  28. package/docs/AGENT_GATEWAY_V0.md +40 -2
  29. package/docs/ERROR_CODES.md +1 -0
  30. package/docs/README.md +1 -0
  31. package/docs/SESSION_IDENTITY.md +91 -0
  32. package/docs/tools.md +4 -0
  33. package/package.json +11 -10
  34. package/dist/chunk-WSQ5QNIY.js +0 -18
@@ -1,19 +1,18 @@
1
1
  import {
2
2
  SAFE_NATIVE_SESSION_ID,
3
+ compareSessionOrder,
3
4
  noopTelemetry,
4
5
  safeCapture
5
- } from "./chunk-4IQ4HHBG.js";
6
+ } from "./chunk-7XAOVFSN.js";
6
7
  import {
7
8
  ErrorCode
8
- } from "./chunk-K25PK5EC.js";
9
+ } from "./chunk-3JFPS2O6.js";
9
10
 
10
11
  // src/server/harness/pi-coding-agent/createHarness.ts
11
12
  import { AsyncLocalStorage } from "node:async_hooks";
12
13
  import {
13
14
  createAgentSession,
14
15
  SessionManager as SessionManager2,
15
- AuthStorage,
16
- ModelRegistry,
17
16
  DefaultResourceLoader,
18
17
  getAgentDir as getAgentDir2,
19
18
  loadSkills
@@ -122,6 +121,11 @@ function createPiFollowUpQueueCompat() {
122
121
  if (nonce) seenNonces.add(nonce);
123
122
  return true;
124
123
  }
124
+ function rollback(text, options) {
125
+ const index = queue.findIndex((item) => item.text === text && item.clientNonce === options?.clientNonce && item.clientSeq === options?.clientSeq);
126
+ if (index >= 0) queue.splice(index, 1);
127
+ if (options?.clientNonce) seenNonces.delete(options.clientNonce);
128
+ }
125
129
  function clear(piSession, options) {
126
130
  syncWithPi(piSession);
127
131
  const removed = removeNativeFollowUp(options);
@@ -176,7 +180,7 @@ function createPiFollowUpQueueCompat() {
176
180
  if (queue.length === 0) seenNonces.clear();
177
181
  return removed;
178
182
  }
179
- return { record, clear };
183
+ return { record, rollback, clear };
180
184
  }
181
185
  function removePiQueuedFollowUp(piSession, text, textOrdinal = 0) {
182
186
  const queue = piQueueAccess(piSession);
@@ -241,12 +245,29 @@ function matchesFollowUpSelector(item, options) {
241
245
  }
242
246
 
243
247
  // src/server/pi-chat/PiAgentSessionAdapter.ts
248
+ var followUpQueuesBySession = /* @__PURE__ */ new WeakMap();
249
+ function followUpQueueFor(session) {
250
+ const existing = followUpQueuesBySession.get(session);
251
+ if (existing) return existing;
252
+ const created = createPiFollowUpQueueCompat();
253
+ followUpQueuesBySession.set(session, created);
254
+ return created;
255
+ }
244
256
  function normalizePromptInput(input) {
245
257
  if (typeof input === "string") return { text: input };
246
258
  return input;
247
259
  }
260
+ var cancellationStateBySession = /* @__PURE__ */ new WeakMap();
261
+ function cancellationStateFor(session) {
262
+ const existing = cancellationStateBySession.get(session);
263
+ if (existing) return existing;
264
+ const created = { abortRequested: false };
265
+ cancellationStateBySession.set(session, created);
266
+ return created;
267
+ }
248
268
  function createPiAgentSessionAdapter(session, options = {}) {
249
- const followUpQueue = createPiFollowUpQueueCompat();
269
+ const followUpQueue = followUpQueueFor(session);
270
+ const cancellationState = cancellationStateFor(session);
250
271
  const adapter = {
251
272
  readSnapshot() {
252
273
  return {
@@ -268,7 +289,25 @@ function createPiAgentSessionAdapter(session, options = {}) {
268
289
  return typeof model?.provider === "string" && typeof model?.id === "string" ? { provider: model.provider, id: model.id } : void 0;
269
290
  },
270
291
  subscribe(listener) {
271
- return session.subscribe(listener);
292
+ return session.subscribe((event) => {
293
+ if (event.type === "agent_start") cancellationState.abortRequested = false;
294
+ if (!cancellationState.abortRequested) {
295
+ listener(event);
296
+ return;
297
+ }
298
+ if (event.type === "message_end" && event.message.role === "assistant") {
299
+ listener({ ...event, message: { ...event.message, stopReason: "aborted" } });
300
+ return;
301
+ }
302
+ if (event.type === "agent_end") {
303
+ const messages = event.messages.map(
304
+ (message, index) => index === event.messages.length - 1 && message.role === "assistant" ? { ...message, stopReason: "aborted" } : message
305
+ );
306
+ listener({ ...event, messages });
307
+ return;
308
+ }
309
+ listener(event);
310
+ });
272
311
  },
273
312
  async prompt(input) {
274
313
  const { text, options: options2 } = normalizePromptInput(input);
@@ -277,12 +316,18 @@ function createPiAgentSessionAdapter(session, options = {}) {
277
316
  async followUp(text, followUpOptions) {
278
317
  const accepted = followUpQueue.record(text, followUpOptions);
279
318
  if (!accepted) return;
280
- await session.followUp(text);
319
+ try {
320
+ await session.followUp(text);
321
+ } catch (error) {
322
+ followUpQueue.rollback(text, followUpOptions);
323
+ throw error;
324
+ }
281
325
  },
282
326
  clearFollowUp(selector) {
283
327
  followUpQueue.clear(session, selector);
284
328
  },
285
329
  async abort() {
330
+ cancellationState.abortRequested = true;
286
331
  await session.abort();
287
332
  }
288
333
  };
@@ -296,20 +341,19 @@ function createPiAgentSessionAdapter(session, options = {}) {
296
341
  }
297
342
 
298
343
  // src/server/harness/pi-coding-agent/sessions.ts
299
- import { randomUUID } from "node:crypto";
344
+ import { randomUUID as randomUUID3 } from "node:crypto";
300
345
  import {
301
- readdir,
302
- readFile,
346
+ readdir as readdir2,
347
+ readFile as readFile3,
303
348
  stat as fsStat2,
304
- rm,
305
- mkdir,
306
- writeFile,
307
- appendFile,
308
- rename,
309
- open as open4
349
+ rm as rm2,
350
+ mkdir as mkdir2,
351
+ writeFile as writeFile2,
352
+ appendFile as appendFile2,
353
+ open as open3
310
354
  } from "node:fs/promises";
311
355
  import { closeSync, openSync, readFileSync, readSync, readdirSync, writeFileSync } from "node:fs";
312
- import { join, basename, resolve } from "node:path";
356
+ import { join as join2, basename, resolve } from "node:path";
313
357
  import { homedir } from "node:os";
314
358
 
315
359
  // src/server/config/env.ts
@@ -324,7 +368,8 @@ import {
324
368
  } from "@mariozechner/pi-coding-agent";
325
369
 
326
370
  // src/server/harness/pi-coding-agent/nativeSessionRename.ts
327
- import { open as open2, stat as fsStat, utimes } from "node:fs/promises";
371
+ import { randomUUID } from "node:crypto";
372
+ import { appendFile, readFile, stat as fsStat, utimes } from "node:fs/promises";
328
373
  import {
329
374
  CURRENT_SESSION_VERSION,
330
375
  SessionManager
@@ -360,99 +405,9 @@ function completeJsonlPrefix(content, reachedLimit) {
360
405
  return lastNewline >= 0 ? content.slice(0, lastNewline + 1) : content;
361
406
  }
362
407
 
363
- // src/server/harness/pi-coding-agent/nativeSessionRename.ts
364
- var NATIVE_RENAME_MAX_APPEND_BYTES = 64 * 1024;
365
- var NATIVE_RENAME_MAX_ATTEMPTS = 3;
366
- async function restoreVerifiedNativeRenameMtime(filepath, before, verifiedSize) {
367
- try {
368
- const current = await fsStat(filepath);
369
- if (current.dev !== before.dev || current.ino !== before.ino || current.size !== verifiedSize) return;
370
- const atimeSeconds = fileTimeSeconds(before.atimeMs);
371
- const mtimeSeconds = fileTimeSeconds(before.mtimeMs);
372
- if (atimeSeconds === void 0 || mtimeSeconds === void 0) return;
373
- await utimes(filepath, atimeSeconds, mtimeSeconds);
374
- const restored = await fsStat(filepath);
375
- if (restored.size !== verifiedSize) {
376
- const restoredAtimeSeconds = fileTimeSeconds(restored.atimeMs);
377
- if (restoredAtimeSeconds !== void 0) await utimes(filepath, restoredAtimeSeconds, Date.now() / 1e3);
378
- }
379
- } catch {
380
- }
381
- }
382
- async function verifiedNativeRenameAppend(filepath, before, append, title) {
383
- const after = await fsStat(filepath);
384
- if (after.dev !== before.dev || after.ino !== before.ino) return null;
385
- const appendedBytes = after.size - before.size;
386
- if (!Number.isSafeInteger(before.size) || !Number.isSafeInteger(after.size) || appendedBytes <= 0 || appendedBytes > NATIVE_RENAME_MAX_APPEND_BYTES) return null;
387
- const handle = await open2(filepath, "r");
388
- try {
389
- const record = Buffer.allocUnsafe(appendedBytes);
390
- const { bytesRead } = await handle.read(record, 0, record.length, before.size);
391
- if (bytesRead !== record.length || record.at(-1) !== 10 || record.indexOf(10) !== record.length - 1) return null;
392
- const parsed = JSON.parse(record.toString("utf-8"));
393
- if (typeof parsed !== "object" || parsed === null) return null;
394
- const entry = parsed;
395
- return entry.type === "session_info" && entry.id === append.id && entry.parentId === append.parentId && entry.name === title ? after.size : null;
396
- } catch {
397
- return null;
398
- } finally {
399
- await handle.close();
400
- }
401
- }
402
- function latestConcurrentEntryId(manager, staleRenameIds) {
403
- for (const entry of manager.getEntries().reverse()) {
404
- if (!staleRenameIds.has(entry.id)) return entry.id;
405
- }
406
- return null;
407
- }
408
- function nativeRenameLockedError(message = "native session changed while renaming; retry") {
409
- return Object.assign(new Error(message), {
410
- code: ErrorCode.enum.SESSION_LOCKED,
411
- statusCode: 409,
412
- retryable: true
413
- });
414
- }
415
- async function appendVerifiedNativeRename(filepath, sessionDir, cwd, title) {
416
- const staleRenameIds = /* @__PURE__ */ new Set();
417
- for (let attempt = 0; attempt < NATIVE_RENAME_MAX_ATTEMPTS; attempt += 1) {
418
- try {
419
- const before = await fsStat(filepath);
420
- const header = parseJsonlPrefixEntries(await readJsonlPrefix(filepath)).find(
421
- (entry) => entry.type === "session"
422
- );
423
- if (header?.version !== CURRENT_SESSION_VERSION) {
424
- throw nativeRenameLockedError("This native session was created by an unsupported Pi version and cannot be renamed.");
425
- }
426
- const manager = SessionManager.open(filepath, sessionDir, cwd);
427
- if (staleRenameIds.size > 0) {
428
- const concurrentLeaf = latestConcurrentEntryId(manager, staleRenameIds);
429
- if (!concurrentLeaf) break;
430
- manager.branch(concurrentLeaf);
431
- }
432
- const append = {
433
- parentId: manager.getLeafId(),
434
- id: manager.appendSessionInfo(title)
435
- };
436
- staleRenameIds.add(append.id);
437
- const verifiedSize = await verifiedNativeRenameAppend(filepath, before, append, title);
438
- if (verifiedSize !== null) {
439
- await restoreVerifiedNativeRenameMtime(filepath, before, verifiedSize);
440
- return;
441
- }
442
- } catch (error) {
443
- if (error.code === ErrorCode.enum.SESSION_LOCKED) throw error;
444
- }
445
- }
446
- throw nativeRenameLockedError();
447
- }
448
- function fileTimeSeconds(milliseconds) {
449
- const seconds = milliseconds / 1e3;
450
- return Number.isFinite(seconds) ? seconds : void 0;
451
- }
452
-
453
408
  // src/server/harness/pi-coding-agent/nativeSessionTranscript.ts
454
409
  import { createReadStream } from "node:fs";
455
- import { open as open3 } from "node:fs/promises";
410
+ import { open as open2 } from "node:fs/promises";
456
411
  import { createInterface } from "node:readline";
457
412
 
458
413
  // src/server/harness/pi-coding-agent/piSessionMessages.ts
@@ -465,14 +420,108 @@ function textFromPiContent(content) {
465
420
  }).join("");
466
421
  }
467
422
 
423
+ // src/server/harness/pi-coding-agent/sessionTitleAuthority.ts
424
+ var USER_SESSION_TITLE_CUSTOM_TYPE = "boring.session-title-authority";
425
+ var MAX_USER_SESSION_TITLE_CHARACTERS = 200;
426
+ function normalizeUserSessionTitle(title) {
427
+ const normalized = title.replace(/[\r\n]+/g, " ").trim();
428
+ if (!normalized) throw new Error("Session title must not be empty");
429
+ if (normalized.length > MAX_USER_SESSION_TITLE_CHARACTERS) {
430
+ throw new Error(`Session title must be at most ${MAX_USER_SESSION_TITLE_CHARACTERS} characters`);
431
+ }
432
+ return normalized;
433
+ }
434
+ function userSessionTitleData(title) {
435
+ return { titleSetByUser: true, title };
436
+ }
437
+ function isAttachedUserSessionTitleMarker(parent, marker) {
438
+ if (marker?.type !== "custom" || typeof marker.id !== "string" || !marker.id || marker.customType !== USER_SESSION_TITLE_CUSTOM_TYPE) return false;
439
+ if (parent && (typeof parent.id !== "string" || !parent.id)) return false;
440
+ return marker.parentId === (parent?.id ?? null);
441
+ }
442
+ function userSessionTitleFromSequence(parent, marker, titleEntry) {
443
+ if (!isAttachedUserSessionTitleMarker(parent, marker)) return void 0;
444
+ if (titleEntry?.type !== "session_info" || typeof titleEntry.id !== "string" || !titleEntry.id || titleEntry.parentId !== marker?.id) return void 0;
445
+ if (parent?.id === marker?.id || parent?.id === titleEntry.id || marker?.id === titleEntry.id) return void 0;
446
+ const data = marker?.data;
447
+ if (data?.titleSetByUser !== true || typeof data.title !== "string") return void 0;
448
+ try {
449
+ const title = normalizeUserSessionTitle(data.title);
450
+ return titleEntry.name === title ? title : void 0;
451
+ } catch {
452
+ return void 0;
453
+ }
454
+ }
455
+ function createUserSessionTitleProjection() {
456
+ let parent;
457
+ let marker;
458
+ let previousKnown = false;
459
+ let markerPredecessorKnown = false;
460
+ let title;
461
+ let timestamp;
462
+ const reset = (predecessorKnown) => {
463
+ parent = void 0;
464
+ marker = void 0;
465
+ previousKnown = predecessorKnown;
466
+ markerPredecessorKnown = false;
467
+ };
468
+ return {
469
+ get title() {
470
+ return title;
471
+ },
472
+ get timestamp() {
473
+ return timestamp;
474
+ },
475
+ accept(entry) {
476
+ if (entry.type === "session" || entry.type === "pi_session_file") {
477
+ reset(true);
478
+ return;
479
+ }
480
+ const projected = markerPredecessorKnown ? userSessionTitleFromSequence(parent, marker, entry) : void 0;
481
+ if (projected) {
482
+ title = projected;
483
+ timestamp = typeof entry.timestamp === "string" ? entry.timestamp : void 0;
484
+ }
485
+ parent = marker;
486
+ marker = entry;
487
+ markerPredecessorKnown = previousKnown;
488
+ previousKnown = true;
489
+ },
490
+ breakSequence() {
491
+ reset(false);
492
+ }
493
+ };
494
+ }
495
+ function createUserSessionTitleEntries(input) {
496
+ const authority = {
497
+ type: "custom",
498
+ id: input.authorityId,
499
+ parentId: input.parentId,
500
+ timestamp: input.timestamp,
501
+ customType: USER_SESSION_TITLE_CUSTOM_TYPE,
502
+ data: userSessionTitleData(input.title)
503
+ };
504
+ return {
505
+ authority,
506
+ title: {
507
+ type: "session_info",
508
+ id: input.titleId,
509
+ parentId: input.authorityId,
510
+ timestamp: input.timestamp,
511
+ name: input.title
512
+ }
513
+ };
514
+ }
515
+
468
516
  // src/server/harness/pi-coding-agent/nativeSessionTranscript.ts
469
517
  var NATIVE_TAIL_CHUNK_BYTES = 64 * 1024;
470
518
  var NATIVE_TAIL_MAX_RECORD_BYTES = 256 * 1024;
471
519
  var NATIVE_TAIL_MAX_RECORD_FRAGMENTS = 4;
472
- async function latestNativeMessageTimestamp(filepath, size) {
473
- const handle = await open3(filepath, "r");
520
+ async function nativeTranscriptOrdering(filepath, size) {
521
+ const handle = await open2(filepath, "r");
474
522
  let end = size;
475
523
  let lineFragments = [];
524
+ let hasEntries = false;
476
525
  try {
477
526
  while (end > 0) {
478
527
  const start = Math.max(0, end - NATIVE_TAIL_CHUNK_BYTES);
@@ -482,11 +531,14 @@ async function latestNativeMessageTimestamp(filepath, size) {
482
531
  while (lineEnd > 0) {
483
532
  const newline = chunk.lastIndexOf(10, lineEnd - 1);
484
533
  if (newline < 0) break;
485
- const timestamp = nativeMessageTimestampFromBoundedPrefix(
534
+ const metadata2 = nativeRecordOrderingFromBoundedPrefix(
486
535
  nativeTailRecordPrefix(chunk.subarray(newline + 1, lineEnd), lineFragments)
487
536
  );
488
537
  lineFragments = [];
489
- if (timestamp !== void 0) return timestamp;
538
+ hasEntries ||= metadata2.hasEntry;
539
+ if (metadata2.latestMessageAtMs !== void 0) {
540
+ return { hasEntries: true, latestMessageAtMs: metadata2.latestMessageAtMs };
541
+ }
490
542
  lineEnd = newline;
491
543
  }
492
544
  if (lineEnd > 0) {
@@ -494,9 +546,13 @@ async function latestNativeMessageTimestamp(filepath, size) {
494
546
  }
495
547
  end = start;
496
548
  }
497
- return nativeMessageTimestampFromBoundedPrefix(
549
+ const metadata = nativeRecordOrderingFromBoundedPrefix(
498
550
  nativeTailRecordPrefix(lineFragments.at(-1) ?? Buffer.alloc(0), lineFragments.slice(0, -1))
499
551
  );
552
+ return {
553
+ hasEntries: hasEntries || metadata.hasEntry,
554
+ ...metadata.latestMessageAtMs !== void 0 ? { latestMessageAtMs: metadata.latestMessageAtMs } : {}
555
+ };
500
556
  } finally {
501
557
  await handle.close();
502
558
  }
@@ -526,44 +582,419 @@ function nativeTailRecordPrefix(recordStart, followingFragments) {
526
582
  }
527
583
  return prefix;
528
584
  }
529
- function nativeMessageTimestampFromBoundedPrefix(prefix) {
530
- if (prefix.length === 0) return void 0;
585
+ function nativeRecordOrderingFromBoundedPrefix(prefix) {
586
+ if (prefix.length === 0) return { hasEntry: false };
531
587
  const line = prefix.subarray(0, NATIVE_TAIL_MAX_RECORD_BYTES).toString("utf-8");
532
- if (!/^\s*\{\s*"type"\s*:\s*"message"(?:\s*,|\s*})/.test(line)) return void 0;
588
+ const typeMatch = /^\s*\{\s*"type"\s*:\s*"([^"\\]+)"(?:\s*,|\s*})/.exec(line);
589
+ if (!typeMatch || typeMatch[1] === "session") return { hasEntry: false };
590
+ if (typeMatch[1] !== "message") return { hasEntry: true };
533
591
  const timestampMatch = /"timestamp"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/.exec(line);
534
- if (!timestampMatch) return void 0;
592
+ if (!timestampMatch) return { hasEntry: true };
535
593
  const timestamp = Date.parse(timestampMatch[1]);
536
- return Number.isFinite(timestamp) ? timestamp : void 0;
594
+ return Number.isFinite(timestamp) ? { hasEntry: true, latestMessageAtMs: timestamp } : { hasEntry: true };
537
595
  }
538
- async function summarizeNativeTranscript(filepath) {
596
+ function createSessionTranscriptAccumulator() {
539
597
  let title;
598
+ let titleTimestamp;
540
599
  let firstUserTitle;
600
+ let entryCount = 0;
541
601
  let turnCount = 0;
542
- let hasAssistantReply2 = false;
602
+ let hasAssistantReply = false;
543
603
  let latestMessageAtMs;
604
+ const userTitle = createUserSessionTitleProjection();
605
+ const authorityMarkerIds = /* @__PURE__ */ new Set();
606
+ return {
607
+ acceptLine(line, committed = true) {
608
+ if (!line.trim()) return void 0;
609
+ let entry;
610
+ try {
611
+ entry = JSON.parse(line);
612
+ } catch {
613
+ userTitle.breakSequence();
614
+ return void 0;
615
+ }
616
+ if (!entry || typeof entry !== "object" || typeof entry.type !== "string") {
617
+ userTitle.breakSequence();
618
+ return void 0;
619
+ }
620
+ const record = entry;
621
+ if (record.type === "custom" && record.customType === USER_SESSION_TITLE_CUSTOM_TYPE && typeof record.id === "string") authorityMarkerIds.add(record.id);
622
+ if (committed) userTitle.accept(entry);
623
+ else userTitle.breakSequence();
624
+ if (entry.type !== "session") entryCount += 1;
625
+ if (entry.type === "session_info" && typeof entry.name === "string") {
626
+ if (!authorityMarkerIds.has(entry.parentId ?? "")) {
627
+ title = entry.name;
628
+ titleTimestamp = typeof entry.timestamp === "string" ? entry.timestamp : void 0;
629
+ }
630
+ } else if (entry.type === "message") {
631
+ const timestamp = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : Number.NaN;
632
+ if (!Number.isNaN(timestamp)) latestMessageAtMs = timestamp;
633
+ if (entry.message?.role === "user") {
634
+ turnCount += 1;
635
+ firstUserTitle ??= textFromPiContent(entry.message.content).slice(0, 80) || void 0;
636
+ } else if (entry.message?.role === "assistant") {
637
+ hasAssistantReply = true;
638
+ }
639
+ }
640
+ return entry;
641
+ },
642
+ summary() {
643
+ return {
644
+ ...title ? { title } : {},
645
+ ...titleTimestamp ? { titleTimestamp } : {},
646
+ ...userTitle.title ? { userTitle: userTitle.title } : {},
647
+ ...userTitle.timestamp ? { userTitleTimestamp: userTitle.timestamp } : {},
648
+ ...firstUserTitle ? { firstUserTitle } : {},
649
+ entryCount,
650
+ turnCount,
651
+ hasAssistantReply,
652
+ ...latestMessageAtMs !== void 0 ? { latestMessageAtMs } : {}
653
+ };
654
+ }
655
+ };
656
+ }
657
+ function parseSessionTranscript(content) {
658
+ const accumulator = createSessionTranscriptAccumulator();
659
+ const entries = [];
660
+ const physicalLines = content.split("\n");
661
+ const lastIndex = physicalLines.length - 1;
662
+ for (const [index, line] of physicalLines.entries()) {
663
+ const entry = accumulator.acceptLine(line, index < lastIndex);
664
+ if (entry) entries.push(entry);
665
+ }
666
+ return { entries, summary: accumulator.summary() };
667
+ }
668
+ async function transcriptEndsWithNewline(filepath) {
669
+ const handle = await open2(filepath, "r");
670
+ try {
671
+ const { size } = await handle.stat();
672
+ if (size === 0) return true;
673
+ const byte = Buffer.allocUnsafe(1);
674
+ const { bytesRead } = await handle.read(byte, 0, 1, size - 1);
675
+ return bytesRead === 1 && byte[0] === 10;
676
+ } finally {
677
+ await handle.close();
678
+ }
679
+ }
680
+ async function summarizeSessionTranscript(filepath) {
681
+ const accumulator = createSessionTranscriptAccumulator();
682
+ const endsWithNewline = await transcriptEndsWithNewline(filepath);
544
683
  const input = createReadStream(filepath, { encoding: "utf-8" });
545
684
  const lines = createInterface({ input, crlfDelay: Infinity });
685
+ let pending;
546
686
  for await (const line of lines) {
687
+ if (pending !== void 0) accumulator.acceptLine(pending);
688
+ pending = line;
689
+ }
690
+ if (pending !== void 0) accumulator.acceptLine(pending, endsWithNewline);
691
+ return accumulator.summary();
692
+ }
693
+
694
+ // src/server/harness/pi-coding-agent/nativeSessionRename.ts
695
+ var SESSION_TITLE_MAX_APPEND_ATTEMPTS = 3;
696
+ var SESSION_TITLE_RETRY_CUSTOM_TYPE = "boring.session-title-retry";
697
+ function sameSnapshot(a, b) {
698
+ return a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
699
+ }
700
+ async function readStableFile(filepath) {
701
+ const before = await fsStat(filepath);
702
+ const content = await readFile(filepath, "utf-8");
703
+ const after = await fsStat(filepath);
704
+ return sameSnapshot(before, after) ? { before, after, content } : null;
705
+ }
706
+ async function restoreVerifiedRenameMtime(filepath, before, verifiedSize) {
707
+ try {
708
+ const current = await fsStat(filepath);
709
+ if (current.dev !== before.dev || current.ino !== before.ino || current.size !== verifiedSize) return;
710
+ const atimeSeconds = fileTimeSeconds(before.atimeMs);
711
+ const mtimeSeconds = fileTimeSeconds(before.mtimeMs);
712
+ if (atimeSeconds === void 0 || mtimeSeconds === void 0) return;
713
+ await utimes(filepath, atimeSeconds, mtimeSeconds);
714
+ const restored = await fsStat(filepath);
715
+ if (restored.size !== verifiedSize) {
716
+ const restoredAtimeSeconds = fileTimeSeconds(restored.atimeMs);
717
+ if (restoredAtimeSeconds !== void 0) await utimes(filepath, restoredAtimeSeconds, Date.now() / 1e3);
718
+ }
719
+ } catch {
720
+ }
721
+ }
722
+ function committedPhysicalLines(content) {
723
+ const lines = content.split("\n");
724
+ if (!content.endsWith("\n")) lines.pop();
725
+ return lines;
726
+ }
727
+ function hasVerifiedTitleSequence(content, append, title) {
728
+ let parent;
729
+ let marker;
730
+ let previousKnown = false;
731
+ let markerPredecessorKnown = false;
732
+ for (const line of committedPhysicalLines(content)) {
547
733
  if (!line.trim()) continue;
734
+ let entry;
548
735
  try {
549
- const entry = JSON.parse(line);
550
- if (entry.type === "session_info" && typeof entry.name === "string") {
551
- title = entry.name;
552
- continue;
553
- }
554
- if (entry.type !== "message") continue;
555
- const timestamp = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : Number.NaN;
556
- if (!Number.isNaN(timestamp)) latestMessageAtMs = timestamp;
557
- if (entry.message?.role === "user") {
558
- turnCount += 1;
559
- firstUserTitle ??= textFromPiContent(entry.message.content).slice(0, 80) || void 0;
560
- } else if (entry.message?.role === "assistant") {
561
- hasAssistantReply2 = true;
562
- }
736
+ entry = JSON.parse(line);
563
737
  } catch {
738
+ parent = void 0;
739
+ marker = void 0;
740
+ previousKnown = false;
741
+ markerPredecessorKnown = false;
742
+ continue;
743
+ }
744
+ if (!entry || typeof entry !== "object" || typeof entry.type !== "string") {
745
+ parent = void 0;
746
+ marker = void 0;
747
+ previousKnown = false;
748
+ markerPredecessorKnown = false;
749
+ continue;
750
+ }
751
+ if (entry.type === "session" || entry.type === "pi_session_file") {
752
+ parent = void 0;
753
+ marker = void 0;
754
+ previousKnown = true;
755
+ markerPredecessorKnown = false;
756
+ continue;
757
+ }
758
+ if (entry.id === append.title.id && marker?.id === append.authority.id && markerPredecessorKnown && marker.parentId === append.authority.parentId && entry.parentId === append.title.parentId && userSessionTitleFromSequence(parent, marker, entry) === title) return true;
759
+ parent = marker;
760
+ marker = entry;
761
+ markerPredecessorKnown = previousKnown;
762
+ previousKnown = true;
763
+ }
764
+ return false;
765
+ }
766
+ async function verifyTitleSequence(filepath, append, title) {
767
+ const snapshot = await readStableFile(filepath);
768
+ if (!snapshot || !hasVerifiedTitleSequence(snapshot.content, append, title)) return null;
769
+ const parsed = parseSessionTranscript(snapshot.content);
770
+ const tailId = parsed.entries.filter((entry) => entry.type !== "session" && typeof entry.id === "string").at(-1)?.id ?? null;
771
+ return { stat: snapshot.after, content: snapshot.content, tailId };
772
+ }
773
+ function isExactRenameOnlyAppend(origin, verifiedContent, append) {
774
+ const prefix = `${origin.content}${origin.content.endsWith("\n") ? "" : "\n"}`;
775
+ if (!verifiedContent.startsWith(prefix)) return false;
776
+ const suffix = verifiedContent.slice(prefix.length);
777
+ const lines = suffix.split("\n");
778
+ if (lines.length !== 3 || lines[2] !== "" || !lines[0]?.trim() || !lines[1]?.trim()) return false;
779
+ try {
780
+ const entries = lines.slice(0, 2).map((line) => JSON.parse(line));
781
+ return entries[0]?.id === append.authority.id && entries[1]?.id === append.title.id;
782
+ } catch {
783
+ return false;
784
+ }
785
+ }
786
+ function physicalTail(content) {
787
+ const endsWithNewline = content.length === 0 || content.endsWith("\n");
788
+ const line = content.split("\n").reverse().find((candidate) => candidate.trim());
789
+ if (!line) return { endsWithNewline, valid: true, parentId: null };
790
+ try {
791
+ const entry = JSON.parse(line);
792
+ if (!entry || typeof entry !== "object" || typeof entry.type !== "string") {
793
+ return { endsWithNewline, valid: false, parentId: null };
794
+ }
795
+ if (entry.type === "session" || entry.type === "pi_session_file") {
796
+ return { endsWithNewline, valid: true, parentId: null };
797
+ }
798
+ return typeof entry.id === "string" && entry.id ? { endsWithNewline, valid: true, parentId: entry.id } : { endsWithNewline, valid: false, parentId: null };
799
+ } catch {
800
+ return { endsWithNewline, valid: false, parentId: null };
801
+ }
802
+ }
803
+ function latestConcurrentEntryId(manager, staleRenameIds) {
804
+ for (const entry of manager.getEntries().reverse()) {
805
+ if (!staleRenameIds.has(entry.id)) return entry.id;
806
+ }
807
+ return null;
808
+ }
809
+ function sessionTitleLockedError(message = "session changed while renaming; retry") {
810
+ return Object.assign(new Error(message), {
811
+ code: ErrorCode.enum.SESSION_LOCKED,
812
+ statusCode: 409,
813
+ retryable: true
814
+ });
815
+ }
816
+ function sessionTranscriptUnreadableError(message) {
817
+ return Object.assign(new Error(message), {
818
+ code: ErrorCode.enum.SESSION_TRANSCRIPT_UNREADABLE,
819
+ statusCode: 500,
820
+ retryable: false
821
+ });
822
+ }
823
+ async function repairNativeTerminalNewline(filepath) {
824
+ const snapshot = await readStableFile(filepath);
825
+ if (!snapshot) return false;
826
+ const tail = physicalTail(snapshot.content);
827
+ if (tail.endsWithNewline) return true;
828
+ if (!tail.valid) throw sessionTitleLockedError("native session has an incomplete transcript record");
829
+ await appendFile(filepath, "\n");
830
+ const repaired = await readStableFile(filepath);
831
+ return Boolean(repaired?.content.endsWith("\n"));
832
+ }
833
+ async function appendVerifiedNativeRename(filepath, sessionDir, cwd, title) {
834
+ const staleRenameIds = /* @__PURE__ */ new Set();
835
+ let renameOnlyOrigin;
836
+ for (let attempt = 0; attempt < SESSION_TITLE_MAX_APPEND_ATTEMPTS; attempt += 1) {
837
+ const attemptOrigin = await readStableFile(filepath);
838
+ if (!attemptOrigin) continue;
839
+ if (attempt === 0) renameOnlyOrigin = attemptOrigin;
840
+ const header = parseJsonlPrefixEntries(await readJsonlPrefix(filepath)).find(
841
+ (entry) => entry.type === "session"
842
+ );
843
+ if (header?.version !== CURRENT_SESSION_VERSION) {
844
+ throw sessionTitleLockedError("This native session was created by an unsupported Pi version and cannot be renamed.");
845
+ }
846
+ if (!await repairNativeTerminalNewline(filepath)) continue;
847
+ const committed = await readStableFile(filepath);
848
+ if (!committed) continue;
849
+ if (!physicalTail(committed.content).valid) {
850
+ throw sessionTranscriptUnreadableError("native session contains a malformed transcript record");
851
+ }
852
+ const manager = SessionManager.open(filepath, sessionDir, cwd);
853
+ if (staleRenameIds.size > 0) {
854
+ const concurrentLeaf = latestConcurrentEntryId(manager, staleRenameIds);
855
+ if (!concurrentLeaf) break;
856
+ manager.branch(concurrentLeaf);
857
+ manager.appendCustomEntry(SESSION_TITLE_RETRY_CUSTOM_TYPE, {});
858
+ }
859
+ const beforeAppend = await readStableFile(filepath);
860
+ if (!beforeAppend || !beforeAppend.content.endsWith("\n")) continue;
861
+ const tail = physicalTail(beforeAppend.content);
862
+ const authorityParentId = manager.getLeafId();
863
+ if (!tail.valid || tail.parentId !== authorityParentId) continue;
864
+ const authorityId = manager.appendCustomEntry(
865
+ USER_SESSION_TITLE_CUSTOM_TYPE,
866
+ userSessionTitleData(title)
867
+ );
868
+ const titleId = manager.appendSessionInfo(title);
869
+ const append = {
870
+ authority: { id: authorityId, parentId: authorityParentId },
871
+ title: { id: titleId, parentId: authorityId }
872
+ };
873
+ staleRenameIds.add(authorityId);
874
+ staleRenameIds.add(titleId);
875
+ const verified = await verifyTitleSequence(filepath, append, title);
876
+ if (!verified) continue;
877
+ if (attempt === 0 && verified.tailId === titleId && renameOnlyOrigin && isExactRenameOnlyAppend(renameOnlyOrigin, verified.content, append)) {
878
+ await restoreVerifiedRenameMtime(filepath, renameOnlyOrigin.before, Number(verified.stat.size));
564
879
  }
880
+ return;
881
+ }
882
+ throw sessionTitleLockedError();
883
+ }
884
+ function wrapperPredecessor(content) {
885
+ const tail = physicalTail(content);
886
+ if (!tail.valid) {
887
+ if (!tail.endsWithNewline) throw sessionTitleLockedError("session has an incomplete transcript record");
888
+ throw sessionTranscriptUnreadableError("session contains a malformed transcript record");
889
+ }
890
+ return {
891
+ parentId: tail.parentId,
892
+ separator: tail.endsWithNewline ? "" : "\n"
893
+ };
894
+ }
895
+ async function appendVerifiedWrapperRename(filepath, title) {
896
+ let renameOnlyStart;
897
+ for (let attempt = 0; attempt < SESSION_TITLE_MAX_APPEND_ATTEMPTS; attempt += 1) {
898
+ const snapshot = await readStableFile(filepath);
899
+ if (!snapshot) continue;
900
+ renameOnlyStart ??= snapshot.before;
901
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
902
+ const predecessor = wrapperPredecessor(snapshot.content);
903
+ const pair = createUserSessionTitleEntries({
904
+ title,
905
+ parentId: predecessor.parentId,
906
+ timestamp,
907
+ authorityId: randomUUID(),
908
+ titleId: randomUUID()
909
+ });
910
+ const append = {
911
+ authority: { id: pair.authority.id, parentId: pair.authority.parentId },
912
+ title: { id: pair.title.id, parentId: pair.title.parentId }
913
+ };
914
+ const records = [pair.authority, pair.title].map((entry) => JSON.stringify(entry)).join("\n");
915
+ const payload = `${predecessor.separator}${records}
916
+ `;
917
+ await appendFile(filepath, payload);
918
+ const verified = await verifyTitleSequence(filepath, append, title);
919
+ if (!verified) continue;
920
+ const exactSize = Number(snapshot.after.size) + Buffer.byteLength(payload);
921
+ if (attempt === 0 && Number(verified.stat.size) === exactSize && renameOnlyStart) {
922
+ await restoreVerifiedRenameMtime(filepath, renameOnlyStart, exactSize);
923
+ }
924
+ return;
925
+ }
926
+ throw sessionTitleLockedError();
927
+ }
928
+ function fileTimeSeconds(milliseconds) {
929
+ const seconds = milliseconds / 1e3;
930
+ return Number.isFinite(seconds) ? seconds : void 0;
931
+ }
932
+
933
+ // src/server/harness/pi-coding-agent/sessionArchiveIndex.ts
934
+ import { randomUUID as randomUUID2 } from "node:crypto";
935
+ import { mkdir, readFile as readFile2, readdir, rename, rm, writeFile } from "node:fs/promises";
936
+ import { join } from "node:path";
937
+ var SESSION_ARCHIVE_DIRECTORY_NAME = ".archive";
938
+ var ARCHIVE_MARKER_VERSION = 1;
939
+ var ARCHIVE_MARKER_SUFFIX = ".json";
940
+ function archiveDirectory(sessionDir) {
941
+ return join(sessionDir, SESSION_ARCHIVE_DIRECTORY_NAME);
942
+ }
943
+ function markerFilename(sessionId) {
944
+ return `${Buffer.from(sessionId, "utf8").toString("base64url")}${ARCHIVE_MARKER_SUFFIX}`;
945
+ }
946
+ function markerPath(sessionDir, sessionId) {
947
+ return join(archiveDirectory(sessionDir), markerFilename(sessionId));
948
+ }
949
+ function parseMarker(raw) {
950
+ try {
951
+ const parsed = JSON.parse(raw);
952
+ return parsed.version === ARCHIVE_MARKER_VERSION && typeof parsed.sessionId === "string" && parsed.sessionId.length > 0 && typeof parsed.archivedAt === "string" ? parsed : null;
953
+ } catch {
954
+ return null;
955
+ }
956
+ }
957
+ async function readArchivedSessionIds(sessionDir) {
958
+ const directory = archiveDirectory(sessionDir);
959
+ const files = await readdir(directory).catch(() => []);
960
+ const ids = /* @__PURE__ */ new Set();
961
+ await Promise.all(files.filter((file) => file.endsWith(ARCHIVE_MARKER_SUFFIX)).map(async (file) => {
962
+ const marker = parseMarker(await readFile2(join(directory, file), "utf8").catch(() => ""));
963
+ if (marker && file === markerFilename(marker.sessionId)) ids.add(marker.sessionId);
964
+ }));
965
+ return ids;
966
+ }
967
+ async function writeSessionArchived(sessionDir, sessionId, archived, now = /* @__PURE__ */ new Date()) {
968
+ const target = markerPath(sessionDir, sessionId);
969
+ if (!archived) {
970
+ try {
971
+ await rm(target);
972
+ return true;
973
+ } catch (error) {
974
+ if (error.code === "ENOENT") return false;
975
+ throw error;
976
+ }
977
+ }
978
+ await mkdir(archiveDirectory(sessionDir), { recursive: true });
979
+ const marker = {
980
+ version: ARCHIVE_MARKER_VERSION,
981
+ sessionId,
982
+ archivedAt: now.toISOString()
983
+ };
984
+ const tmp = `${target}.${randomUUID2()}.tmp`;
985
+ try {
986
+ await writeFile(tmp, `${JSON.stringify(marker)}
987
+ `, { encoding: "utf8", flag: "wx" });
988
+ await rename(tmp, target);
989
+ return true;
990
+ } catch (error) {
991
+ await rm(tmp, { force: true }).catch(() => {
992
+ });
993
+ throw error;
565
994
  }
566
- return { title, firstUserTitle, turnCount, hasAssistantReply: hasAssistantReply2, latestMessageAtMs };
995
+ }
996
+ async function forgetArchivedSession(sessionDir, sessionId) {
997
+ await rm(markerPath(sessionDir, sessionId), { force: true });
567
998
  }
568
999
 
569
1000
  // src/server/harness/pi-coding-agent/sessions.ts
@@ -593,31 +1024,37 @@ function sessionBaseDir(explicitRoot) {
593
1024
  const explicit = explicitRoot?.trim();
594
1025
  if (explicit) return resolve(explicit);
595
1026
  const configured = getEnv(SESSION_ROOT_ENV)?.trim();
596
- return configured ? resolve(configured) : join(homedir(), ".pi", "agent", "sessions");
1027
+ return configured ? resolve(configured) : join2(homedir(), ".pi", "agent", "sessions");
597
1028
  }
598
1029
  function defaultSessionDir(cwd, explicitRoot) {
599
1030
  if (explicitRoot && cwd.trim().length === 0) return sessionBaseDir(explicitRoot);
600
1031
  const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
601
- return join(sessionBaseDir(explicitRoot), safePath);
1032
+ return join2(sessionBaseDir(explicitRoot), safePath);
602
1033
  }
603
1034
  var SAFE_SESSION_NAMESPACE = /^[a-zA-Z0-9_-]+$/;
604
1035
  var SESSION_ROOT_ENV = "BORING_AGENT_SESSION_ROOT";
605
1036
  var SUMMARY_PREFIX_BYTES2 = 64 * 1024;
1037
+ var MAX_CONCURRENT_SESSION_SUMMARIES = 4;
606
1038
  var DEFAULT_LEGACY_WORKSPACE_ID = "default";
607
1039
  var TRUSTED_LOCAL_USER_ID = "local";
1040
+ function withArchivedFlag(summary, archivedIds) {
1041
+ return archivedIds.has(summary.id) ? { ...summary, archived: true } : summary;
1042
+ }
608
1043
  function sessionDirForNamespace(namespace, explicitRoot) {
609
1044
  const safeNamespace = namespace.trim();
610
1045
  if (!SAFE_SESSION_NAMESPACE.test(safeNamespace)) {
611
1046
  throw new Error("session namespace must contain only letters, numbers, underscores, and dashes");
612
1047
  }
613
- return join(sessionBaseDir(explicitRoot), safeNamespace);
1048
+ return join2(sessionBaseDir(explicitRoot), safeNamespace);
614
1049
  }
615
1050
  function normalizeListOptions(options) {
616
1051
  return {
617
1052
  limit: options?.limit === void 0 ? void 0 : Math.max(0, options.limit),
618
1053
  offset: Math.max(0, options?.offset ?? 0),
619
1054
  includeId: options?.includeId,
620
- includeEmpty: options?.includeEmpty === true
1055
+ includeEmpty: options?.includeEmpty === true,
1056
+ archived: options?.archived ?? "all",
1057
+ after: options?.after
621
1058
  };
622
1059
  }
623
1060
  var PiSessionStore = class {
@@ -663,7 +1100,9 @@ var PiSessionStore = class {
663
1100
  normalizedOptions.limit ?? null,
664
1101
  normalizedOptions.offset,
665
1102
  normalizedOptions.includeId ?? null,
666
- normalizedOptions.includeEmpty
1103
+ normalizedOptions.includeEmpty,
1104
+ normalizedOptions.archived,
1105
+ normalizedOptions.after ?? null
667
1106
  ]);
668
1107
  const inFlight = this.listInFlight.get(inFlightKey);
669
1108
  if (inFlight) return inFlight;
@@ -676,9 +1115,9 @@ var PiSessionStore = class {
676
1115
  }
677
1116
  }
678
1117
  async listUncached(ctx, options) {
679
- const files = await readdir(this.sessionDir).catch(() => []);
1118
+ const files = await readdir2(this.sessionDir).catch(() => []);
680
1119
  const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
681
- const filepaths = jsonlFiles.map((f) => join(this.sessionDir, f));
1120
+ const filepaths = jsonlFiles.map((f) => join2(this.sessionDir, f));
682
1121
  const fileStats = await Promise.all(filepaths.map(async (filepath) => {
683
1122
  try {
684
1123
  return { filepath, stat: await fsStat2(filepath) };
@@ -689,23 +1128,35 @@ var PiSessionStore = class {
689
1128
  const existingFiles = fileStats.filter((item) => item !== null);
690
1129
  const referencedPiFiles = await this.referencedPiFiles(existingFiles);
691
1130
  const visibleFiles = await Promise.all(existingFiles.filter(({ filepath }) => !referencedPiFiles.has(resolve(filepath))).map(async (file) => ({ ...file, ...await this.sessionSortKey(file) })));
692
- visibleFiles.sort((a, b) => b.sortMtimeMs - a.sortMtimeMs || a.sortId.localeCompare(b.sortId));
1131
+ visibleFiles.sort((a, b) => compareSessionOrder(
1132
+ [a.orderingUpdatedAtMs, "", a.sortId],
1133
+ [b.orderingUpdatedAtMs, "", b.sortId]
1134
+ ));
693
1135
  const { offset, limit } = options;
694
1136
  const includeId = options.includeId;
695
- const pageSummaries = await this.summarizeVisiblePage(visibleFiles, {
1137
+ const archivedIds = await readArchivedSessionIds(this.sessionDir);
1138
+ const after = options.after;
1139
+ const afterFiles = after ? visibleFiles.filter((file) => compareSessionOrder(
1140
+ [file.orderingUpdatedAtMs, after.agentTypeId, file.sortId],
1141
+ after.position
1142
+ ) > 0) : visibleFiles;
1143
+ const filteredFiles = options.archived === "all" ? afterFiles : afterFiles.filter((file) => archivedIds.has(file.sortId) === (options.archived === "archived"));
1144
+ const pageSummaries = await this.summarizeVisiblePage(filteredFiles, {
696
1145
  ctx,
697
1146
  offset,
698
1147
  limit,
699
1148
  includeId,
700
- includeEmpty: options.includeEmpty
1149
+ includeEmpty: options.includeEmpty,
1150
+ archivedIds
701
1151
  });
702
1152
  if (!includeId || pageSummaries.some((summary) => summary.id === includeId)) return pageSummaries;
703
1153
  const includeSummary = await this.summarizeIncludedSession(ctx, includeId, referencedPiFiles);
704
- return includeSummary ? [...pageSummaries, includeSummary] : pageSummaries;
1154
+ return includeSummary ? [...pageSummaries, withArchivedFlag(includeSummary, archivedIds)] : pageSummaries;
705
1155
  }
706
1156
  async create(ctx, init) {
707
- await mkdir(this.sessionDir, { recursive: true });
708
- const id = randomUUID();
1157
+ const manualTitle = init?.title !== void 0 ? normalizeUserSessionTitle(init.title) : void 0;
1158
+ await mkdir2(this.sessionDir, { recursive: true });
1159
+ const id = randomUUID3();
709
1160
  const now = (/* @__PURE__ */ new Date()).toISOString();
710
1161
  const header = {
711
1162
  type: "session",
@@ -716,21 +1167,21 @@ var PiSessionStore = class {
716
1167
  boringSessionCtx: normalizeSessionCtx(ctx) ?? {}
717
1168
  };
718
1169
  const lines = [JSON.stringify(header)];
719
- if (init?.title) {
720
- const infoEntry = {
721
- type: "session_info",
722
- id: randomUUID(),
1170
+ if (manualTitle) {
1171
+ const pair = createUserSessionTitleEntries({
1172
+ title: manualTitle,
723
1173
  parentId: null,
724
1174
  timestamp: now,
725
- name: init.title
726
- };
727
- lines.push(JSON.stringify(infoEntry));
1175
+ authorityId: randomUUID3(),
1176
+ titleId: randomUUID3()
1177
+ });
1178
+ lines.push(JSON.stringify(pair.authority), JSON.stringify(pair.title));
728
1179
  }
729
- const filepath = join(this.sessionDir, `${nativeSessionFilename(id, now)}`);
730
- await writeFile(filepath, lines.join("\n") + "\n", "utf-8");
1180
+ const filepath = join2(this.sessionDir, `${nativeSessionFilename(id, now)}`);
1181
+ await writeFile2(filepath, lines.join("\n") + "\n", "utf-8");
731
1182
  return {
732
1183
  id,
733
- title: init?.title ?? "New session",
1184
+ title: manualTitle ?? "New session",
734
1185
  createdAt: now,
735
1186
  updatedAt: now,
736
1187
  turnCount: 0,
@@ -738,23 +1189,39 @@ var PiSessionStore = class {
738
1189
  hasAssistantReply: false
739
1190
  };
740
1191
  }
1192
+ /**
1193
+ * Visibility only. Resolve tenancy and build the receipt through the bounded
1194
+ * listing projection; never call `load`, whose legacy compatibility path may
1195
+ * compact ui_snapshot entries and therefore mutate transcript bytes/mtime.
1196
+ */
1197
+ async setArchived(ctx, sessionId, archived) {
1198
+ const filepath = await this.resolveSessionFile(sessionId, ctx);
1199
+ const summary = await this.summarizeFile(ctx, filepath);
1200
+ if (!summary) throw new Error(`Session not found: ${sessionId}`);
1201
+ await writeSessionArchived(this.sessionDir, summary.id, archived);
1202
+ try {
1203
+ await this.resolveSessionFile(summary.id, ctx);
1204
+ } catch (error) {
1205
+ await forgetArchivedSession(this.sessionDir, summary.id);
1206
+ throw error;
1207
+ }
1208
+ return archived ? { ...summary, archived: true } : summary;
1209
+ }
741
1210
  async load(ctx, sessionId) {
742
1211
  const resolved = await this.resolveSessionTranscript(ctx, sessionId);
743
- const nativeSummary = resolved.directNative ? await summarizeNativeTranscript(resolved.filepath) : null;
744
- const title = newestDurableTitle(resolved.sessionEntries, resolved.linkedEntries) ?? nativeSummary?.title ?? nativeSummary?.firstUserTitle ?? "New session";
745
- const turnCount = countUserTurns(resolved.transcriptEntries);
746
- const updatedAtMs = nativeSummary?.latestMessageAtMs ?? Math.max(resolved.fileStat.mtime.getTime(), resolved.linkedMtimeMs ?? 0);
747
- return {
1212
+ const title = projectedTranscriptTitle(resolved.sessionSummary, resolved.linkedSummary) ?? "New session";
1213
+ const updatedAtMs = resolved.transcriptSummary.latestMessageAtMs ?? Math.max(resolved.fileStat.mtime.getTime(), resolved.linkedMtimeMs ?? 0);
1214
+ return withArchivedFlag({
748
1215
  id: resolved.resolvedSessionId,
749
1216
  title,
750
1217
  createdAt: resolved.header?.timestamp ?? resolved.fileStat.birthtime.toISOString(),
751
1218
  updatedAt: new Date(updatedAtMs).toISOString(),
752
- turnCount,
1219
+ turnCount: resolved.transcriptSummary.turnCount,
753
1220
  ...resolved.directNative ? {
754
1221
  nativeSessionId: resolved.resolvedSessionId,
755
- hasAssistantReply: hasAssistantReply(resolved.transcriptEntries)
1222
+ hasAssistantReply: resolved.transcriptSummary.hasAssistantReply
756
1223
  } : {}
757
- };
1224
+ }, await readArchivedSessionIds(this.sessionDir));
758
1225
  }
759
1226
  /**
760
1227
  * Returns the persisted pi message objects in file order so callers can run
@@ -787,11 +1254,12 @@ var PiSessionStore = class {
787
1254
  const filepath = await this.resolveSessionFile(sessionId, ctx);
788
1255
  let content;
789
1256
  try {
790
- content = await readFile(filepath, "utf-8");
1257
+ content = await readFile3(filepath, "utf-8");
791
1258
  } catch {
792
1259
  throw new Error(`Session not found: ${sessionId}`);
793
1260
  }
794
- const fileEntries = safeParseEntries(content);
1261
+ const parsedFile = parseSessionTranscript(content);
1262
+ const fileEntries = parsedFile.entries;
795
1263
  const header = fileEntries.find(
796
1264
  (e) => e.type === "session"
797
1265
  );
@@ -799,17 +1267,6 @@ var PiSessionStore = class {
799
1267
  filepath,
800
1268
  header?.id ?? sessionId
801
1269
  );
802
- if (!timestampNamedNative && fileEntries.some((e) => e.type === "ui_snapshot")) {
803
- const compacted = fileEntries.filter((e) => e.type !== "ui_snapshot").map((e) => JSON.stringify(e)).join("\n") + "\n";
804
- const tmp = `${filepath}.compact-${randomUUID()}`;
805
- try {
806
- await writeFile(tmp, compacted, "utf-8");
807
- await rename(tmp, filepath);
808
- } catch {
809
- await rm(tmp, { force: true }).catch(() => {
810
- });
811
- }
812
- }
813
1270
  if (!this.headerBelongsToCtx(header, ctx, timestampNamedNative)) {
814
1271
  throw new Error(`Session not found: ${sessionId}`);
815
1272
  }
@@ -823,17 +1280,18 @@ var PiSessionStore = class {
823
1280
  const linkedEntries = linked?.entries.filter(
824
1281
  (e) => e.type !== "session"
825
1282
  ) ?? [];
826
- const transcriptEntries = linkedEntries.length > 0 ? linkedEntries : sessionEntries;
1283
+ const transcriptSummary = selectTranscriptSummary(parsedFile.summary, linked?.summary);
1284
+ const transcriptEntries = transcriptSummary === linked?.summary ? linkedEntries : sessionEntries;
827
1285
  return {
828
1286
  resolvedSessionId: header?.id ?? sessionId,
829
1287
  header,
830
- sessionEntries,
831
- linkedEntries,
832
1288
  transcriptEntries,
1289
+ sessionSummary: parsedFile.summary,
1290
+ ...linked ? { linkedSummary: linked.summary } : {},
1291
+ transcriptSummary,
833
1292
  fileStat,
834
- linkedMtimeMs: linked?.mtime.getTime(),
1293
+ linkedMtimeMs: linked?.mtimeMs,
835
1294
  filepath,
836
- ...linkedPiFile && linked ? { linkedFilepath: linkedPiFile } : {},
837
1295
  directNative
838
1296
  };
839
1297
  }
@@ -842,8 +1300,7 @@ var PiSessionStore = class {
842
1300
  * appended through the same JSONL path under one process-local writer lock.
843
1301
  */
844
1302
  async rename(ctx, sessionId, title) {
845
- const trimmed = title.replace(/[\r\n]+/g, " ").trim();
846
- if (!trimmed) throw new Error("Session title must not be empty");
1303
+ const trimmed = normalizeUserSessionTitle(title);
847
1304
  return await this.withWriter(sessionId, async () => {
848
1305
  const resolved = await this.resolveSessionTranscript(ctx, sessionId);
849
1306
  if (resolved.directNative) {
@@ -859,20 +1316,8 @@ var PiSessionStore = class {
859
1316
  }
860
1317
  return await this.load(ctx, sessionId);
861
1318
  }
862
- const entry = {
863
- type: "session_info",
864
- id: randomUUID(),
865
- parentId: null,
866
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
867
- name: trimmed
868
- };
869
- const line = `${JSON.stringify(entry)}
870
- `;
871
- const targets = [resolved.filepath, resolved.linkedFilepath].filter((path) => Boolean(path));
872
- for (const filepath of targets) {
873
- await appendFile(filepath, line);
874
- this.prefixCache.delete(filepath);
875
- }
1319
+ await appendVerifiedWrapperRename(resolved.filepath, trimmed);
1320
+ this.prefixCache.delete(resolved.filepath);
876
1321
  return await this.load(ctx, sessionId);
877
1322
  });
878
1323
  }
@@ -898,7 +1343,7 @@ var PiSessionStore = class {
898
1343
  loadPiSessionFileSync(ctx, sessionId) {
899
1344
  if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) return null;
900
1345
  try {
901
- const direct = join(this.sessionDir, `${sessionId}.jsonl`);
1346
+ const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
902
1347
  let filepath = direct;
903
1348
  let content;
904
1349
  try {
@@ -908,7 +1353,7 @@ var PiSessionStore = class {
908
1353
  (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
909
1354
  );
910
1355
  if (files.length === 0) return null;
911
- filepath = join(this.sessionDir, files[0]);
1356
+ filepath = join2(this.sessionDir, files[0]);
912
1357
  content = readFileSync(filepath, "utf-8");
913
1358
  }
914
1359
  const entries = safeParseEntries(content);
@@ -938,19 +1383,19 @@ var PiSessionStore = class {
938
1383
  async loadPiSessionFile(ctx, sessionId) {
939
1384
  if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) return null;
940
1385
  try {
941
- const direct = join(this.sessionDir, `${sessionId}.jsonl`);
1386
+ const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
942
1387
  let filepath = direct;
943
1388
  let content;
944
1389
  try {
945
- content = await readFile(direct, "utf-8");
1390
+ content = await readFile3(direct, "utf-8");
946
1391
  } catch {
947
- const files = await readdir(this.sessionDir).catch(() => []);
1392
+ const files = await readdir2(this.sessionDir).catch(() => []);
948
1393
  const match = files.find(
949
1394
  (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
950
1395
  );
951
1396
  if (!match) return null;
952
- filepath = join(this.sessionDir, match);
953
- content = await readFile(filepath, "utf-8");
1397
+ filepath = join2(this.sessionDir, match);
1398
+ content = await readFile3(filepath, "utf-8");
954
1399
  }
955
1400
  const entries = safeParseEntries(content);
956
1401
  const header = entries.find((entry) => entry.type === "session");
@@ -984,7 +1429,7 @@ var PiSessionStore = class {
984
1429
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
985
1430
  path: piFilePath
986
1431
  });
987
- await appendFile(filepath, entry + "\n");
1432
+ await appendFile2(filepath, entry + "\n");
988
1433
  }
989
1434
  async delete(ctx, sessionId) {
990
1435
  const filepath = await this.resolveSessionFile(sessionId, ctx).catch(
@@ -994,30 +1439,31 @@ var PiSessionStore = class {
994
1439
  const fileSessionId = await this.readSessionFileId(filepath);
995
1440
  if (fileSessionId && fileSessionId !== sessionId) return;
996
1441
  const linkedPiFile = await this.linkedPiFileFor(filepath);
997
- await rm(filepath, { force: true });
1442
+ await rm2(filepath, { force: true });
998
1443
  this.prefixCache.delete(filepath);
999
1444
  if (linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)) {
1000
- await rm(linkedPiFile, { force: true });
1445
+ await rm2(linkedPiFile, { force: true });
1001
1446
  this.prefixCache.delete(linkedPiFile);
1002
1447
  }
1448
+ await forgetArchivedSession(this.sessionDir, fileSessionId ?? sessionId);
1003
1449
  }
1004
1450
  async resolveSessionFile(sessionId, ctx) {
1005
1451
  if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) {
1006
1452
  throw new Error(`Session not found: ${sessionId}`);
1007
1453
  }
1008
- const direct = join(this.sessionDir, `${sessionId}.jsonl`);
1454
+ const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
1009
1455
  try {
1010
1456
  await fsStat2(direct);
1011
1457
  if (ctx) await this.assertFileBelongsToCtx(direct, ctx, sessionId);
1012
1458
  return direct;
1013
1459
  } catch {
1014
1460
  }
1015
- const files = await readdir(this.sessionDir).catch(() => []);
1461
+ const files = await readdir2(this.sessionDir).catch(() => []);
1016
1462
  const match = files.find(
1017
1463
  (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
1018
1464
  );
1019
1465
  if (!match) throw new Error(`Session not found: ${sessionId}`);
1020
- const matchedPath = join(this.sessionDir, match);
1466
+ const matchedPath = join2(this.sessionDir, match);
1021
1467
  if (!isTimestampNamedPiSessionFile(matchedPath, sessionId)) {
1022
1468
  if (ctx) await this.assertFileBelongsToCtx(matchedPath, ctx, sessionId);
1023
1469
  return matchedPath;
@@ -1047,7 +1493,7 @@ var PiSessionStore = class {
1047
1493
  }
1048
1494
  async linkedPiFileFor(filepath) {
1049
1495
  try {
1050
- const content = await readFile(filepath, "utf-8");
1496
+ const content = await readFile3(filepath, "utf-8");
1051
1497
  return extractPiSessionFilePath(safeParseEntries(content));
1052
1498
  } catch {
1053
1499
  return null;
@@ -1067,35 +1513,51 @@ var PiSessionStore = class {
1067
1513
  return referenced;
1068
1514
  }
1069
1515
  async sessionSortKey({ filepath, stat }) {
1070
- let sortMtimeMs = stat.mtime.getTime();
1071
- let headerId = void 0;
1516
+ let fileMtimeMs = Number(stat.mtimeMs);
1517
+ let headerId;
1072
1518
  const stemFallback = () => ({
1073
- sortMtimeMs,
1519
+ orderingUpdatedAtMs: fileMtimeMs,
1074
1520
  sortId: wrapperSessionId(filepath)
1075
1521
  });
1076
1522
  try {
1077
1523
  const cached = await this.readPrefixCache(filepath, stat);
1524
+ fileMtimeMs = cached.mtimeMs;
1078
1525
  headerId = cached.headerId ?? void 0;
1526
+ if (!headerId) return stemFallback();
1079
1527
  const linkedPiFile = cached.referencedPiFile;
1080
- if (linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)) {
1081
- const linkedStat = await fsStat2(linkedPiFile);
1082
- sortMtimeMs = Math.max(sortMtimeMs, linkedStat.mtime.getTime());
1083
- }
1084
- if (!headerId) {
1085
- return stemFallback();
1086
- }
1087
- if (!isTimestampNamedPiSessionFile(filepath, headerId)) {
1088
- return { sortMtimeMs, sortId: headerId };
1528
+ const hasLinkedTranscript = Boolean(
1529
+ linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)
1530
+ );
1531
+ const linkedStat = hasLinkedTranscript ? await fsStat2(linkedPiFile).catch(() => void 0) : void 0;
1532
+ const linkedSnapshotMatches = linkedStat ? cached.linkedDev === Number(linkedStat.dev) && cached.linkedIno === Number(linkedStat.ino) && cached.linkedMtimeMs === Number(linkedStat.mtimeMs) && cached.linkedCtimeMs === Number(linkedStat.ctimeMs) && cached.linkedSize === Number(linkedStat.size) : cached.linkedMtimeMs === void 0 && cached.linkedSize === void 0;
1533
+ if (cached.orderingUpdatedAtMs !== void 0 && (!hasLinkedTranscript || linkedSnapshotMatches)) {
1534
+ return { orderingUpdatedAtMs: cached.orderingUpdatedAtMs, sortId: headerId };
1089
1535
  }
1090
- if (cached.nativeSortMtimeMs !== void 0) {
1091
- return { sortMtimeMs: cached.nativeSortMtimeMs, sortId: headerId };
1536
+ let selectedOrdering;
1537
+ if (linkedPiFile && linkedStat) {
1538
+ const linkedOrdering = await nativeTranscriptOrdering(
1539
+ linkedPiFile,
1540
+ Number(linkedStat.size)
1541
+ ).catch(() => void 0);
1542
+ if (linkedOrdering?.hasEntries) selectedOrdering = linkedOrdering;
1092
1543
  }
1093
- const latest = await latestNativeMessageTimestamp(filepath, Number(stat.size));
1094
- if (latest === void 0) return { sortMtimeMs, sortId: headerId };
1095
- this.prefixCache.set(filepath, { ...cached, nativeSortMtimeMs: latest });
1096
- return { sortMtimeMs: latest, sortId: headerId };
1544
+ selectedOrdering ??= await nativeTranscriptOrdering(filepath, cached.size).catch(() => ({ hasEntries: false }));
1545
+ const orderingUpdatedAtMs = selectedOrdering.latestMessageAtMs ?? Math.max(fileMtimeMs, linkedStat ? Number(linkedStat.mtimeMs) : 0);
1546
+ const { summary: _staleSummary, ...cacheWithoutSummary } = cached;
1547
+ this.prefixCache.set(filepath, {
1548
+ ...cacheWithoutSummary,
1549
+ orderingUpdatedAtMs,
1550
+ ...linkedStat ? {
1551
+ linkedDev: Number(linkedStat.dev),
1552
+ linkedIno: Number(linkedStat.ino),
1553
+ linkedMtimeMs: Number(linkedStat.mtimeMs),
1554
+ linkedCtimeMs: Number(linkedStat.ctimeMs),
1555
+ linkedSize: Number(linkedStat.size)
1556
+ } : {}
1557
+ });
1558
+ return { orderingUpdatedAtMs, sortId: headerId };
1097
1559
  } catch {
1098
- return headerId ? { sortMtimeMs, sortId: headerId } : stemFallback();
1560
+ return headerId ? { orderingUpdatedAtMs: fileMtimeMs, sortId: headerId } : stemFallback();
1099
1561
  }
1100
1562
  }
1101
1563
  async summarizeFile(ctx, filepath, existingStat) {
@@ -1116,43 +1578,51 @@ var PiSessionStore = class {
1116
1578
  const directNative = isTimestampNamedPiSessionFile(filepath, header.id);
1117
1579
  if (directNative ? !this.nativeFileBelongsToCtx(header, ctx) : !this.storedCtxBelongsToCtx(sessionCtx, ctx)) return null;
1118
1580
  const entries = parseJsonlPrefixEntries2(content);
1119
- const sessionEntries = entries.filter(
1120
- (e) => e.type !== "session"
1121
- );
1122
1581
  const linkedPiFile = extractPiSessionFilePath(entries);
1123
- const linked = linkedPiFile && resolve(linkedPiFile) !== resolve(filepath) ? await this.readLinkedPiSessionSummary(linkedPiFile) : null;
1124
- const linkedEntries = linked?.entries.filter(
1125
- (e) => e.type !== "session"
1126
- ) ?? [];
1127
- const nativeSummary = directNative ? await summarizeNativeTranscript(filepath) : null;
1128
- const title = newestDurableTitle(sessionEntries, linkedEntries) ?? nativeSummary?.title ?? nativeSummary?.firstUserTitle ?? firstUserMessage(linkedEntries) ?? firstUserMessage(sessionEntries) ?? "New session";
1129
- const turnCount = nativeSummary?.turnCount ?? [...sessionEntries, ...linkedEntries].filter(
1130
- (e) => e.type === "message" && e.message?.role === "user"
1131
- ).length;
1132
- const updatedAtMs = nativeSummary?.latestMessageAtMs ?? Math.max(fileStat.mtime.getTime(), linked?.mtime.getTime() ?? 0);
1582
+ const [sessionScan, linked, order] = await Promise.all([
1583
+ this.readTranscriptSummary(filepath),
1584
+ linkedPiFile && resolve(linkedPiFile) !== resolve(filepath) ? this.readLinkedPiSessionSummary(linkedPiFile) : null,
1585
+ this.sessionSortKey({ filepath, stat: fileStat })
1586
+ ]);
1587
+ if (!sessionScan) return null;
1588
+ const sessionTranscript = sessionScan.summary;
1589
+ const transcript = selectTranscriptSummary(sessionTranscript, linked?.summary);
1590
+ const title = projectedTranscriptTitle(sessionTranscript, linked?.summary) ?? "New session";
1133
1591
  const summary = {
1134
1592
  id: header.id,
1135
1593
  title,
1136
1594
  createdAt: header.timestamp,
1137
- updatedAt: new Date(updatedAtMs).toISOString(),
1138
- turnCount,
1139
- ...nativeSummary ? {
1595
+ updatedAt: new Date(order.orderingUpdatedAtMs).toISOString(),
1596
+ turnCount: transcript.turnCount,
1597
+ ...directNative ? {
1140
1598
  nativeSessionId: header.id,
1141
- hasAssistantReply: nativeSummary.hasAssistantReply
1599
+ hasAssistantReply: transcript.hasAssistantReply
1142
1600
  } : {}
1143
1601
  };
1144
- this.prefixCache.set(filepath, {
1145
- mtimeMs: fileStat.mtime.getTime(),
1146
- size: Number(fileStat.size),
1147
- referencedPiFile: linkedPiFile,
1148
- // Carried so a later `sessionSortMtimeMs` on this same entry still
1149
- // recognises a native transcript instead of falling back to mtime.
1150
- headerId: header.id,
1151
- ...cached?.nativeSortMtimeMs !== void 0 ? { nativeSortMtimeMs: cached.nativeSortMtimeMs } : {},
1152
- sessionCtx,
1153
- ...linked ? { linkedMtimeMs: linked.mtime.getTime(), linkedSize: linked.size } : {},
1154
- ...!nativeSummary ? { summary } : {}
1155
- });
1602
+ const stableForCache = sessionScan.stable && sameFileSnapshot(fileStat, sessionScan.stat) && (!linked || linked.stable);
1603
+ if (stableForCache) {
1604
+ this.prefixCache.set(filepath, {
1605
+ dev: Number(sessionScan.stat.dev),
1606
+ ino: Number(sessionScan.stat.ino),
1607
+ mtimeMs: Number(sessionScan.stat.mtimeMs),
1608
+ ctimeMs: Number(sessionScan.stat.ctimeMs),
1609
+ size: Number(sessionScan.stat.size),
1610
+ referencedPiFile: linkedPiFile,
1611
+ // Preserve the exact ordering timestamp emitted above while
1612
+ // retaining snapshot identity required by title-authority caching.
1613
+ headerId: header.id,
1614
+ orderingUpdatedAtMs: order.orderingUpdatedAtMs,
1615
+ sessionCtx,
1616
+ ...linked ? {
1617
+ linkedDev: Number(linked.dev),
1618
+ linkedIno: Number(linked.ino),
1619
+ linkedMtimeMs: linked.mtimeMs,
1620
+ linkedCtimeMs: linked.ctimeMs,
1621
+ linkedSize: linked.size
1622
+ } : {},
1623
+ summary
1624
+ });
1625
+ }
1156
1626
  return summary;
1157
1627
  } catch {
1158
1628
  return null;
@@ -1161,7 +1631,7 @@ var PiSessionStore = class {
1161
1631
  cachedPrefix(filepath, fileStat) {
1162
1632
  const cached = this.prefixCache.get(filepath);
1163
1633
  if (!cached) return void 0;
1164
- if (cached.mtimeMs !== fileStat.mtime.getTime() || cached.size !== Number(fileStat.size)) return void 0;
1634
+ if (cached.dev !== Number(fileStat.dev) || cached.ino !== Number(fileStat.ino) || cached.mtimeMs !== Number(fileStat.mtimeMs) || cached.ctimeMs !== Number(fileStat.ctimeMs) || cached.size !== Number(fileStat.size)) return void 0;
1165
1635
  return cached;
1166
1636
  }
1167
1637
  async cachedSummaryIsFresh(filepath, cached) {
@@ -1169,7 +1639,7 @@ var PiSessionStore = class {
1169
1639
  if (!linkedPiFile || resolve(linkedPiFile) === resolve(filepath)) return true;
1170
1640
  try {
1171
1641
  const linkedStat = await fsStat2(linkedPiFile);
1172
- return cached.linkedMtimeMs === linkedStat.mtime.getTime() && cached.linkedSize === Number(linkedStat.size);
1642
+ return cached.linkedDev === Number(linkedStat.dev) && cached.linkedIno === Number(linkedStat.ino) && cached.linkedMtimeMs === Number(linkedStat.mtimeMs) && cached.linkedCtimeMs === Number(linkedStat.ctimeMs) && cached.linkedSize === Number(linkedStat.size);
1173
1643
  } catch {
1174
1644
  return cached.linkedMtimeMs === void 0 && cached.linkedSize === void 0;
1175
1645
  }
@@ -1177,17 +1647,29 @@ var PiSessionStore = class {
1177
1647
  async readPrefixCache(filepath, fileStat) {
1178
1648
  const cached = this.cachedPrefix(filepath, fileStat);
1179
1649
  if (cached) return cached;
1180
- const content = await readJsonlPrefix2(filepath);
1181
- const entries = parseJsonlPrefixEntries2(content);
1182
- const header = entries.find((item) => item.type === "session");
1183
- const entry = {
1184
- mtimeMs: fileStat.mtime.getTime(),
1185
- size: Number(fileStat.size),
1186
- referencedPiFile: extractPiSessionFilePath(entries),
1187
- headerId: header?.id ?? null,
1188
- sessionCtx: readHeaderSessionCtx(header)
1189
- };
1190
- this.prefixCache.set(filepath, entry);
1650
+ let before = fileStat;
1651
+ let entry;
1652
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1653
+ const content = await readJsonlPrefix2(filepath);
1654
+ const after = await fsStat2(filepath);
1655
+ const entries = parseJsonlPrefixEntries2(content);
1656
+ const header = entries.find((item) => item.type === "session");
1657
+ entry = {
1658
+ dev: Number(after.dev),
1659
+ ino: Number(after.ino),
1660
+ mtimeMs: Number(after.mtimeMs),
1661
+ ctimeMs: Number(after.ctimeMs),
1662
+ size: Number(after.size),
1663
+ referencedPiFile: extractPiSessionFilePath(entries),
1664
+ headerId: header?.id ?? null,
1665
+ sessionCtx: readHeaderSessionCtx(header)
1666
+ };
1667
+ if (sameFileSnapshot(before, after)) {
1668
+ this.prefixCache.set(filepath, entry);
1669
+ return entry;
1670
+ }
1671
+ before = after;
1672
+ }
1191
1673
  return entry;
1192
1674
  }
1193
1675
  async summarizeVisiblePage(visibleFiles, options) {
@@ -1195,15 +1677,19 @@ var PiSessionStore = class {
1195
1677
  const page = [];
1196
1678
  let validSeen = 0;
1197
1679
  let index = 0;
1198
- const batchSize = options.limit === void 0 ? Math.max(1, visibleFiles.length) : Math.max(1, options.limit);
1680
+ const batchSize = Math.min(
1681
+ MAX_CONCURRENT_SESSION_SUMMARIES,
1682
+ options.limit === void 0 ? Math.max(1, visibleFiles.length) : Math.max(1, options.limit)
1683
+ );
1199
1684
  while (index < visibleFiles.length && (options.limit === void 0 || page.length < options.limit)) {
1200
1685
  const batch = visibleFiles.slice(index, index + batchSize);
1201
1686
  index += batch.length;
1202
1687
  const summaries = await Promise.all(
1203
1688
  batch.map(({ filepath, stat }) => this.summarizeFile(options.ctx, filepath, stat))
1204
1689
  );
1205
- for (const summary of summaries) {
1206
- if (!summary) continue;
1690
+ for (const rawSummary of summaries) {
1691
+ if (!rawSummary) continue;
1692
+ const summary = withArchivedFlag(rawSummary, options.archivedIds);
1207
1693
  if (summary.turnCount === 0 && !options.includeEmpty && summary.id !== options.includeId) {
1208
1694
  continue;
1209
1695
  }
@@ -1232,7 +1718,7 @@ var PiSessionStore = class {
1232
1718
  try {
1233
1719
  const files = readdirSync(this.sessionDir).filter((file) => file.endsWith(".jsonl"));
1234
1720
  for (const file of files) {
1235
- const filepath = join(this.sessionDir, file);
1721
+ const filepath = join2(this.sessionDir, file);
1236
1722
  if (resolve(filepath) === resolvedNativePath) continue;
1237
1723
  try {
1238
1724
  const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries2(readJsonlPrefixSync(filepath)));
@@ -1247,10 +1733,10 @@ var PiSessionStore = class {
1247
1733
  }
1248
1734
  async findWrapperReferencingNativeSession(nativePath) {
1249
1735
  const resolvedNativePath = resolve(nativePath);
1250
- const files = await readdir(this.sessionDir).catch(() => []);
1736
+ const files = await readdir2(this.sessionDir).catch(() => []);
1251
1737
  for (const file of files) {
1252
1738
  if (!file.endsWith(".jsonl")) continue;
1253
- const filepath = join(this.sessionDir, file);
1739
+ const filepath = join2(this.sessionDir, file);
1254
1740
  if (resolve(filepath) === resolvedNativePath) continue;
1255
1741
  try {
1256
1742
  const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath)));
@@ -1261,7 +1747,7 @@ var PiSessionStore = class {
1261
1747
  return null;
1262
1748
  }
1263
1749
  ensureWrapperForNativeSessionSync(sessionId, nativePath, entries, ctx) {
1264
- const wrapperPath = join(this.sessionDir, `${sessionId}.jsonl`);
1750
+ const wrapperPath = join2(this.sessionDir, `${sessionId}.jsonl`);
1265
1751
  if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
1266
1752
  try {
1267
1753
  readFileSync(wrapperPath, "utf-8");
@@ -1281,7 +1767,7 @@ var PiSessionStore = class {
1281
1767
  return wrapperPath;
1282
1768
  }
1283
1769
  async ensureWrapperForNativeSession(sessionId, nativePath, ctx) {
1284
- const wrapperPath = join(this.sessionDir, `${sessionId}.jsonl`);
1770
+ const wrapperPath = join2(this.sessionDir, `${sessionId}.jsonl`);
1285
1771
  if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
1286
1772
  try {
1287
1773
  await fsStat2(wrapperPath);
@@ -1290,7 +1776,7 @@ var PiSessionStore = class {
1290
1776
  }
1291
1777
  const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(nativePath));
1292
1778
  try {
1293
- await writeFile(
1779
+ await writeFile2(
1294
1780
  wrapperPath,
1295
1781
  buildNativePiSessionWrapper(sessionId, this.cwd, nativePath, entries, ctx),
1296
1782
  { encoding: "utf-8", flag: "wx" }
@@ -1302,27 +1788,67 @@ var PiSessionStore = class {
1302
1788
  return wrapperPath;
1303
1789
  }
1304
1790
  async readLinkedPiSession(filepath) {
1791
+ let last;
1792
+ let parsed;
1305
1793
  try {
1306
- const [fileStat, content] = await Promise.all([
1307
- fsStat2(filepath),
1308
- readFile(filepath, "utf-8")
1309
- ]);
1310
- return { entries: safeParseEntries(content), mtime: fileStat.mtime, size: Number(fileStat.size) };
1794
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1795
+ const before = await fsStat2(filepath);
1796
+ parsed = parseSessionTranscript(await readFile3(filepath, "utf-8"));
1797
+ const after = await fsStat2(filepath);
1798
+ last = after;
1799
+ if (sameFileSnapshot(before, after)) {
1800
+ return {
1801
+ ...parsed,
1802
+ dev: Number(after.dev),
1803
+ ino: Number(after.ino),
1804
+ mtimeMs: Number(after.mtimeMs),
1805
+ ctimeMs: Number(after.ctimeMs),
1806
+ size: Number(after.size),
1807
+ stable: true
1808
+ };
1809
+ }
1810
+ }
1811
+ return parsed && last ? {
1812
+ ...parsed,
1813
+ dev: Number(last.dev),
1814
+ ino: Number(last.ino),
1815
+ mtimeMs: Number(last.mtimeMs),
1816
+ ctimeMs: Number(last.ctimeMs),
1817
+ size: Number(last.size),
1818
+ stable: false
1819
+ } : null;
1311
1820
  } catch {
1312
1821
  return null;
1313
1822
  }
1314
1823
  }
1315
- async readLinkedPiSessionSummary(filepath) {
1824
+ async readTranscriptSummary(filepath) {
1825
+ let last;
1826
+ let summary;
1316
1827
  try {
1317
- const [fileStat, content] = await Promise.all([
1318
- fsStat2(filepath),
1319
- readJsonlPrefix2(filepath)
1320
- ]);
1321
- return { entries: parseJsonlPrefixEntries2(content), mtime: fileStat.mtime, size: Number(fileStat.size) };
1828
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1829
+ const before = await fsStat2(filepath);
1830
+ summary = await summarizeSessionTranscript(filepath);
1831
+ const after = await fsStat2(filepath);
1832
+ last = after;
1833
+ if (sameFileSnapshot(before, after)) return { summary, stat: after, stable: true };
1834
+ }
1835
+ return summary && last ? { summary, stat: last, stable: false } : null;
1322
1836
  } catch {
1323
1837
  return null;
1324
1838
  }
1325
1839
  }
1840
+ async readLinkedPiSessionSummary(filepath) {
1841
+ const scan = await this.readTranscriptSummary(filepath);
1842
+ return scan ? {
1843
+ summary: scan.summary,
1844
+ dev: Number(scan.stat.dev),
1845
+ ino: Number(scan.stat.ino),
1846
+ mtimeMs: Number(scan.stat.mtimeMs),
1847
+ ctimeMs: Number(scan.stat.ctimeMs),
1848
+ size: Number(scan.stat.size),
1849
+ stable: scan.stable
1850
+ } : null;
1851
+ }
1326
1852
  headerBelongsToCtx(header, ctx, directNative = false) {
1327
1853
  if (directNative) return this.nativeFileBelongsToCtx(header, ctx);
1328
1854
  return header ? this.storedCtxBelongsToCtx(readHeaderSessionCtx(header), ctx) : isEmptySessionCtx(ctx);
@@ -1362,7 +1888,7 @@ var PiSessionStore = class {
1362
1888
  }
1363
1889
  };
1364
1890
  async function readJsonlPrefix2(filepath, maxBytes = SUMMARY_PREFIX_BYTES2) {
1365
- const handle = await open4(filepath, "r");
1891
+ const handle = await open3(filepath, "r");
1366
1892
  try {
1367
1893
  const buffer = Buffer.alloc(maxBytes);
1368
1894
  const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
@@ -1458,35 +1984,15 @@ function isTimestampNamedPiSessionFile(filepath, sessionId) {
1458
1984
  const filename = basename(filepath);
1459
1985
  return /^\d{4}-\d{2}-\d{2}/.test(filename) && filename.endsWith(`_${sessionId}.jsonl`);
1460
1986
  }
1461
- function countUserTurns(entries) {
1462
- return entries.filter(
1463
- (e) => e.type === "message" && e.message?.role === "user"
1464
- ).length;
1987
+ function sameFileSnapshot(first, second) {
1988
+ return first.dev === second.dev && first.ino === second.ino && first.size === second.size && first.mtimeMs === second.mtimeMs && first.ctimeMs === second.ctimeMs;
1465
1989
  }
1466
- function hasAssistantReply(entries) {
1467
- return entries.some(
1468
- (entry) => entry.type === "message" && entry.message.role === "assistant"
1469
- );
1990
+ function selectTranscriptSummary(wrapper, native) {
1991
+ return native && native.entryCount > 0 ? native : wrapper;
1470
1992
  }
1471
- function newestDurableTitle(wrapperEntries, nativeEntries) {
1472
- const candidates = [
1473
- ...wrapperEntries.flatMap((entry, offset) => entry.type === "session_info" ? [{ entry, native: 0, offset }] : []),
1474
- ...nativeEntries.flatMap((entry, offset) => entry.type === "session_info" ? [{ entry, native: 1, offset }] : [])
1475
- ];
1476
- candidates.sort((a, b) => {
1477
- const timestamp = Date.parse(b.entry.timestamp) - Date.parse(a.entry.timestamp);
1478
- return timestamp || b.native - a.native || b.offset - a.offset;
1479
- });
1480
- return candidates[0]?.entry.name;
1481
- }
1482
- function firstUserMessage(entries) {
1483
- for (const e of entries) {
1484
- if (e.type !== "message") continue;
1485
- const msg = e.message;
1486
- if (msg?.role !== "user") continue;
1487
- const text = textFromPiContent2(msg.content);
1488
- if (text) return text.slice(0, 80);
1489
- }
1993
+ function projectedTranscriptTitle(wrapper, native) {
1994
+ const selected = selectTranscriptSummary(wrapper, native);
1995
+ return wrapper.userTitle ?? selected.title ?? wrapper.title ?? native?.title ?? selected.firstUserTitle;
1490
1996
  }
1491
1997
  function safeParseEntries(content) {
1492
1998
  try {
@@ -1514,14 +2020,6 @@ function parseJsonlPrefixEntries2(content) {
1514
2020
  }
1515
2021
  return entries;
1516
2022
  }
1517
- function textFromPiContent2(content) {
1518
- if (typeof content === "string") return content;
1519
- if (!Array.isArray(content)) return "";
1520
- return content.map((part) => {
1521
- const item = part;
1522
- return item?.type === "text" && typeof item.text === "string" ? item.text : "";
1523
- }).join("");
1524
- }
1525
2023
  function withStableMessageId(message, entryId) {
1526
2024
  if (!entryId || !message || typeof message !== "object" || Array.isArray(message)) return message;
1527
2025
  if (typeof message.id === "string") return message;
@@ -1622,7 +2120,7 @@ function readApiKeyEnv(candidates) {
1622
2120
  function buildOpenAICompatibleProviderConfig(opts) {
1623
2121
  return {
1624
2122
  baseUrl: opts.baseUrl,
1625
- // pi-coding-agent 0.80.7 resolves `$ENV_VAR` config references through
2123
+ // Pi resolves `$ENV_VAR` config references through
1626
2124
  // the provider environment. A bare name is a literal API key, which would
1627
2125
  // send "INFOMANIAK_API_TOKEN" as the bearer token and cause provider 401s.
1628
2126
  apiKey: `$${opts.apiKeyEnv}`,
@@ -1753,6 +2251,17 @@ function readConfiguredDefaultModel() {
1753
2251
  return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.defaultModel ?? readCustomProvider()?.defaultModel;
1754
2252
  }
1755
2253
 
2254
+ // src/server/models/modelRuntime.ts
2255
+ import {
2256
+ ModelRuntime
2257
+ } from "@mariozechner/pi-coding-agent";
2258
+ async function createConfiguredModelRuntime(options) {
2259
+ const modelRuntime = await ModelRuntime.create(options);
2260
+ const configuredModels = registerConfiguredModelProviders(modelRuntime);
2261
+ await modelRuntime.refresh({ allowNetwork: false });
2262
+ return { modelRuntime, configuredModels };
2263
+ }
2264
+
1756
2265
  // src/server/piPackages.ts
1757
2266
  var PI_PACKAGE_RESOURCE_FILTERS = [
1758
2267
  "extensions",
@@ -1797,7 +2306,7 @@ function mergePiPackageSources(base = [], additional = []) {
1797
2306
 
1798
2307
  // src/server/harness/pi-coding-agent/resourceSettingsManager.ts
1799
2308
  import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1800
- import { join as join2 } from "node:path";
2309
+ import { join as join3 } from "node:path";
1801
2310
  import { SettingsManager as SettingsManager2 } from "@mariozechner/pi-coding-agent";
1802
2311
  function readSettingsFileIfPresent(path) {
1803
2312
  return existsSync(path) ? readFileSync2(path, "utf-8") : void 0;
@@ -1812,8 +2321,8 @@ function mergeInjectedProjectPackages(settingsJson, piPackages, includePackage)
1812
2321
  }
1813
2322
  function createResourceSettingsManager(cwd, agentDir, piPackages, options = {}) {
1814
2323
  if (piPackages.length === 0 && !options.includePackage) return SettingsManager2.create(cwd, agentDir);
1815
- const globalSettingsPath = join2(agentDir, "settings.json");
1816
- const projectSettingsPath = join2(cwd, ".pi", "settings.json");
2324
+ const globalSettingsPath = join3(agentDir, "settings.json");
2325
+ const projectSettingsPath = join3(cwd, ".pi", "settings.json");
1817
2326
  let globalSettingsOverrideJson;
1818
2327
  let projectSettingsOverrideJson;
1819
2328
  const storage = {
@@ -1931,11 +2440,11 @@ function meteredExtensionCommandContext(ctx, command) {
1931
2440
  guarded.switchSession = async () => block();
1932
2441
  return guarded;
1933
2442
  }
1934
- function resolveRequestedModel(modelRegistry, input, options = {}) {
2443
+ function resolveRequestedModel(modelRuntime, input, options = {}) {
1935
2444
  const requestedId = input.model?.id;
1936
2445
  if (!input.model || !requestedId) return void 0;
1937
- const model = modelRegistry.find(input.model.provider, requestedId);
1938
- const available = modelRegistry.getAvailable();
2446
+ const model = modelRuntime.getModel(input.model.provider, requestedId);
2447
+ const available = modelRuntime.getAvailableSnapshot();
1939
2448
  const hasAuth = Boolean(model) && available.some(
1940
2449
  (m) => m.provider === model.provider && m.id === model.id
1941
2450
  );
@@ -1945,13 +2454,13 @@ function resolveRequestedModel(modelRegistry, input, options = {}) {
1945
2454
  }
1946
2455
  return model;
1947
2456
  }
1948
- function resolveDefaultModel(modelRegistry, override, strict) {
2457
+ function resolveDefaultModel(modelRuntime, override, strict) {
1949
2458
  if (override) {
1950
- return resolveRequestedModel(modelRegistry, { model: override }, { strict });
2459
+ return resolveRequestedModel(modelRuntime, { model: override }, { strict });
1951
2460
  }
1952
2461
  const configured = readConfiguredDefaultModel();
1953
2462
  if (configured) {
1954
- const model = modelRegistry.find(configured.provider, configured.id);
2463
+ const model = modelRuntime.getModel(configured.provider, configured.id);
1955
2464
  if (model) return model;
1956
2465
  }
1957
2466
  return void 0;
@@ -1970,7 +2479,7 @@ function sessionCacheKey(sessionId, ctx) {
1970
2479
  return JSON.stringify([sessionId, ctx.workspaceId ?? ""]);
1971
2480
  }
1972
2481
  async function applyRequestedSessionOptions(handle, input, options = {}) {
1973
- const requestedModel = resolveRequestedModel(handle.modelRegistry, input, { strict: options.strictModelResolution });
2482
+ const requestedModel = resolveRequestedModel(handle.modelRuntime, input, { strict: options.strictModelResolution });
1974
2483
  if (requestedModel) {
1975
2484
  const current = handle.piSession.model;
1976
2485
  if (!current || current.provider !== requestedModel.provider || current.id !== requestedModel.id) {
@@ -2102,10 +2611,7 @@ function createPiCodingAgentHarness(opts) {
2102
2611
  return await runContextStorage.run(ctx, run);
2103
2612
  }
2104
2613
  function createRunBoundAdapter(handle, sessionId, ctx) {
2105
- const adapter = createPiAgentSessionAdapter(handle.piSession, {
2106
- sessionId,
2107
- ...handle.piSession.agent && typeof handle.piSession.agent.continue === "function" ? { continueQueuedFollowUp: () => handle.piSession.agent.continue() } : {}
2108
- });
2614
+ const adapter = createPiAgentSessionAdapter(handle.piSession, { sessionId });
2109
2615
  return {
2110
2616
  ...adapter,
2111
2617
  prompt: (promptInput) => bindRunContext(ctx, () => adapter.prompt(promptInput)),
@@ -2115,11 +2621,9 @@ function createPiCodingAgentHarness(opts) {
2115
2621
  };
2116
2622
  }
2117
2623
  async function createPiSession(sessionId, sessionCtx, input, ctx) {
2118
- const authStorage = AuthStorage.create();
2119
- const modelRegistry = ModelRegistry.create(authStorage);
2120
- registerConfiguredModelProviders(modelRegistry);
2121
- const resolvedModel = resolveRequestedModel(modelRegistry, input, { strict: pi.strictModelResolution });
2122
- const model = resolvedModel ?? resolveDefaultModel(modelRegistry, pi.defaultModel, pi.strictModelResolution);
2624
+ const { modelRuntime } = await createConfiguredModelRuntime();
2625
+ const resolvedModel = resolveRequestedModel(modelRuntime, input, { strict: pi.strictModelResolution });
2626
+ const model = resolvedModel ?? resolveDefaultModel(modelRuntime, pi.defaultModel, pi.strictModelResolution);
2123
2627
  const savedPiFile = await sessionStore.loadPiSessionFile(sessionCtx, sessionId);
2124
2628
  const runtimeCwd = opts.runtimeCwd ?? ctx.workdir;
2125
2629
  if (!savedPiFile) {
@@ -2193,8 +2697,8 @@ function createPiCodingAgentHarness(opts) {
2193
2697
  model,
2194
2698
  thinkingLevel: input.thinkingLevel ?? "off",
2195
2699
  sessionManager,
2196
- authStorage,
2197
- modelRegistry,
2700
+ modelRuntime,
2701
+ settingsManager,
2198
2702
  ...resourceLoader ? { resourceLoader } : {}
2199
2703
  });
2200
2704
  const restoreFollowUpContextWrapper = rememberQueuedFollowUpRunContexts(piSession, runContextState, () => runContextStorage.getStore());
@@ -2205,7 +2709,7 @@ function createPiCodingAgentHarness(opts) {
2205
2709
  };
2206
2710
  const handle = {
2207
2711
  piSession,
2208
- modelRegistry,
2712
+ modelRuntime,
2209
2713
  sessionManager,
2210
2714
  resourceLoader,
2211
2715
  sessionId,
@@ -2267,7 +2771,8 @@ function createPiCodingAgentHarness(opts) {
2267
2771
  * the expected pre-first-turn state, not an error.
2268
2772
  */
2269
2773
  getSystemPrompt(sessionId) {
2270
- return piSessionHandlesFor(sessionId)[0]?.piSession.systemPrompt;
2774
+ const prompt = piSessionHandlesFor(sessionId)[0]?.piSession.systemPrompt;
2775
+ return prompt && pi.locateSkillResource ? projectSkillResourceLocations(prompt, pi.locateSkillResource) : prompt;
2271
2776
  },
2272
2777
  hasPiSession(sessionId, ctx) {
2273
2778
  return ctx ? piSessions.has(sessionCacheKey(sessionId, ctx)) : piSessionHandlesFor(sessionId).length > 0;
@@ -2339,8 +2844,8 @@ export {
2339
2844
  getEnv,
2340
2845
  PiSessionStore,
2341
2846
  parseEncodedModelSelection,
2342
- registerConfiguredModelProviders,
2343
2847
  readConfiguredDefaultModel,
2848
+ createConfiguredModelRuntime,
2344
2849
  PI_PACKAGE_RESOURCE_FILTERS,
2345
2850
  piPackageSourceKey,
2346
2851
  compactPiPackages,