@hachej/boring-agent 0.1.104 → 0.1.107

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-I7KFNGB4.js → chunk-GS4ADE34.js} +877 -349
  5. package/dist/{chunk-BTYOMR7W.js → chunk-KDXIXVF6.js} +5179 -3668
  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-POZ4AXCZ.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 +43 -7
  14. package/dist/front/index.js +1505 -729
  15. package/dist/front/styles.css +109 -15
  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 +207 -243
  23. package/dist/server/index.js +15 -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-B1xDlBSo.d.ts → types-B7-PN5jV.d.ts} +75 -23
  28. package/docs/AGENT_GATEWAY_V0.md +40 -2
  29. package/docs/ERROR_CODES.md +2 -1
  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) };
@@ -688,27 +1127,36 @@ var PiSessionStore = class {
688
1127
  }));
689
1128
  const existingFiles = fileStats.filter((item) => item !== null);
690
1129
  const referencedPiFiles = await this.referencedPiFiles(existingFiles);
691
- const visibleFiles = await Promise.all(existingFiles.filter(({ filepath }) => !referencedPiFiles.has(resolve(filepath))).map(async (file) => ({
692
- ...file,
693
- sortMtimeMs: await this.sessionSortMtimeMs(file)
694
- })));
695
- visibleFiles.sort((a, b) => b.sortMtimeMs - a.sortMtimeMs);
1130
+ const visibleFiles = await Promise.all(existingFiles.filter(({ filepath }) => !referencedPiFiles.has(resolve(filepath))).map(async (file) => ({ ...file, ...await this.sessionSortKey(file) })));
1131
+ visibleFiles.sort((a, b) => compareSessionOrder(
1132
+ [a.orderingUpdatedAtMs, "", a.sortId],
1133
+ [b.orderingUpdatedAtMs, "", b.sortId]
1134
+ ));
696
1135
  const { offset, limit } = options;
697
1136
  const includeId = options.includeId;
698
- 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, {
699
1145
  ctx,
700
1146
  offset,
701
1147
  limit,
702
1148
  includeId,
703
- includeEmpty: options.includeEmpty
1149
+ includeEmpty: options.includeEmpty,
1150
+ archivedIds
704
1151
  });
705
1152
  if (!includeId || pageSummaries.some((summary) => summary.id === includeId)) return pageSummaries;
706
1153
  const includeSummary = await this.summarizeIncludedSession(ctx, includeId, referencedPiFiles);
707
- return includeSummary ? [...pageSummaries, includeSummary] : pageSummaries;
1154
+ return includeSummary ? [...pageSummaries, withArchivedFlag(includeSummary, archivedIds)] : pageSummaries;
708
1155
  }
709
1156
  async create(ctx, init) {
710
- await mkdir(this.sessionDir, { recursive: true });
711
- 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();
712
1160
  const now = (/* @__PURE__ */ new Date()).toISOString();
713
1161
  const header = {
714
1162
  type: "session",
@@ -719,21 +1167,21 @@ var PiSessionStore = class {
719
1167
  boringSessionCtx: normalizeSessionCtx(ctx) ?? {}
720
1168
  };
721
1169
  const lines = [JSON.stringify(header)];
722
- if (init?.title) {
723
- const infoEntry = {
724
- type: "session_info",
725
- id: randomUUID(),
1170
+ if (manualTitle) {
1171
+ const pair = createUserSessionTitleEntries({
1172
+ title: manualTitle,
726
1173
  parentId: null,
727
1174
  timestamp: now,
728
- name: init.title
729
- };
730
- lines.push(JSON.stringify(infoEntry));
1175
+ authorityId: randomUUID3(),
1176
+ titleId: randomUUID3()
1177
+ });
1178
+ lines.push(JSON.stringify(pair.authority), JSON.stringify(pair.title));
731
1179
  }
732
- const filepath = join(this.sessionDir, `${nativeSessionFilename(id, now)}`);
733
- 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");
734
1182
  return {
735
1183
  id,
736
- title: init?.title ?? "New session",
1184
+ title: manualTitle ?? "New session",
737
1185
  createdAt: now,
738
1186
  updatedAt: now,
739
1187
  turnCount: 0,
@@ -741,23 +1189,39 @@ var PiSessionStore = class {
741
1189
  hasAssistantReply: false
742
1190
  };
743
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
+ }
744
1210
  async load(ctx, sessionId) {
745
1211
  const resolved = await this.resolveSessionTranscript(ctx, sessionId);
746
- const nativeSummary = resolved.directNative ? await summarizeNativeTranscript(resolved.filepath) : null;
747
- const title = newestDurableTitle(resolved.sessionEntries, resolved.linkedEntries) ?? nativeSummary?.title ?? nativeSummary?.firstUserTitle ?? "New session";
748
- const turnCount = countUserTurns(resolved.transcriptEntries);
749
- const updatedAtMs = nativeSummary?.latestMessageAtMs ?? Math.max(resolved.fileStat.mtime.getTime(), resolved.linkedMtimeMs ?? 0);
750
- 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({
751
1215
  id: resolved.resolvedSessionId,
752
1216
  title,
753
1217
  createdAt: resolved.header?.timestamp ?? resolved.fileStat.birthtime.toISOString(),
754
1218
  updatedAt: new Date(updatedAtMs).toISOString(),
755
- turnCount,
1219
+ turnCount: resolved.transcriptSummary.turnCount,
756
1220
  ...resolved.directNative ? {
757
1221
  nativeSessionId: resolved.resolvedSessionId,
758
- hasAssistantReply: hasAssistantReply(resolved.transcriptEntries)
1222
+ hasAssistantReply: resolved.transcriptSummary.hasAssistantReply
759
1223
  } : {}
760
- };
1224
+ }, await readArchivedSessionIds(this.sessionDir));
761
1225
  }
762
1226
  /**
763
1227
  * Returns the persisted pi message objects in file order so callers can run
@@ -790,11 +1254,12 @@ var PiSessionStore = class {
790
1254
  const filepath = await this.resolveSessionFile(sessionId, ctx);
791
1255
  let content;
792
1256
  try {
793
- content = await readFile(filepath, "utf-8");
1257
+ content = await readFile3(filepath, "utf-8");
794
1258
  } catch {
795
1259
  throw new Error(`Session not found: ${sessionId}`);
796
1260
  }
797
- const fileEntries = safeParseEntries(content);
1261
+ const parsedFile = parseSessionTranscript(content);
1262
+ const fileEntries = parsedFile.entries;
798
1263
  const header = fileEntries.find(
799
1264
  (e) => e.type === "session"
800
1265
  );
@@ -802,17 +1267,6 @@ var PiSessionStore = class {
802
1267
  filepath,
803
1268
  header?.id ?? sessionId
804
1269
  );
805
- if (!timestampNamedNative && fileEntries.some((e) => e.type === "ui_snapshot")) {
806
- const compacted = fileEntries.filter((e) => e.type !== "ui_snapshot").map((e) => JSON.stringify(e)).join("\n") + "\n";
807
- const tmp = `${filepath}.compact-${randomUUID()}`;
808
- try {
809
- await writeFile(tmp, compacted, "utf-8");
810
- await rename(tmp, filepath);
811
- } catch {
812
- await rm(tmp, { force: true }).catch(() => {
813
- });
814
- }
815
- }
816
1270
  if (!this.headerBelongsToCtx(header, ctx, timestampNamedNative)) {
817
1271
  throw new Error(`Session not found: ${sessionId}`);
818
1272
  }
@@ -826,17 +1280,18 @@ var PiSessionStore = class {
826
1280
  const linkedEntries = linked?.entries.filter(
827
1281
  (e) => e.type !== "session"
828
1282
  ) ?? [];
829
- const transcriptEntries = linkedEntries.length > 0 ? linkedEntries : sessionEntries;
1283
+ const transcriptSummary = selectTranscriptSummary(parsedFile.summary, linked?.summary);
1284
+ const transcriptEntries = transcriptSummary === linked?.summary ? linkedEntries : sessionEntries;
830
1285
  return {
831
1286
  resolvedSessionId: header?.id ?? sessionId,
832
1287
  header,
833
- sessionEntries,
834
- linkedEntries,
835
1288
  transcriptEntries,
1289
+ sessionSummary: parsedFile.summary,
1290
+ ...linked ? { linkedSummary: linked.summary } : {},
1291
+ transcriptSummary,
836
1292
  fileStat,
837
- linkedMtimeMs: linked?.mtime.getTime(),
1293
+ linkedMtimeMs: linked?.mtimeMs,
838
1294
  filepath,
839
- ...linkedPiFile && linked ? { linkedFilepath: linkedPiFile } : {},
840
1295
  directNative
841
1296
  };
842
1297
  }
@@ -845,8 +1300,7 @@ var PiSessionStore = class {
845
1300
  * appended through the same JSONL path under one process-local writer lock.
846
1301
  */
847
1302
  async rename(ctx, sessionId, title) {
848
- const trimmed = title.replace(/[\r\n]+/g, " ").trim();
849
- if (!trimmed) throw new Error("Session title must not be empty");
1303
+ const trimmed = normalizeUserSessionTitle(title);
850
1304
  return await this.withWriter(sessionId, async () => {
851
1305
  const resolved = await this.resolveSessionTranscript(ctx, sessionId);
852
1306
  if (resolved.directNative) {
@@ -862,20 +1316,8 @@ var PiSessionStore = class {
862
1316
  }
863
1317
  return await this.load(ctx, sessionId);
864
1318
  }
865
- const entry = {
866
- type: "session_info",
867
- id: randomUUID(),
868
- parentId: null,
869
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
870
- name: trimmed
871
- };
872
- const line = `${JSON.stringify(entry)}
873
- `;
874
- const targets = [resolved.filepath, resolved.linkedFilepath].filter((path) => Boolean(path));
875
- for (const filepath of targets) {
876
- await appendFile(filepath, line);
877
- this.prefixCache.delete(filepath);
878
- }
1319
+ await appendVerifiedWrapperRename(resolved.filepath, trimmed);
1320
+ this.prefixCache.delete(resolved.filepath);
879
1321
  return await this.load(ctx, sessionId);
880
1322
  });
881
1323
  }
@@ -901,7 +1343,7 @@ var PiSessionStore = class {
901
1343
  loadPiSessionFileSync(ctx, sessionId) {
902
1344
  if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) return null;
903
1345
  try {
904
- const direct = join(this.sessionDir, `${sessionId}.jsonl`);
1346
+ const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
905
1347
  let filepath = direct;
906
1348
  let content;
907
1349
  try {
@@ -911,7 +1353,7 @@ var PiSessionStore = class {
911
1353
  (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
912
1354
  );
913
1355
  if (files.length === 0) return null;
914
- filepath = join(this.sessionDir, files[0]);
1356
+ filepath = join2(this.sessionDir, files[0]);
915
1357
  content = readFileSync(filepath, "utf-8");
916
1358
  }
917
1359
  const entries = safeParseEntries(content);
@@ -941,19 +1383,19 @@ var PiSessionStore = class {
941
1383
  async loadPiSessionFile(ctx, sessionId) {
942
1384
  if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) return null;
943
1385
  try {
944
- const direct = join(this.sessionDir, `${sessionId}.jsonl`);
1386
+ const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
945
1387
  let filepath = direct;
946
1388
  let content;
947
1389
  try {
948
- content = await readFile(direct, "utf-8");
1390
+ content = await readFile3(direct, "utf-8");
949
1391
  } catch {
950
- const files = await readdir(this.sessionDir).catch(() => []);
1392
+ const files = await readdir2(this.sessionDir).catch(() => []);
951
1393
  const match = files.find(
952
1394
  (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
953
1395
  );
954
1396
  if (!match) return null;
955
- filepath = join(this.sessionDir, match);
956
- content = await readFile(filepath, "utf-8");
1397
+ filepath = join2(this.sessionDir, match);
1398
+ content = await readFile3(filepath, "utf-8");
957
1399
  }
958
1400
  const entries = safeParseEntries(content);
959
1401
  const header = entries.find((entry) => entry.type === "session");
@@ -967,8 +1409,8 @@ var PiSessionStore = class {
967
1409
  if (readHeaderSessionCtx(header) !== null) return null;
968
1410
  const existingWrapper = await this.findWrapperReferencingNativeSession(filepath);
969
1411
  if (existingWrapper) {
970
- const wrapperSessionId = await this.readSessionFileId(existingWrapper);
971
- if (wrapperSessionId !== sessionId) return null;
1412
+ const wrapperSessionId2 = await this.readSessionFileId(existingWrapper);
1413
+ if (wrapperSessionId2 !== sessionId) return null;
972
1414
  const wrapperEntries = parseJsonlPrefixEntries2(await readJsonlPrefix2(existingWrapper));
973
1415
  const wrapperHeader = wrapperEntries.find((entry) => entry.type === "session");
974
1416
  if (!this.headerBelongsToCtx(wrapperHeader, ctx)) return null;
@@ -987,7 +1429,7 @@ var PiSessionStore = class {
987
1429
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
988
1430
  path: piFilePath
989
1431
  });
990
- await appendFile(filepath, entry + "\n");
1432
+ await appendFile2(filepath, entry + "\n");
991
1433
  }
992
1434
  async delete(ctx, sessionId) {
993
1435
  const filepath = await this.resolveSessionFile(sessionId, ctx).catch(
@@ -997,30 +1439,31 @@ var PiSessionStore = class {
997
1439
  const fileSessionId = await this.readSessionFileId(filepath);
998
1440
  if (fileSessionId && fileSessionId !== sessionId) return;
999
1441
  const linkedPiFile = await this.linkedPiFileFor(filepath);
1000
- await rm(filepath, { force: true });
1442
+ await rm2(filepath, { force: true });
1001
1443
  this.prefixCache.delete(filepath);
1002
1444
  if (linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)) {
1003
- await rm(linkedPiFile, { force: true });
1445
+ await rm2(linkedPiFile, { force: true });
1004
1446
  this.prefixCache.delete(linkedPiFile);
1005
1447
  }
1448
+ await forgetArchivedSession(this.sessionDir, fileSessionId ?? sessionId);
1006
1449
  }
1007
1450
  async resolveSessionFile(sessionId, ctx) {
1008
1451
  if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) {
1009
1452
  throw new Error(`Session not found: ${sessionId}`);
1010
1453
  }
1011
- const direct = join(this.sessionDir, `${sessionId}.jsonl`);
1454
+ const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
1012
1455
  try {
1013
1456
  await fsStat2(direct);
1014
1457
  if (ctx) await this.assertFileBelongsToCtx(direct, ctx, sessionId);
1015
1458
  return direct;
1016
1459
  } catch {
1017
1460
  }
1018
- const files = await readdir(this.sessionDir).catch(() => []);
1461
+ const files = await readdir2(this.sessionDir).catch(() => []);
1019
1462
  const match = files.find(
1020
1463
  (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
1021
1464
  );
1022
1465
  if (!match) throw new Error(`Session not found: ${sessionId}`);
1023
- const matchedPath = join(this.sessionDir, match);
1466
+ const matchedPath = join2(this.sessionDir, match);
1024
1467
  if (!isTimestampNamedPiSessionFile(matchedPath, sessionId)) {
1025
1468
  if (ctx) await this.assertFileBelongsToCtx(matchedPath, ctx, sessionId);
1026
1469
  return matchedPath;
@@ -1050,7 +1493,7 @@ var PiSessionStore = class {
1050
1493
  }
1051
1494
  async linkedPiFileFor(filepath) {
1052
1495
  try {
1053
- const content = await readFile(filepath, "utf-8");
1496
+ const content = await readFile3(filepath, "utf-8");
1054
1497
  return extractPiSessionFilePath(safeParseEntries(content));
1055
1498
  } catch {
1056
1499
  return null;
@@ -1069,21 +1512,53 @@ var PiSessionStore = class {
1069
1512
  }));
1070
1513
  return referenced;
1071
1514
  }
1072
- async sessionSortMtimeMs({ filepath, stat }) {
1073
- let sortMtimeMs = stat.mtime.getTime();
1515
+ async sessionSortKey({ filepath, stat }) {
1516
+ let fileMtimeMs = Number(stat.mtimeMs);
1517
+ let headerId;
1518
+ const stemFallback = () => ({
1519
+ orderingUpdatedAtMs: fileMtimeMs,
1520
+ sortId: wrapperSessionId(filepath)
1521
+ });
1074
1522
  try {
1075
- const linkedPiFile = (await this.readPrefixCache(filepath, stat)).referencedPiFile;
1076
- if (linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)) {
1077
- const linkedStat = await fsStat2(linkedPiFile);
1078
- sortMtimeMs = Math.max(sortMtimeMs, linkedStat.mtime.getTime());
1523
+ const cached = await this.readPrefixCache(filepath, stat);
1524
+ fileMtimeMs = cached.mtimeMs;
1525
+ headerId = cached.headerId ?? void 0;
1526
+ if (!headerId) return stemFallback();
1527
+ const linkedPiFile = cached.referencedPiFile;
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 };
1079
1535
  }
1080
- const header = parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath)).find((entry) => entry.type === "session");
1081
- if (header && isTimestampNamedPiSessionFile(filepath, header.id)) {
1082
- sortMtimeMs = await latestNativeMessageTimestamp(filepath, Number(stat.size)) ?? sortMtimeMs;
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;
1083
1543
  }
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 };
1084
1559
  } catch {
1560
+ return headerId ? { orderingUpdatedAtMs: fileMtimeMs, sortId: headerId } : stemFallback();
1085
1561
  }
1086
- return sortMtimeMs;
1087
1562
  }
1088
1563
  async summarizeFile(ctx, filepath, existingStat) {
1089
1564
  try {
@@ -1103,39 +1578,51 @@ var PiSessionStore = class {
1103
1578
  const directNative = isTimestampNamedPiSessionFile(filepath, header.id);
1104
1579
  if (directNative ? !this.nativeFileBelongsToCtx(header, ctx) : !this.storedCtxBelongsToCtx(sessionCtx, ctx)) return null;
1105
1580
  const entries = parseJsonlPrefixEntries2(content);
1106
- const sessionEntries = entries.filter(
1107
- (e) => e.type !== "session"
1108
- );
1109
1581
  const linkedPiFile = extractPiSessionFilePath(entries);
1110
- const linked = linkedPiFile && resolve(linkedPiFile) !== resolve(filepath) ? await this.readLinkedPiSessionSummary(linkedPiFile) : null;
1111
- const linkedEntries = linked?.entries.filter(
1112
- (e) => e.type !== "session"
1113
- ) ?? [];
1114
- const nativeSummary = directNative ? await summarizeNativeTranscript(filepath) : null;
1115
- const title = newestDurableTitle(sessionEntries, linkedEntries) ?? nativeSummary?.title ?? nativeSummary?.firstUserTitle ?? firstUserMessage(linkedEntries) ?? firstUserMessage(sessionEntries) ?? "New session";
1116
- const turnCount = nativeSummary?.turnCount ?? [...sessionEntries, ...linkedEntries].filter(
1117
- (e) => e.type === "message" && e.message?.role === "user"
1118
- ).length;
1119
- 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";
1120
1591
  const summary = {
1121
1592
  id: header.id,
1122
1593
  title,
1123
1594
  createdAt: header.timestamp,
1124
- updatedAt: new Date(updatedAtMs).toISOString(),
1125
- turnCount,
1126
- ...nativeSummary ? {
1595
+ updatedAt: new Date(order.orderingUpdatedAtMs).toISOString(),
1596
+ turnCount: transcript.turnCount,
1597
+ ...directNative ? {
1127
1598
  nativeSessionId: header.id,
1128
- hasAssistantReply: nativeSummary.hasAssistantReply
1599
+ hasAssistantReply: transcript.hasAssistantReply
1129
1600
  } : {}
1130
1601
  };
1131
- this.prefixCache.set(filepath, {
1132
- mtimeMs: fileStat.mtime.getTime(),
1133
- size: Number(fileStat.size),
1134
- referencedPiFile: linkedPiFile,
1135
- sessionCtx,
1136
- ...linked ? { linkedMtimeMs: linked.mtime.getTime(), linkedSize: linked.size } : {},
1137
- ...!nativeSummary ? { summary } : {}
1138
- });
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
+ }
1139
1626
  return summary;
1140
1627
  } catch {
1141
1628
  return null;
@@ -1144,7 +1631,7 @@ var PiSessionStore = class {
1144
1631
  cachedPrefix(filepath, fileStat) {
1145
1632
  const cached = this.prefixCache.get(filepath);
1146
1633
  if (!cached) return void 0;
1147
- 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;
1148
1635
  return cached;
1149
1636
  }
1150
1637
  async cachedSummaryIsFresh(filepath, cached) {
@@ -1152,7 +1639,7 @@ var PiSessionStore = class {
1152
1639
  if (!linkedPiFile || resolve(linkedPiFile) === resolve(filepath)) return true;
1153
1640
  try {
1154
1641
  const linkedStat = await fsStat2(linkedPiFile);
1155
- 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);
1156
1643
  } catch {
1157
1644
  return cached.linkedMtimeMs === void 0 && cached.linkedSize === void 0;
1158
1645
  }
@@ -1160,15 +1647,29 @@ var PiSessionStore = class {
1160
1647
  async readPrefixCache(filepath, fileStat) {
1161
1648
  const cached = this.cachedPrefix(filepath, fileStat);
1162
1649
  if (cached) return cached;
1163
- const content = await readJsonlPrefix2(filepath);
1164
- const entries = parseJsonlPrefixEntries2(content);
1165
- const entry = {
1166
- mtimeMs: fileStat.mtime.getTime(),
1167
- size: Number(fileStat.size),
1168
- referencedPiFile: extractPiSessionFilePath(entries),
1169
- sessionCtx: readHeaderSessionCtx(entries.find((item) => item.type === "session"))
1170
- };
1171
- 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
+ }
1172
1673
  return entry;
1173
1674
  }
1174
1675
  async summarizeVisiblePage(visibleFiles, options) {
@@ -1176,15 +1677,19 @@ var PiSessionStore = class {
1176
1677
  const page = [];
1177
1678
  let validSeen = 0;
1178
1679
  let index = 0;
1179
- 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
+ );
1180
1684
  while (index < visibleFiles.length && (options.limit === void 0 || page.length < options.limit)) {
1181
1685
  const batch = visibleFiles.slice(index, index + batchSize);
1182
1686
  index += batch.length;
1183
1687
  const summaries = await Promise.all(
1184
1688
  batch.map(({ filepath, stat }) => this.summarizeFile(options.ctx, filepath, stat))
1185
1689
  );
1186
- for (const summary of summaries) {
1187
- if (!summary) continue;
1690
+ for (const rawSummary of summaries) {
1691
+ if (!rawSummary) continue;
1692
+ const summary = withArchivedFlag(rawSummary, options.archivedIds);
1188
1693
  if (summary.turnCount === 0 && !options.includeEmpty && summary.id !== options.includeId) {
1189
1694
  continue;
1190
1695
  }
@@ -1213,7 +1718,7 @@ var PiSessionStore = class {
1213
1718
  try {
1214
1719
  const files = readdirSync(this.sessionDir).filter((file) => file.endsWith(".jsonl"));
1215
1720
  for (const file of files) {
1216
- const filepath = join(this.sessionDir, file);
1721
+ const filepath = join2(this.sessionDir, file);
1217
1722
  if (resolve(filepath) === resolvedNativePath) continue;
1218
1723
  try {
1219
1724
  const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries2(readJsonlPrefixSync(filepath)));
@@ -1228,10 +1733,10 @@ var PiSessionStore = class {
1228
1733
  }
1229
1734
  async findWrapperReferencingNativeSession(nativePath) {
1230
1735
  const resolvedNativePath = resolve(nativePath);
1231
- const files = await readdir(this.sessionDir).catch(() => []);
1736
+ const files = await readdir2(this.sessionDir).catch(() => []);
1232
1737
  for (const file of files) {
1233
1738
  if (!file.endsWith(".jsonl")) continue;
1234
- const filepath = join(this.sessionDir, file);
1739
+ const filepath = join2(this.sessionDir, file);
1235
1740
  if (resolve(filepath) === resolvedNativePath) continue;
1236
1741
  try {
1237
1742
  const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath)));
@@ -1242,7 +1747,7 @@ var PiSessionStore = class {
1242
1747
  return null;
1243
1748
  }
1244
1749
  ensureWrapperForNativeSessionSync(sessionId, nativePath, entries, ctx) {
1245
- const wrapperPath = join(this.sessionDir, `${sessionId}.jsonl`);
1750
+ const wrapperPath = join2(this.sessionDir, `${sessionId}.jsonl`);
1246
1751
  if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
1247
1752
  try {
1248
1753
  readFileSync(wrapperPath, "utf-8");
@@ -1262,7 +1767,7 @@ var PiSessionStore = class {
1262
1767
  return wrapperPath;
1263
1768
  }
1264
1769
  async ensureWrapperForNativeSession(sessionId, nativePath, ctx) {
1265
- const wrapperPath = join(this.sessionDir, `${sessionId}.jsonl`);
1770
+ const wrapperPath = join2(this.sessionDir, `${sessionId}.jsonl`);
1266
1771
  if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
1267
1772
  try {
1268
1773
  await fsStat2(wrapperPath);
@@ -1271,7 +1776,7 @@ var PiSessionStore = class {
1271
1776
  }
1272
1777
  const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(nativePath));
1273
1778
  try {
1274
- await writeFile(
1779
+ await writeFile2(
1275
1780
  wrapperPath,
1276
1781
  buildNativePiSessionWrapper(sessionId, this.cwd, nativePath, entries, ctx),
1277
1782
  { encoding: "utf-8", flag: "wx" }
@@ -1283,27 +1788,67 @@ var PiSessionStore = class {
1283
1788
  return wrapperPath;
1284
1789
  }
1285
1790
  async readLinkedPiSession(filepath) {
1791
+ let last;
1792
+ let parsed;
1286
1793
  try {
1287
- const [fileStat, content] = await Promise.all([
1288
- fsStat2(filepath),
1289
- readFile(filepath, "utf-8")
1290
- ]);
1291
- 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;
1292
1820
  } catch {
1293
1821
  return null;
1294
1822
  }
1295
1823
  }
1296
- async readLinkedPiSessionSummary(filepath) {
1824
+ async readTranscriptSummary(filepath) {
1825
+ let last;
1826
+ let summary;
1297
1827
  try {
1298
- const [fileStat, content] = await Promise.all([
1299
- fsStat2(filepath),
1300
- readJsonlPrefix2(filepath)
1301
- ]);
1302
- 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;
1303
1836
  } catch {
1304
1837
  return null;
1305
1838
  }
1306
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
+ }
1307
1852
  headerBelongsToCtx(header, ctx, directNative = false) {
1308
1853
  if (directNative) return this.nativeFileBelongsToCtx(header, ctx);
1309
1854
  return header ? this.storedCtxBelongsToCtx(readHeaderSessionCtx(header), ctx) : isEmptySessionCtx(ctx);
@@ -1343,7 +1888,7 @@ var PiSessionStore = class {
1343
1888
  }
1344
1889
  };
1345
1890
  async function readJsonlPrefix2(filepath, maxBytes = SUMMARY_PREFIX_BYTES2) {
1346
- const handle = await open4(filepath, "r");
1891
+ const handle = await open3(filepath, "r");
1347
1892
  try {
1348
1893
  const buffer = Buffer.alloc(maxBytes);
1349
1894
  const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
@@ -1431,39 +1976,23 @@ function extractSessionHeaderId(entries) {
1431
1976
  function nativeSessionFilename(sessionId, isoTimestamp) {
1432
1977
  return `${isoTimestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`;
1433
1978
  }
1979
+ function wrapperSessionId(filepath) {
1980
+ const base = basename(filepath);
1981
+ return base.endsWith(".jsonl") ? base.slice(0, -".jsonl".length) : base;
1982
+ }
1434
1983
  function isTimestampNamedPiSessionFile(filepath, sessionId) {
1435
1984
  const filename = basename(filepath);
1436
1985
  return /^\d{4}-\d{2}-\d{2}/.test(filename) && filename.endsWith(`_${sessionId}.jsonl`);
1437
1986
  }
1438
- function countUserTurns(entries) {
1439
- return entries.filter(
1440
- (e) => e.type === "message" && e.message?.role === "user"
1441
- ).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;
1442
1989
  }
1443
- function hasAssistantReply(entries) {
1444
- return entries.some(
1445
- (entry) => entry.type === "message" && entry.message.role === "assistant"
1446
- );
1990
+ function selectTranscriptSummary(wrapper, native) {
1991
+ return native && native.entryCount > 0 ? native : wrapper;
1447
1992
  }
1448
- function newestDurableTitle(wrapperEntries, nativeEntries) {
1449
- const candidates = [
1450
- ...wrapperEntries.flatMap((entry, offset) => entry.type === "session_info" ? [{ entry, native: 0, offset }] : []),
1451
- ...nativeEntries.flatMap((entry, offset) => entry.type === "session_info" ? [{ entry, native: 1, offset }] : [])
1452
- ];
1453
- candidates.sort((a, b) => {
1454
- const timestamp = Date.parse(b.entry.timestamp) - Date.parse(a.entry.timestamp);
1455
- return timestamp || b.native - a.native || b.offset - a.offset;
1456
- });
1457
- return candidates[0]?.entry.name;
1458
- }
1459
- function firstUserMessage(entries) {
1460
- for (const e of entries) {
1461
- if (e.type !== "message") continue;
1462
- const msg = e.message;
1463
- if (msg?.role !== "user") continue;
1464
- const text = textFromPiContent2(msg.content);
1465
- if (text) return text.slice(0, 80);
1466
- }
1993
+ function projectedTranscriptTitle(wrapper, native) {
1994
+ const selected = selectTranscriptSummary(wrapper, native);
1995
+ return wrapper.userTitle ?? selected.title ?? wrapper.title ?? native?.title ?? selected.firstUserTitle;
1467
1996
  }
1468
1997
  function safeParseEntries(content) {
1469
1998
  try {
@@ -1491,14 +2020,6 @@ function parseJsonlPrefixEntries2(content) {
1491
2020
  }
1492
2021
  return entries;
1493
2022
  }
1494
- function textFromPiContent2(content) {
1495
- if (typeof content === "string") return content;
1496
- if (!Array.isArray(content)) return "";
1497
- return content.map((part) => {
1498
- const item = part;
1499
- return item?.type === "text" && typeof item.text === "string" ? item.text : "";
1500
- }).join("");
1501
- }
1502
2023
  function withStableMessageId(message, entryId) {
1503
2024
  if (!entryId || !message || typeof message !== "object" || Array.isArray(message)) return message;
1504
2025
  if (typeof message.id === "string") return message;
@@ -1599,7 +2120,7 @@ function readApiKeyEnv(candidates) {
1599
2120
  function buildOpenAICompatibleProviderConfig(opts) {
1600
2121
  return {
1601
2122
  baseUrl: opts.baseUrl,
1602
- // pi-coding-agent 0.80.7 resolves `$ENV_VAR` config references through
2123
+ // Pi resolves `$ENV_VAR` config references through
1603
2124
  // the provider environment. A bare name is a literal API key, which would
1604
2125
  // send "INFOMANIAK_API_TOKEN" as the bearer token and cause provider 401s.
1605
2126
  apiKey: `$${opts.apiKeyEnv}`,
@@ -1730,6 +2251,17 @@ function readConfiguredDefaultModel() {
1730
2251
  return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.defaultModel ?? readCustomProvider()?.defaultModel;
1731
2252
  }
1732
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
+
1733
2265
  // src/server/piPackages.ts
1734
2266
  var PI_PACKAGE_RESOURCE_FILTERS = [
1735
2267
  "extensions",
@@ -1774,7 +2306,7 @@ function mergePiPackageSources(base = [], additional = []) {
1774
2306
 
1775
2307
  // src/server/harness/pi-coding-agent/resourceSettingsManager.ts
1776
2308
  import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1777
- import { join as join2 } from "node:path";
2309
+ import { join as join3 } from "node:path";
1778
2310
  import { SettingsManager as SettingsManager2 } from "@mariozechner/pi-coding-agent";
1779
2311
  function readSettingsFileIfPresent(path) {
1780
2312
  return existsSync(path) ? readFileSync2(path, "utf-8") : void 0;
@@ -1789,8 +2321,8 @@ function mergeInjectedProjectPackages(settingsJson, piPackages, includePackage)
1789
2321
  }
1790
2322
  function createResourceSettingsManager(cwd, agentDir, piPackages, options = {}) {
1791
2323
  if (piPackages.length === 0 && !options.includePackage) return SettingsManager2.create(cwd, agentDir);
1792
- const globalSettingsPath = join2(agentDir, "settings.json");
1793
- const projectSettingsPath = join2(cwd, ".pi", "settings.json");
2324
+ const globalSettingsPath = join3(agentDir, "settings.json");
2325
+ const projectSettingsPath = join3(cwd, ".pi", "settings.json");
1794
2326
  let globalSettingsOverrideJson;
1795
2327
  let projectSettingsOverrideJson;
1796
2328
  const storage = {
@@ -1908,11 +2440,11 @@ function meteredExtensionCommandContext(ctx, command) {
1908
2440
  guarded.switchSession = async () => block();
1909
2441
  return guarded;
1910
2442
  }
1911
- function resolveRequestedModel(modelRegistry, input, options = {}) {
2443
+ function resolveRequestedModel(modelRuntime, input, options = {}) {
1912
2444
  const requestedId = input.model?.id;
1913
2445
  if (!input.model || !requestedId) return void 0;
1914
- const model = modelRegistry.find(input.model.provider, requestedId);
1915
- const available = modelRegistry.getAvailable();
2446
+ const model = modelRuntime.getModel(input.model.provider, requestedId);
2447
+ const available = modelRuntime.getAvailableSnapshot();
1916
2448
  const hasAuth = Boolean(model) && available.some(
1917
2449
  (m) => m.provider === model.provider && m.id === model.id
1918
2450
  );
@@ -1922,13 +2454,13 @@ function resolveRequestedModel(modelRegistry, input, options = {}) {
1922
2454
  }
1923
2455
  return model;
1924
2456
  }
1925
- function resolveDefaultModel(modelRegistry, override, strict) {
2457
+ function resolveDefaultModel(modelRuntime, override, strict) {
1926
2458
  if (override) {
1927
- return resolveRequestedModel(modelRegistry, { model: override }, { strict });
2459
+ return resolveRequestedModel(modelRuntime, { model: override }, { strict });
1928
2460
  }
1929
2461
  const configured = readConfiguredDefaultModel();
1930
2462
  if (configured) {
1931
- const model = modelRegistry.find(configured.provider, configured.id);
2463
+ const model = modelRuntime.getModel(configured.provider, configured.id);
1932
2464
  if (model) return model;
1933
2465
  }
1934
2466
  return void 0;
@@ -1947,7 +2479,7 @@ function sessionCacheKey(sessionId, ctx) {
1947
2479
  return JSON.stringify([sessionId, ctx.workspaceId ?? ""]);
1948
2480
  }
1949
2481
  async function applyRequestedSessionOptions(handle, input, options = {}) {
1950
- const requestedModel = resolveRequestedModel(handle.modelRegistry, input, { strict: options.strictModelResolution });
2482
+ const requestedModel = resolveRequestedModel(handle.modelRuntime, input, { strict: options.strictModelResolution });
1951
2483
  if (requestedModel) {
1952
2484
  const current = handle.piSession.model;
1953
2485
  if (!current || current.provider !== requestedModel.provider || current.id !== requestedModel.id) {
@@ -2079,10 +2611,7 @@ function createPiCodingAgentHarness(opts) {
2079
2611
  return await runContextStorage.run(ctx, run);
2080
2612
  }
2081
2613
  function createRunBoundAdapter(handle, sessionId, ctx) {
2082
- const adapter = createPiAgentSessionAdapter(handle.piSession, {
2083
- sessionId,
2084
- ...handle.piSession.agent && typeof handle.piSession.agent.continue === "function" ? { continueQueuedFollowUp: () => handle.piSession.agent.continue() } : {}
2085
- });
2614
+ const adapter = createPiAgentSessionAdapter(handle.piSession, { sessionId });
2086
2615
  return {
2087
2616
  ...adapter,
2088
2617
  prompt: (promptInput) => bindRunContext(ctx, () => adapter.prompt(promptInput)),
@@ -2092,11 +2621,9 @@ function createPiCodingAgentHarness(opts) {
2092
2621
  };
2093
2622
  }
2094
2623
  async function createPiSession(sessionId, sessionCtx, input, ctx) {
2095
- const authStorage = AuthStorage.create();
2096
- const modelRegistry = ModelRegistry.create(authStorage);
2097
- registerConfiguredModelProviders(modelRegistry);
2098
- const resolvedModel = resolveRequestedModel(modelRegistry, input, { strict: pi.strictModelResolution });
2099
- 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);
2100
2627
  const savedPiFile = await sessionStore.loadPiSessionFile(sessionCtx, sessionId);
2101
2628
  const runtimeCwd = opts.runtimeCwd ?? ctx.workdir;
2102
2629
  if (!savedPiFile) {
@@ -2170,8 +2697,8 @@ function createPiCodingAgentHarness(opts) {
2170
2697
  model,
2171
2698
  thinkingLevel: input.thinkingLevel ?? "off",
2172
2699
  sessionManager,
2173
- authStorage,
2174
- modelRegistry,
2700
+ modelRuntime,
2701
+ settingsManager,
2175
2702
  ...resourceLoader ? { resourceLoader } : {}
2176
2703
  });
2177
2704
  const restoreFollowUpContextWrapper = rememberQueuedFollowUpRunContexts(piSession, runContextState, () => runContextStorage.getStore());
@@ -2182,7 +2709,7 @@ function createPiCodingAgentHarness(opts) {
2182
2709
  };
2183
2710
  const handle = {
2184
2711
  piSession,
2185
- modelRegistry,
2712
+ modelRuntime,
2186
2713
  sessionManager,
2187
2714
  resourceLoader,
2188
2715
  sessionId,
@@ -2244,7 +2771,8 @@ function createPiCodingAgentHarness(opts) {
2244
2771
  * the expected pre-first-turn state, not an error.
2245
2772
  */
2246
2773
  getSystemPrompt(sessionId) {
2247
- return piSessionHandlesFor(sessionId)[0]?.piSession.systemPrompt;
2774
+ const prompt = piSessionHandlesFor(sessionId)[0]?.piSession.systemPrompt;
2775
+ return prompt && pi.locateSkillResource ? projectSkillResourceLocations(prompt, pi.locateSkillResource) : prompt;
2248
2776
  },
2249
2777
  hasPiSession(sessionId, ctx) {
2250
2778
  return ctx ? piSessions.has(sessionCacheKey(sessionId, ctx)) : piSessionHandlesFor(sessionId).length > 0;
@@ -2316,8 +2844,8 @@ export {
2316
2844
  getEnv,
2317
2845
  PiSessionStore,
2318
2846
  parseEncodedModelSelection,
2319
- registerConfiguredModelProviders,
2320
2847
  readConfiguredDefaultModel,
2848
+ createConfiguredModelRuntime,
2321
2849
  PI_PACKAGE_RESOURCE_FILTERS,
2322
2850
  piPackageSourceKey,
2323
2851
  compactPiPackages,