@hachej/boring-agent 0.1.95 → 0.1.96

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 (45) hide show
  1. package/dist/DebugDrawer-2TO5SE2D.js +7 -0
  2. package/dist/{chunk-NITBKFG6.js → chunk-3OEDBRKY.js} +93 -58
  3. package/dist/{chunk-BOIV2I3N.js → chunk-46HJK2ES.js} +31 -16
  4. package/dist/{chunk-AQBXNPMD.js → chunk-4IQ4HHBG.js} +4 -0
  5. package/dist/{chunk-OFQ7J6SI.js → chunk-LSO2AO6Q.js} +57 -1
  6. package/dist/{chunk-2EMAKBVR.js → chunk-NSE2RHU3.js} +8967 -7740
  7. package/dist/{chunk-2GBHYJVC.js → chunk-VXF5DWS6.js} +34 -0
  8. package/dist/{chunk-2TMS2RU2.js → chunk-WC5KBGP2.js} +518 -165
  9. package/dist/chunk-YGRQ4Q5P.js +20 -0
  10. package/dist/core/index.d.ts +5 -5
  11. package/dist/core/index.js +604 -7
  12. package/dist/{createHarness-UCD74XXK.js → createHarness-PW3A2MRB.js} +3 -3
  13. package/dist/{errors-G6T6S7PQ.d.ts → errors-cNC4HMpV.d.ts} +14 -7
  14. package/dist/eval/index.d.ts +5 -3
  15. package/dist/eval/index.js +31 -23
  16. package/dist/file-search-QxnanNjO.d.ts +87 -0
  17. package/dist/front/index.d.ts +84 -16
  18. package/dist/front/index.js +832 -339
  19. package/dist/front/styles.css +1171 -2129
  20. package/dist/{harness-Cc48o6uS.d.ts → harness-DS8jLKIx.d.ts} +4 -2
  21. package/dist/{lease-CusB-uLg.d.ts → lease-D8WC6pQl.d.ts} +10 -89
  22. package/dist/{middleware-CzW8n7B7.d.ts → middleware-Cq_cYHjJ.d.ts} +1 -1
  23. package/dist/piChatEvent-B3ehh9V0.d.ts +384 -0
  24. package/dist/{piChatSessionService-C0KbcGw9.d.ts → piChatSessionService-xnv8rcdh.d.ts} +1 -1
  25. package/dist/{sandbox-DAD8cWQR.d.ts → sandbox-KQiEdz4W.d.ts} +12 -1
  26. package/dist/server/agent-host/testing/compositionRouteProof.d.ts +8 -0
  27. package/dist/server/agent-host/testing/compositionRouteProof.js +78 -0
  28. package/dist/server/agent-host/testing/gatewayConformance.d.ts +6 -6
  29. package/dist/server/agent-host/testing/gatewayConformance.js +17 -9
  30. package/dist/server/index.d.ts +569 -370
  31. package/dist/server/index.js +69 -17
  32. package/dist/server/worker/index.d.ts +2 -2
  33. package/dist/server/worker/index.js +9 -9
  34. package/dist/shared/index.d.ts +212 -56
  35. package/dist/shared/index.js +43 -7
  36. package/dist/{types-C-Eroi__.d.ts → types-D_SD-7_l.d.ts} +304 -87
  37. package/docs/AGENT_GATEWAY_V0.md +6 -3
  38. package/docs/AGENT_PACKAGES.md +80 -0
  39. package/docs/ERROR_CODES.md +58 -0
  40. package/docs/README.md +1 -0
  41. package/docs/runtime.md +19 -2
  42. package/package.json +34 -29
  43. package/dist/DebugDrawer-RYEBQ6CO.js +0 -6
  44. package/dist/chunk-EOMY2HDT.js +0 -619
  45. package/dist/piChatEvent-BsMp3MSo.d.ts +0 -368
@@ -1,22 +1,20 @@
1
1
  import {
2
+ SAFE_NATIVE_SESSION_ID,
2
3
  noopTelemetry,
3
4
  safeCapture
4
- } from "./chunk-AQBXNPMD.js";
5
+ } from "./chunk-4IQ4HHBG.js";
5
6
  import {
6
7
  ErrorCode
7
- } from "./chunk-2GBHYJVC.js";
8
+ } from "./chunk-VXF5DWS6.js";
8
9
 
9
10
  // src/server/harness/pi-coding-agent/createHarness.ts
10
- import { AsyncLocalStorage } from "async_hooks";
11
- import { existsSync, readFileSync as readFileSync2 } from "fs";
12
- import { extname, join as join2 } from "path";
11
+ import { AsyncLocalStorage } from "node:async_hooks";
13
12
  import {
14
13
  createAgentSession,
15
- SessionManager,
14
+ SessionManager as SessionManager2,
16
15
  AuthStorage,
17
16
  ModelRegistry,
18
17
  DefaultResourceLoader,
19
- SettingsManager as SettingsManager2,
20
18
  getAgentDir as getAgentDir2,
21
19
  loadSkills
22
20
  } from "@mariozechner/pi-coding-agent";
@@ -298,21 +296,21 @@ function createPiAgentSessionAdapter(session, options = {}) {
298
296
  }
299
297
 
300
298
  // src/server/harness/pi-coding-agent/sessions.ts
301
- import { randomUUID } from "crypto";
299
+ import { randomUUID } from "node:crypto";
302
300
  import {
303
301
  readdir,
304
302
  readFile,
305
- stat as fsStat,
303
+ stat as fsStat2,
306
304
  rm,
307
305
  mkdir,
308
306
  writeFile,
309
307
  appendFile,
310
308
  rename,
311
- open
312
- } from "fs/promises";
313
- import { closeSync, openSync, readFileSync, readSync, readdirSync, writeFileSync } from "fs";
314
- import { join, basename, resolve } from "path";
315
- import { homedir } from "os";
309
+ open as open4
310
+ } from "node:fs/promises";
311
+ import { closeSync, openSync, readFileSync, readSync, readdirSync, writeFileSync } from "node:fs";
312
+ import { join, basename, resolve } from "node:path";
313
+ import { homedir } from "node:os";
316
314
 
317
315
  // src/server/config/env.ts
318
316
  function getEnv(name) {
@@ -322,8 +320,253 @@ function getEnv(name) {
322
320
  // src/server/harness/pi-coding-agent/sessions.ts
323
321
  import {
324
322
  parseSessionEntries,
325
- CURRENT_SESSION_VERSION
323
+ CURRENT_SESSION_VERSION as CURRENT_SESSION_VERSION2
326
324
  } from "@mariozechner/pi-coding-agent";
325
+
326
+ // src/server/harness/pi-coding-agent/nativeSessionRename.ts
327
+ import { open as open2, stat as fsStat, utimes } from "node:fs/promises";
328
+ import {
329
+ CURRENT_SESSION_VERSION,
330
+ SessionManager
331
+ } from "@mariozechner/pi-coding-agent";
332
+
333
+ // src/server/harness/pi-coding-agent/sessionJsonlPrefix.ts
334
+ import { open } from "node:fs/promises";
335
+ var SUMMARY_PREFIX_BYTES = 64 * 1024;
336
+ async function readJsonlPrefix(filepath, maxBytes = SUMMARY_PREFIX_BYTES) {
337
+ const handle = await open(filepath, "r");
338
+ try {
339
+ const buffer = Buffer.alloc(maxBytes);
340
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
341
+ return completeJsonlPrefix(buffer.subarray(0, bytesRead).toString("utf-8"), bytesRead === maxBytes);
342
+ } finally {
343
+ await handle.close();
344
+ }
345
+ }
346
+ function parseJsonlPrefixEntries(content) {
347
+ const entries = [];
348
+ for (const line of content.split("\n")) {
349
+ if (!line.trim()) continue;
350
+ try {
351
+ entries.push(JSON.parse(line));
352
+ } catch {
353
+ }
354
+ }
355
+ return entries;
356
+ }
357
+ function completeJsonlPrefix(content, reachedLimit) {
358
+ if (!reachedLimit) return content;
359
+ const lastNewline = content.lastIndexOf("\n");
360
+ return lastNewline >= 0 ? content.slice(0, lastNewline + 1) : content;
361
+ }
362
+
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
+ // src/server/harness/pi-coding-agent/nativeSessionTranscript.ts
454
+ import { createReadStream } from "node:fs";
455
+ import { open as open3 } from "node:fs/promises";
456
+ import { createInterface } from "node:readline";
457
+
458
+ // src/server/harness/pi-coding-agent/piSessionMessages.ts
459
+ function textFromPiContent(content) {
460
+ if (typeof content === "string") return content;
461
+ if (!Array.isArray(content)) return "";
462
+ return content.map((part) => {
463
+ const item = part;
464
+ return item?.type === "text" && typeof item.text === "string" ? item.text : "";
465
+ }).join("");
466
+ }
467
+
468
+ // src/server/harness/pi-coding-agent/nativeSessionTranscript.ts
469
+ var NATIVE_TAIL_CHUNK_BYTES = 64 * 1024;
470
+ var NATIVE_TAIL_MAX_RECORD_BYTES = 256 * 1024;
471
+ var NATIVE_TAIL_MAX_RECORD_FRAGMENTS = 4;
472
+ async function latestNativeMessageTimestamp(filepath, size) {
473
+ const handle = await open3(filepath, "r");
474
+ let end = size;
475
+ let lineFragments = [];
476
+ try {
477
+ while (end > 0) {
478
+ const start = Math.max(0, end - NATIVE_TAIL_CHUNK_BYTES);
479
+ const chunk = Buffer.alloc(end - start);
480
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, start);
481
+ let lineEnd = bytesRead;
482
+ while (lineEnd > 0) {
483
+ const newline = chunk.lastIndexOf(10, lineEnd - 1);
484
+ if (newline < 0) break;
485
+ const timestamp = nativeMessageTimestampFromBoundedPrefix(
486
+ nativeTailRecordPrefix(chunk.subarray(newline + 1, lineEnd), lineFragments)
487
+ );
488
+ lineFragments = [];
489
+ if (timestamp !== void 0) return timestamp;
490
+ lineEnd = newline;
491
+ }
492
+ if (lineEnd > 0) {
493
+ lineFragments = retainNativeTailFragment(chunk.subarray(0, lineEnd), lineFragments);
494
+ }
495
+ end = start;
496
+ }
497
+ return nativeMessageTimestampFromBoundedPrefix(
498
+ nativeTailRecordPrefix(lineFragments.at(-1) ?? Buffer.alloc(0), lineFragments.slice(0, -1))
499
+ );
500
+ } finally {
501
+ await handle.close();
502
+ }
503
+ }
504
+ function retainNativeTailFragment(fragment, fragments) {
505
+ const next = [...fragments, fragment.subarray(0, NATIVE_TAIL_MAX_RECORD_BYTES)];
506
+ while (next.length > NATIVE_TAIL_MAX_RECORD_FRAGMENTS || nativeTailFragmentBytes(next) > NATIVE_TAIL_MAX_RECORD_BYTES) {
507
+ next.shift();
508
+ }
509
+ return next;
510
+ }
511
+ function nativeTailFragmentBytes(fragments) {
512
+ return fragments.reduce((total, fragment) => total + fragment.length, 0);
513
+ }
514
+ function nativeTailRecordPrefix(recordStart, followingFragments) {
515
+ const total = Math.min(
516
+ NATIVE_TAIL_MAX_RECORD_BYTES,
517
+ recordStart.length + nativeTailFragmentBytes(followingFragments)
518
+ );
519
+ const prefix = Buffer.allocUnsafe(total);
520
+ let offset = 0;
521
+ for (const fragment of [recordStart, ...followingFragments.slice().reverse()]) {
522
+ const length = Math.min(fragment.length, total - offset);
523
+ if (length <= 0) break;
524
+ fragment.copy(prefix, offset, 0, length);
525
+ offset += length;
526
+ }
527
+ return prefix;
528
+ }
529
+ function nativeMessageTimestampFromBoundedPrefix(prefix) {
530
+ if (prefix.length === 0) return void 0;
531
+ 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;
533
+ const timestampMatch = /"timestamp"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/.exec(line);
534
+ if (!timestampMatch) return void 0;
535
+ const timestamp = Date.parse(timestampMatch[1]);
536
+ return Number.isFinite(timestamp) ? timestamp : void 0;
537
+ }
538
+ async function summarizeNativeTranscript(filepath) {
539
+ let title;
540
+ let firstUserTitle;
541
+ let turnCount = 0;
542
+ let hasAssistantReply2 = false;
543
+ let latestMessageAtMs;
544
+ const input = createReadStream(filepath, { encoding: "utf-8" });
545
+ const lines = createInterface({ input, crlfDelay: Infinity });
546
+ for await (const line of lines) {
547
+ if (!line.trim()) continue;
548
+ 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
+ }
563
+ } catch {
564
+ }
565
+ }
566
+ return { title, firstUserTitle, turnCount, hasAssistantReply: hasAssistantReply2, latestMessageAtMs };
567
+ }
568
+
569
+ // src/server/harness/pi-coding-agent/sessions.ts
327
570
  function sessionBaseDir(explicitRoot) {
328
571
  const explicit = explicitRoot?.trim();
329
572
  if (explicit) return resolve(explicit);
@@ -335,10 +578,9 @@ function defaultSessionDir(cwd, explicitRoot) {
335
578
  const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
336
579
  return join(sessionBaseDir(explicitRoot), safePath);
337
580
  }
338
- var SAFE_ID = /^[a-zA-Z0-9_-]+$/;
339
581
  var SAFE_SESSION_NAMESPACE = /^[a-zA-Z0-9_-]+$/;
340
582
  var SESSION_ROOT_ENV = "BORING_AGENT_SESSION_ROOT";
341
- var SUMMARY_PREFIX_BYTES = 64 * 1024;
583
+ var SUMMARY_PREFIX_BYTES2 = 64 * 1024;
342
584
  var DEFAULT_LEGACY_WORKSPACE_ID = "default";
343
585
  var TRUSTED_LOCAL_USER_ID = "local";
344
586
  function sessionDirForNamespace(namespace, explicitRoot) {
@@ -352,7 +594,8 @@ function normalizeListOptions(options) {
352
594
  return {
353
595
  limit: options?.limit === void 0 ? void 0 : Math.max(0, options.limit),
354
596
  offset: Math.max(0, options?.offset ?? 0),
355
- includeId: options?.includeId
597
+ includeId: options?.includeId,
598
+ includeEmpty: options?.includeEmpty === true
356
599
  };
357
600
  }
358
601
  var PiSessionStore = class {
@@ -360,6 +603,7 @@ var PiSessionStore = class {
360
603
  sessionDir;
361
604
  allowLegacyUnscopedAccess;
362
605
  pathDerivedLegacyAccess;
606
+ trustedNativeRuntimeScopeIdentity;
363
607
  prefixCache = /* @__PURE__ */ new Map();
364
608
  listInFlight = /* @__PURE__ */ new Map();
365
609
  writerTails = /* @__PURE__ */ new Map();
@@ -369,10 +613,12 @@ var PiSessionStore = class {
369
613
  this.sessionDir = options;
370
614
  this.allowLegacyUnscopedAccess = true;
371
615
  this.pathDerivedLegacyAccess = false;
616
+ this.trustedNativeRuntimeScopeIdentity = void 0;
372
617
  return;
373
618
  }
374
619
  this.allowLegacyUnscopedAccess = true;
375
620
  this.pathDerivedLegacyAccess = options?.sessionDir === void 0 && options?.sessionNamespace === void 0;
621
+ this.trustedNativeRuntimeScopeIdentity = options?.trustedNativeRuntimeScopeIdentity?.trim() || void 0;
376
622
  this.sessionDir = options?.sessionDir ?? (options?.sessionNamespace ? sessionDirForNamespace(options.sessionNamespace, options.sessionRoot) : defaultSessionDir(options?.storageCwd ?? cwd, options?.sessionRoot));
377
623
  }
378
624
  getSessionDir() {
@@ -381,9 +627,10 @@ var PiSessionStore = class {
381
627
  /** Reads the Host execution pin from authoritative session metadata. */
382
628
  async readRuntimeScopeIdentity(ctx, sessionId) {
383
629
  const filepath = await this.resolveSessionFile(sessionId, ctx);
384
- const entries = parseJsonlPrefixEntries(await readJsonlPrefix(filepath));
630
+ const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath));
385
631
  const header = entries.find((entry) => entry.type === "session");
386
- if (!this.headerBelongsToCtx(header, ctx)) throw new Error(`Session not found: ${sessionId}`);
632
+ const directNative = isTimestampNamedPiSessionFile(filepath, header?.id ?? sessionId);
633
+ if (!this.headerBelongsToCtx(header, ctx, directNative)) throw new Error(`Session not found: ${sessionId}`);
387
634
  return readHeaderRuntimeScopeIdentity(header);
388
635
  }
389
636
  async list(ctx, options) {
@@ -393,7 +640,8 @@ var PiSessionStore = class {
393
640
  ctx.userId ?? null,
394
641
  normalizedOptions.limit ?? null,
395
642
  normalizedOptions.offset,
396
- normalizedOptions.includeId ?? null
643
+ normalizedOptions.includeId ?? null,
644
+ normalizedOptions.includeEmpty
397
645
  ]);
398
646
  const inFlight = this.listInFlight.get(inFlightKey);
399
647
  if (inFlight) return inFlight;
@@ -411,7 +659,7 @@ var PiSessionStore = class {
411
659
  const filepaths = jsonlFiles.map((f) => join(this.sessionDir, f));
412
660
  const fileStats = await Promise.all(filepaths.map(async (filepath) => {
413
661
  try {
414
- return { filepath, stat: await fsStat(filepath) };
662
+ return { filepath, stat: await fsStat2(filepath) };
415
663
  } catch {
416
664
  return null;
417
665
  }
@@ -424,8 +672,14 @@ var PiSessionStore = class {
424
672
  })));
425
673
  visibleFiles.sort((a, b) => b.sortMtimeMs - a.sortMtimeMs);
426
674
  const { offset, limit } = options;
427
- const pageSummaries = await this.summarizeVisiblePage(visibleFiles, { ctx, offset, limit });
428
675
  const includeId = options.includeId;
676
+ const pageSummaries = await this.summarizeVisiblePage(visibleFiles, {
677
+ ctx,
678
+ offset,
679
+ limit,
680
+ includeId,
681
+ includeEmpty: options.includeEmpty
682
+ });
429
683
  if (!includeId || pageSummaries.some((summary) => summary.id === includeId)) return pageSummaries;
430
684
  const includeSummary = await this.summarizeIncludedSession(ctx, includeId, referencedPiFiles);
431
685
  return includeSummary ? [...pageSummaries, includeSummary] : pageSummaries;
@@ -436,7 +690,7 @@ var PiSessionStore = class {
436
690
  const now = (/* @__PURE__ */ new Date()).toISOString();
437
691
  const header = {
438
692
  type: "session",
439
- version: CURRENT_SESSION_VERSION,
693
+ version: CURRENT_SESSION_VERSION2,
440
694
  id,
441
695
  timestamp: now,
442
696
  cwd: this.cwd,
@@ -453,27 +707,34 @@ var PiSessionStore = class {
453
707
  };
454
708
  lines.push(JSON.stringify(infoEntry));
455
709
  }
456
- const filepath = join(this.sessionDir, `${id}.jsonl`);
710
+ const filepath = join(this.sessionDir, `${nativeSessionFilename(id, now)}`);
457
711
  await writeFile(filepath, lines.join("\n") + "\n", "utf-8");
458
712
  return {
459
713
  id,
460
714
  title: init?.title ?? "New session",
461
715
  createdAt: now,
462
716
  updatedAt: now,
463
- turnCount: 0
717
+ turnCount: 0,
718
+ nativeSessionId: id,
719
+ hasAssistantReply: false
464
720
  };
465
721
  }
466
722
  async load(ctx, sessionId) {
467
723
  const resolved = await this.resolveSessionTranscript(ctx, sessionId);
468
- const title = newestDurableTitle(resolved.sessionEntries, resolved.linkedEntries) ?? "New session";
724
+ const nativeSummary = resolved.directNative ? await summarizeNativeTranscript(resolved.filepath) : null;
725
+ const title = newestDurableTitle(resolved.sessionEntries, resolved.linkedEntries) ?? nativeSummary?.title ?? nativeSummary?.firstUserTitle ?? "New session";
469
726
  const turnCount = countUserTurns(resolved.transcriptEntries);
470
- const updatedAtMs = Math.max(resolved.fileStat.mtime.getTime(), resolved.linkedMtimeMs ?? 0);
727
+ const updatedAtMs = nativeSummary?.latestMessageAtMs ?? Math.max(resolved.fileStat.mtime.getTime(), resolved.linkedMtimeMs ?? 0);
471
728
  return {
472
729
  id: resolved.resolvedSessionId,
473
730
  title,
474
731
  createdAt: resolved.header?.timestamp ?? resolved.fileStat.birthtime.toISOString(),
475
732
  updatedAt: new Date(updatedAtMs).toISOString(),
476
- turnCount
733
+ turnCount,
734
+ ...resolved.directNative ? {
735
+ nativeSessionId: resolved.resolvedSessionId,
736
+ hasAssistantReply: hasAssistantReply(resolved.transcriptEntries)
737
+ } : {}
477
738
  };
478
739
  }
479
740
  /**
@@ -508,7 +769,14 @@ var PiSessionStore = class {
508
769
  throw new Error(`Session not found: ${sessionId}`);
509
770
  }
510
771
  const fileEntries = safeParseEntries(content);
511
- if (fileEntries.some((e) => e.type === "ui_snapshot")) {
772
+ const header = fileEntries.find(
773
+ (e) => e.type === "session"
774
+ );
775
+ const timestampNamedNative = isTimestampNamedPiSessionFile(
776
+ filepath,
777
+ header?.id ?? sessionId
778
+ );
779
+ if (!timestampNamedNative && fileEntries.some((e) => e.type === "ui_snapshot")) {
512
780
  const compacted = fileEntries.filter((e) => e.type !== "ui_snapshot").map((e) => JSON.stringify(e)).join("\n") + "\n";
513
781
  const tmp = `${filepath}.compact-${randomUUID()}`;
514
782
  try {
@@ -519,15 +787,15 @@ var PiSessionStore = class {
519
787
  });
520
788
  }
521
789
  }
522
- const header = fileEntries.find(
523
- (e) => e.type === "session"
524
- );
525
- if (!this.headerBelongsToCtx(header, ctx)) throw new Error(`Session not found: ${sessionId}`);
790
+ if (!this.headerBelongsToCtx(header, ctx, timestampNamedNative)) {
791
+ throw new Error(`Session not found: ${sessionId}`);
792
+ }
526
793
  const sessionEntries = fileEntries.filter(
527
794
  (e) => e.type !== "session" && e.type !== "ui_snapshot"
528
795
  );
529
- const fileStat = await fsStat(filepath);
796
+ const fileStat = await fsStat2(filepath);
530
797
  const linkedPiFile = extractPiSessionFilePath(fileEntries);
798
+ const directNative = timestampNamedNative && !linkedPiFile;
531
799
  const linked = linkedPiFile && resolve(linkedPiFile) !== resolve(filepath) ? await this.readLinkedPiSession(linkedPiFile) : null;
532
800
  const linkedEntries = linked?.entries.filter(
533
801
  (e) => e.type !== "session"
@@ -542,7 +810,8 @@ var PiSessionStore = class {
542
810
  fileStat,
543
811
  linkedMtimeMs: linked?.mtime.getTime(),
544
812
  filepath,
545
- ...linkedPiFile && linked ? { linkedFilepath: linkedPiFile } : {}
813
+ ...linkedPiFile && linked ? { linkedFilepath: linkedPiFile } : {},
814
+ directNative
546
815
  };
547
816
  }
548
817
  /**
@@ -550,10 +819,23 @@ var PiSessionStore = class {
550
819
  * appended through the same JSONL path under one process-local writer lock.
551
820
  */
552
821
  async rename(ctx, sessionId, title) {
553
- const trimmed = title.trim();
822
+ const trimmed = title.replace(/[\r\n]+/g, " ").trim();
554
823
  if (!trimmed) throw new Error("Session title must not be empty");
555
824
  return await this.withWriter(sessionId, async () => {
556
825
  const resolved = await this.resolveSessionTranscript(ctx, sessionId);
826
+ if (resolved.directNative) {
827
+ try {
828
+ await appendVerifiedNativeRename(
829
+ resolved.filepath,
830
+ this.sessionDir,
831
+ this.cwd,
832
+ trimmed
833
+ );
834
+ } finally {
835
+ this.prefixCache.delete(resolved.filepath);
836
+ }
837
+ return await this.load(ctx, sessionId);
838
+ }
557
839
  const entry = {
558
840
  type: "session_info",
559
841
  id: randomUUID(),
@@ -591,7 +873,7 @@ var PiSessionStore = class {
591
873
  // I/O hop is introduced before createAgentSession (which would break test
592
874
  // timing when fake timers are in use). The file is tiny (metadata only).
593
875
  loadPiSessionFileSync(ctx, sessionId) {
594
- if (!SAFE_ID.test(sessionId)) return null;
876
+ if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) return null;
595
877
  try {
596
878
  const direct = join(this.sessionDir, `${sessionId}.jsonl`);
597
879
  let filepath = direct;
@@ -608,13 +890,17 @@ var PiSessionStore = class {
608
890
  }
609
891
  const entries = safeParseEntries(content);
610
892
  const header = entries.find((entry) => entry.type === "session");
611
- if (!this.headerBelongsToCtx(header, ctx)) return null;
893
+ const directNative = isTimestampNamedPiSessionFile(filepath, header?.id ?? sessionId);
612
894
  const linkedPiFile = extractPiSessionFilePath(entries);
613
- if (linkedPiFile) return linkedPiFile;
614
- if (!isTimestampNamedPiSessionFile(filepath, sessionId)) return null;
895
+ if (!directNative) {
896
+ if (!this.headerBelongsToCtx(header, ctx)) return null;
897
+ return linkedPiFile;
898
+ }
899
+ if (this.nativeFileBelongsToCtx(header, ctx)) return filepath;
900
+ if (readHeaderSessionCtx(header) !== null) return null;
615
901
  const existingWrapper = this.findWrapperReferencingNativeSessionSync(filepath);
616
902
  if (existingWrapper) {
617
- const existingEntries = parseJsonlPrefixEntries(readJsonlPrefixSync(existingWrapper));
903
+ const existingEntries = parseJsonlPrefixEntries2(readJsonlPrefixSync(existingWrapper));
618
904
  if (extractSessionHeaderId(existingEntries) !== sessionId) return null;
619
905
  const wrapperHeader = existingEntries.find((entry) => entry.type === "session");
620
906
  if (!this.headerBelongsToCtx(wrapperHeader, ctx)) return null;
@@ -627,7 +913,7 @@ var PiSessionStore = class {
627
913
  }
628
914
  }
629
915
  async loadPiSessionFile(ctx, sessionId) {
630
- if (!SAFE_ID.test(sessionId)) return null;
916
+ if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) return null;
631
917
  try {
632
918
  const direct = join(this.sessionDir, `${sessionId}.jsonl`);
633
919
  let filepath = direct;
@@ -645,20 +931,25 @@ var PiSessionStore = class {
645
931
  }
646
932
  const entries = safeParseEntries(content);
647
933
  const header = entries.find((entry) => entry.type === "session");
648
- if (!this.headerBelongsToCtx(header, ctx)) return null;
934
+ const directNative = isTimestampNamedPiSessionFile(filepath, header?.id ?? sessionId);
649
935
  const linkedPiFile = extractPiSessionFilePath(entries);
650
- if (linkedPiFile) return linkedPiFile;
651
- if (!isTimestampNamedPiSessionFile(filepath, sessionId)) return null;
936
+ if (!directNative) {
937
+ if (!this.headerBelongsToCtx(header, ctx)) return null;
938
+ return linkedPiFile;
939
+ }
940
+ if (this.nativeFileBelongsToCtx(header, ctx)) return filepath;
941
+ if (readHeaderSessionCtx(header) !== null) return null;
652
942
  const existingWrapper = await this.findWrapperReferencingNativeSession(filepath);
653
943
  if (existingWrapper) {
654
944
  const wrapperSessionId = await this.readSessionFileId(existingWrapper);
655
945
  if (wrapperSessionId !== sessionId) return null;
656
- const wrapperEntries = parseJsonlPrefixEntries(await readJsonlPrefix(existingWrapper));
946
+ const wrapperEntries = parseJsonlPrefixEntries2(await readJsonlPrefix2(existingWrapper));
657
947
  const wrapperHeader = wrapperEntries.find((entry) => entry.type === "session");
658
948
  if (!this.headerBelongsToCtx(wrapperHeader, ctx)) return null;
659
949
  return extractPiSessionFilePath(wrapperEntries);
660
950
  }
661
- return await this.ensureWrapperForNativeSession(sessionId, filepath, ctx);
951
+ await this.ensureWrapperForNativeSession(sessionId, filepath, ctx);
952
+ return filepath;
662
953
  } catch {
663
954
  return null;
664
955
  }
@@ -688,12 +979,12 @@ var PiSessionStore = class {
688
979
  }
689
980
  }
690
981
  async resolveSessionFile(sessionId, ctx) {
691
- if (!SAFE_ID.test(sessionId)) {
982
+ if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) {
692
983
  throw new Error(`Session not found: ${sessionId}`);
693
984
  }
694
985
  const direct = join(this.sessionDir, `${sessionId}.jsonl`);
695
986
  try {
696
- await fsStat(direct);
987
+ await fsStat2(direct);
697
988
  if (ctx) await this.assertFileBelongsToCtx(direct, ctx, sessionId);
698
989
  return direct;
699
990
  } catch {
@@ -708,26 +999,24 @@ var PiSessionStore = class {
708
999
  if (ctx) await this.assertFileBelongsToCtx(matchedPath, ctx, sessionId);
709
1000
  return matchedPath;
710
1001
  }
711
- const existingWrapper = await this.findWrapperReferencingNativeSession(matchedPath);
712
- if (existingWrapper) {
713
- const wrapperSessionId = await this.readSessionFileId(existingWrapper);
714
- if (wrapperSessionId === sessionId) {
715
- if (ctx) await this.assertFileBelongsToCtx(existingWrapper, ctx, sessionId);
716
- return existingWrapper;
717
- }
718
- throw new Error(`Session not found: ${sessionId}`);
1002
+ if (ctx && (this.pathDerivedLegacyAccess || this.hasTrustedNativeRuntimeCapability(ctx) || await this.nativeFilePin(matchedPath) !== null)) {
1003
+ await this.assertFileBelongsToCtx(matchedPath, ctx, sessionId);
1004
+ return matchedPath;
719
1005
  }
720
- if (ctx) await this.assertFileBelongsToCtx(matchedPath, ctx, sessionId);
721
- return this.ensureWrapperForNativeSession(sessionId, matchedPath, ctx);
1006
+ if (!ctx && this.pathDerivedLegacyAccess) return matchedPath;
1007
+ throw new Error(`Session not found: ${sessionId}`);
722
1008
  }
723
1009
  async assertFileBelongsToCtx(filepath, ctx, sessionId) {
724
- const entries = parseJsonlPrefixEntries(await readJsonlPrefix(filepath));
1010
+ const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath));
725
1011
  const header = entries.find((entry) => entry.type === "session");
726
- if (!this.headerBelongsToCtx(header, ctx)) throw new Error(`Session not found: ${sessionId}`);
1012
+ const directNative = !extractPiSessionFilePath(entries) && isTimestampNamedPiSessionFile(filepath, header?.id ?? sessionId);
1013
+ if (!this.headerBelongsToCtx(header, ctx, directNative)) {
1014
+ throw new Error(`Session not found: ${sessionId}`);
1015
+ }
727
1016
  }
728
1017
  async readSessionFileId(filepath) {
729
1018
  try {
730
- const entries = parseJsonlPrefixEntries(await readJsonlPrefix(filepath));
1019
+ const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath));
731
1020
  return extractSessionHeaderId(entries);
732
1021
  } catch {
733
1022
  return null;
@@ -759,21 +1048,25 @@ var PiSessionStore = class {
759
1048
  try {
760
1049
  const linkedPiFile = (await this.readPrefixCache(filepath, stat)).referencedPiFile;
761
1050
  if (linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)) {
762
- const linkedStat = await fsStat(linkedPiFile);
1051
+ const linkedStat = await fsStat2(linkedPiFile);
763
1052
  sortMtimeMs = Math.max(sortMtimeMs, linkedStat.mtime.getTime());
764
1053
  }
1054
+ const header = parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath)).find((entry) => entry.type === "session");
1055
+ if (header && isTimestampNamedPiSessionFile(filepath, header.id)) {
1056
+ sortMtimeMs = await latestNativeMessageTimestamp(filepath, Number(stat.size)) ?? sortMtimeMs;
1057
+ }
765
1058
  } catch {
766
1059
  }
767
1060
  return sortMtimeMs;
768
1061
  }
769
1062
  async summarizeFile(ctx, filepath, existingStat) {
770
1063
  try {
771
- const fileStat = existingStat ?? await fsStat(filepath);
1064
+ const fileStat = existingStat ?? await fsStat2(filepath);
772
1065
  const cached = this.cachedPrefix(filepath, fileStat);
773
1066
  if (cached && "summary" in cached && cached.sessionCtx !== void 0 && this.storedCtxBelongsToCtx(cached.sessionCtx, ctx) && await this.cachedSummaryIsFresh(filepath, cached)) {
774
1067
  return cached.summary ?? null;
775
1068
  }
776
- const content = await readJsonlPrefix(filepath);
1069
+ const content = await readJsonlPrefix2(filepath);
777
1070
  const firstNewline = content.indexOf("\n");
778
1071
  if (firstNewline === -1) return null;
779
1072
  const header = JSON.parse(
@@ -781,8 +1074,9 @@ var PiSessionStore = class {
781
1074
  );
782
1075
  if (header.type !== "session") return null;
783
1076
  const sessionCtx = readHeaderSessionCtx(header);
784
- if (!this.storedCtxBelongsToCtx(sessionCtx, ctx)) return null;
785
- const entries = parseJsonlPrefixEntries(content);
1077
+ const directNative = isTimestampNamedPiSessionFile(filepath, header.id);
1078
+ if (directNative ? !this.nativeFileBelongsToCtx(header, ctx) : !this.storedCtxBelongsToCtx(sessionCtx, ctx)) return null;
1079
+ const entries = parseJsonlPrefixEntries2(content);
786
1080
  const sessionEntries = entries.filter(
787
1081
  (e) => e.type !== "session"
788
1082
  );
@@ -791,17 +1085,22 @@ var PiSessionStore = class {
791
1085
  const linkedEntries = linked?.entries.filter(
792
1086
  (e) => e.type !== "session"
793
1087
  ) ?? [];
794
- const title = newestDurableTitle(sessionEntries, linkedEntries) ?? firstUserMessage(linkedEntries) ?? firstUserMessage(sessionEntries) ?? "New session";
795
- const turnCount = [...sessionEntries, ...linkedEntries].filter(
1088
+ const nativeSummary = directNative ? await summarizeNativeTranscript(filepath) : null;
1089
+ const title = newestDurableTitle(sessionEntries, linkedEntries) ?? nativeSummary?.title ?? nativeSummary?.firstUserTitle ?? firstUserMessage(linkedEntries) ?? firstUserMessage(sessionEntries) ?? "New session";
1090
+ const turnCount = nativeSummary?.turnCount ?? [...sessionEntries, ...linkedEntries].filter(
796
1091
  (e) => e.type === "message" && e.message?.role === "user"
797
1092
  ).length;
798
- const updatedAtMs = Math.max(fileStat.mtime.getTime(), linked?.mtime.getTime() ?? 0);
1093
+ const updatedAtMs = nativeSummary?.latestMessageAtMs ?? Math.max(fileStat.mtime.getTime(), linked?.mtime.getTime() ?? 0);
799
1094
  const summary = {
800
1095
  id: header.id,
801
1096
  title,
802
1097
  createdAt: header.timestamp,
803
1098
  updatedAt: new Date(updatedAtMs).toISOString(),
804
- turnCount
1099
+ turnCount,
1100
+ ...nativeSummary ? {
1101
+ nativeSessionId: header.id,
1102
+ hasAssistantReply: nativeSummary.hasAssistantReply
1103
+ } : {}
805
1104
  };
806
1105
  this.prefixCache.set(filepath, {
807
1106
  mtimeMs: fileStat.mtime.getTime(),
@@ -809,7 +1108,7 @@ var PiSessionStore = class {
809
1108
  referencedPiFile: linkedPiFile,
810
1109
  sessionCtx,
811
1110
  ...linked ? { linkedMtimeMs: linked.mtime.getTime(), linkedSize: linked.size } : {},
812
- summary
1111
+ ...!nativeSummary ? { summary } : {}
813
1112
  });
814
1113
  return summary;
815
1114
  } catch {
@@ -826,7 +1125,7 @@ var PiSessionStore = class {
826
1125
  const linkedPiFile = cached.referencedPiFile;
827
1126
  if (!linkedPiFile || resolve(linkedPiFile) === resolve(filepath)) return true;
828
1127
  try {
829
- const linkedStat = await fsStat(linkedPiFile);
1128
+ const linkedStat = await fsStat2(linkedPiFile);
830
1129
  return cached.linkedMtimeMs === linkedStat.mtime.getTime() && cached.linkedSize === Number(linkedStat.size);
831
1130
  } catch {
832
1131
  return cached.linkedMtimeMs === void 0 && cached.linkedSize === void 0;
@@ -835,8 +1134,8 @@ var PiSessionStore = class {
835
1134
  async readPrefixCache(filepath, fileStat) {
836
1135
  const cached = this.cachedPrefix(filepath, fileStat);
837
1136
  if (cached) return cached;
838
- const content = await readJsonlPrefix(filepath);
839
- const entries = parseJsonlPrefixEntries(content);
1137
+ const content = await readJsonlPrefix2(filepath);
1138
+ const entries = parseJsonlPrefixEntries2(content);
840
1139
  const entry = {
841
1140
  mtimeMs: fileStat.mtime.getTime(),
842
1141
  size: Number(fileStat.size),
@@ -860,6 +1159,9 @@ var PiSessionStore = class {
860
1159
  );
861
1160
  for (const summary of summaries) {
862
1161
  if (!summary) continue;
1162
+ if (summary.turnCount === 0 && !options.includeEmpty && summary.id !== options.includeId) {
1163
+ continue;
1164
+ }
863
1165
  if (validSeen < options.offset) {
864
1166
  validSeen += 1;
865
1167
  continue;
@@ -888,7 +1190,7 @@ var PiSessionStore = class {
888
1190
  const filepath = join(this.sessionDir, file);
889
1191
  if (resolve(filepath) === resolvedNativePath) continue;
890
1192
  try {
891
- const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries(readJsonlPrefixSync(filepath)));
1193
+ const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries2(readJsonlPrefixSync(filepath)));
892
1194
  if (linkedPiFile && resolve(linkedPiFile) === resolvedNativePath) return filepath;
893
1195
  } catch {
894
1196
  }
@@ -906,7 +1208,7 @@ var PiSessionStore = class {
906
1208
  const filepath = join(this.sessionDir, file);
907
1209
  if (resolve(filepath) === resolvedNativePath) continue;
908
1210
  try {
909
- const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries(await readJsonlPrefix(filepath)));
1211
+ const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath)));
910
1212
  if (linkedPiFile && resolve(linkedPiFile) === resolvedNativePath) return filepath;
911
1213
  } catch {
912
1214
  }
@@ -937,11 +1239,11 @@ var PiSessionStore = class {
937
1239
  const wrapperPath = join(this.sessionDir, `${sessionId}.jsonl`);
938
1240
  if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
939
1241
  try {
940
- await fsStat(wrapperPath);
1242
+ await fsStat2(wrapperPath);
941
1243
  return wrapperPath;
942
1244
  } catch {
943
1245
  }
944
- const entries = parseJsonlPrefixEntries(await readJsonlPrefix(nativePath));
1246
+ const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(nativePath));
945
1247
  try {
946
1248
  await writeFile(
947
1249
  wrapperPath,
@@ -957,7 +1259,7 @@ var PiSessionStore = class {
957
1259
  async readLinkedPiSession(filepath) {
958
1260
  try {
959
1261
  const [fileStat, content] = await Promise.all([
960
- fsStat(filepath),
1262
+ fsStat2(filepath),
961
1263
  readFile(filepath, "utf-8")
962
1264
  ]);
963
1265
  return { entries: safeParseEntries(content), mtime: fileStat.mtime, size: Number(fileStat.size) };
@@ -968,17 +1270,45 @@ var PiSessionStore = class {
968
1270
  async readLinkedPiSessionSummary(filepath) {
969
1271
  try {
970
1272
  const [fileStat, content] = await Promise.all([
971
- fsStat(filepath),
972
- readJsonlPrefix(filepath)
1273
+ fsStat2(filepath),
1274
+ readJsonlPrefix2(filepath)
973
1275
  ]);
974
- return { entries: parseJsonlPrefixEntries(content), mtime: fileStat.mtime, size: Number(fileStat.size) };
1276
+ return { entries: parseJsonlPrefixEntries2(content), mtime: fileStat.mtime, size: Number(fileStat.size) };
975
1277
  } catch {
976
1278
  return null;
977
1279
  }
978
1280
  }
979
- headerBelongsToCtx(header, ctx) {
1281
+ headerBelongsToCtx(header, ctx, directNative = false) {
1282
+ if (directNative) return this.nativeFileBelongsToCtx(header, ctx);
980
1283
  return header ? this.storedCtxBelongsToCtx(readHeaderSessionCtx(header), ctx) : isEmptySessionCtx(ctx);
981
1284
  }
1285
+ /**
1286
+ * A bare Pi transcript carries no tenancy, so only a path-derived trusted
1287
+ * local store may reach it. Boring-created native transcripts carry an exact
1288
+ * persisted tenancy/runtime pin and remain reachable by hosted/namespaced
1289
+ * stores without minting a compatibility wrapper.
1290
+ */
1291
+ nativeFileBelongsToCtx(header, ctx) {
1292
+ const pinned = readHeaderSessionCtx(header);
1293
+ if (pinned === null && this.hasTrustedNativeRuntimeCapability(ctx)) return true;
1294
+ if (this.pathDerivedLegacyAccess) return this.storedCtxBelongsToCtx(pinned, ctx);
1295
+ return pinned !== null && sameSessionCtx(pinned, ctx);
1296
+ }
1297
+ hasTrustedNativeRuntimeCapability(ctx) {
1298
+ const runtimeScopeIdentity = ctx.runtimeScopeIdentity?.trim();
1299
+ return Boolean(
1300
+ runtimeScopeIdentity && this.trustedNativeRuntimeScopeIdentity && runtimeScopeIdentity === this.trustedNativeRuntimeScopeIdentity
1301
+ );
1302
+ }
1303
+ /** The Boring tenancy pin on a native transcript, or null when unpinned. */
1304
+ async nativeFilePin(filepath) {
1305
+ try {
1306
+ const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath));
1307
+ return readHeaderSessionCtx(entries.find((entry) => entry.type === "session"));
1308
+ } catch {
1309
+ return null;
1310
+ }
1311
+ }
982
1312
  storedCtxBelongsToCtx(storedCtx, ctx) {
983
1313
  const trustedLocalCtx = !ctx.userId || ctx.userId === TRUSTED_LOCAL_USER_ID;
984
1314
  if (storedCtx === null) {
@@ -992,8 +1322,8 @@ var PiSessionStore = class {
992
1322
  );
993
1323
  }
994
1324
  };
995
- async function readJsonlPrefix(filepath, maxBytes = SUMMARY_PREFIX_BYTES) {
996
- const handle = await open(filepath, "r");
1325
+ async function readJsonlPrefix2(filepath, maxBytes = SUMMARY_PREFIX_BYTES2) {
1326
+ const handle = await open4(filepath, "r");
997
1327
  try {
998
1328
  const buffer = Buffer.alloc(maxBytes);
999
1329
  const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
@@ -1007,7 +1337,7 @@ async function readJsonlPrefix(filepath, maxBytes = SUMMARY_PREFIX_BYTES) {
1007
1337
  await handle.close();
1008
1338
  }
1009
1339
  }
1010
- function readJsonlPrefixSync(filepath, maxBytes = SUMMARY_PREFIX_BYTES) {
1340
+ function readJsonlPrefixSync(filepath, maxBytes = SUMMARY_PREFIX_BYTES2) {
1011
1341
  const fd = openSync(filepath, "r");
1012
1342
  try {
1013
1343
  const buffer = Buffer.alloc(maxBytes);
@@ -1067,7 +1397,7 @@ function buildNativePiSessionWrapper(sessionId, cwd, piFilePath, entries, ctx) {
1067
1397
  const timestamp = nativeHeader?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
1068
1398
  const header = {
1069
1399
  type: "session",
1070
- version: CURRENT_SESSION_VERSION,
1400
+ version: CURRENT_SESSION_VERSION2,
1071
1401
  id: sessionId,
1072
1402
  timestamp,
1073
1403
  cwd: nativeHeader?.cwd ?? cwd,
@@ -1086,14 +1416,23 @@ function extractSessionHeaderId(entries) {
1086
1416
  const header = entries.find((entry) => entry.type === "session");
1087
1417
  return header?.id ?? null;
1088
1418
  }
1419
+ function nativeSessionFilename(sessionId, isoTimestamp) {
1420
+ return `${isoTimestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`;
1421
+ }
1089
1422
  function isTimestampNamedPiSessionFile(filepath, sessionId) {
1090
- return basename(filepath).endsWith(`_${sessionId}.jsonl`);
1423
+ const filename = basename(filepath);
1424
+ return /^\d{4}-\d{2}-\d{2}/.test(filename) && filename.endsWith(`_${sessionId}.jsonl`);
1091
1425
  }
1092
1426
  function countUserTurns(entries) {
1093
1427
  return entries.filter(
1094
1428
  (e) => e.type === "message" && e.message?.role === "user"
1095
1429
  ).length;
1096
1430
  }
1431
+ function hasAssistantReply(entries) {
1432
+ return entries.some(
1433
+ (entry) => entry.type === "message" && entry.message.role === "assistant"
1434
+ );
1435
+ }
1097
1436
  function newestDurableTitle(wrapperEntries, nativeEntries) {
1098
1437
  const candidates = [
1099
1438
  ...wrapperEntries.flatMap((entry, offset) => entry.type === "session_info" ? [{ entry, native: 0, offset }] : []),
@@ -1110,7 +1449,7 @@ function firstUserMessage(entries) {
1110
1449
  if (e.type !== "message") continue;
1111
1450
  const msg = e.message;
1112
1451
  if (msg?.role !== "user") continue;
1113
- const text = textFromPiContent(msg.content);
1452
+ const text = textFromPiContent2(msg.content);
1114
1453
  if (text) return text.slice(0, 80);
1115
1454
  }
1116
1455
  }
@@ -1129,7 +1468,7 @@ function safeParseEntries(content) {
1129
1468
  return results;
1130
1469
  }
1131
1470
  }
1132
- function parseJsonlPrefixEntries(content) {
1471
+ function parseJsonlPrefixEntries2(content) {
1133
1472
  const entries = [];
1134
1473
  for (const line of content.split("\n")) {
1135
1474
  if (!line.trim()) continue;
@@ -1140,7 +1479,7 @@ function parseJsonlPrefixEntries(content) {
1140
1479
  }
1141
1480
  return entries;
1142
1481
  }
1143
- function textFromPiContent(content) {
1482
+ function textFromPiContent2(content) {
1144
1483
  if (typeof content === "string") return content;
1145
1484
  if (!Array.isArray(content)) return "";
1146
1485
  return content.map((part) => {
@@ -1421,6 +1760,43 @@ function mergePiPackageSources(base = [], additional = []) {
1421
1760
  return compactPiPackages([...base, ...additional]);
1422
1761
  }
1423
1762
 
1763
+ // src/server/harness/pi-coding-agent/resourceSettingsManager.ts
1764
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1765
+ import { join as join2 } from "node:path";
1766
+ import { SettingsManager as SettingsManager2 } from "@mariozechner/pi-coding-agent";
1767
+ function readSettingsFileIfPresent(path) {
1768
+ return existsSync(path) ? readFileSync2(path, "utf-8") : void 0;
1769
+ }
1770
+ function mergeInjectedProjectPackages(settingsJson, piPackages, includePackage) {
1771
+ const settings = settingsJson ? JSON.parse(settingsJson) : {};
1772
+ const configuredPackages = Array.isArray(settings.packages) ? settings.packages : [];
1773
+ return JSON.stringify({
1774
+ ...settings,
1775
+ packages: mergePiPackageSources(configuredPackages, piPackages).filter((source) => includePackage?.(source) ?? true)
1776
+ });
1777
+ }
1778
+ function createResourceSettingsManager(cwd, agentDir, piPackages, options = {}) {
1779
+ if (piPackages.length === 0 && !options.includePackage) return SettingsManager2.create(cwd, agentDir);
1780
+ const globalSettingsPath = join2(agentDir, "settings.json");
1781
+ const projectSettingsPath = join2(cwd, ".pi", "settings.json");
1782
+ let globalSettingsOverrideJson;
1783
+ let projectSettingsOverrideJson;
1784
+ const storage = {
1785
+ withLock(scope, fn) {
1786
+ if (scope === "global") {
1787
+ const current2 = globalSettingsOverrideJson ?? mergeInjectedProjectPackages(readSettingsFileIfPresent(globalSettingsPath), [], options.includePackage);
1788
+ const next2 = fn(current2);
1789
+ if (next2 !== void 0) globalSettingsOverrideJson = next2;
1790
+ return;
1791
+ }
1792
+ const current = projectSettingsOverrideJson ?? mergeInjectedProjectPackages(readSettingsFileIfPresent(projectSettingsPath), piPackages, options.includePackage);
1793
+ const next = fn(current);
1794
+ if (next !== void 0) projectSettingsOverrideJson = next;
1795
+ }
1796
+ };
1797
+ return SettingsManager2.fromStorage(storage);
1798
+ }
1799
+
1424
1800
  // src/server/harness/pi-coding-agent/createHarness.ts
1425
1801
  var WORKSPACE_PATHS_GUIDELINE = [
1426
1802
  "## Workspace paths",
@@ -1520,52 +1896,20 @@ function resolveDefaultModel(modelRegistry, override, strict) {
1520
1896
  return void 0;
1521
1897
  }
1522
1898
  function sessionCtxForInput(input, ctx) {
1523
- return normalizeSessionCtx2(input.ctx ?? { workspaceId: ctx.workspaceId }) ?? {};
1899
+ return normalizeSessionCtx2(input.ctx ?? ctx.sessionCtx ?? { workspaceId: ctx.workspaceId }) ?? {};
1524
1900
  }
1525
1901
  function sessionCtxFromRunContext(ctx) {
1526
- return normalizeSessionCtx2({ workspaceId: ctx.workspaceId, userId: ctx.userId }) ?? {};
1902
+ return normalizeSessionCtx2(ctx.sessionCtx ?? { workspaceId: ctx.workspaceId }) ?? {};
1527
1903
  }
1528
1904
  function normalizeSessionCtx2(ctx) {
1529
- if (!ctx?.workspaceId && !ctx?.userId) return void 0;
1905
+ if (!ctx?.workspaceId && !ctx?.runtimeScopeIdentity) return void 0;
1530
1906
  return {
1531
1907
  ...ctx.workspaceId ? { workspaceId: ctx.workspaceId } : {},
1532
- ...ctx.userId ? { userId: ctx.userId } : {}
1908
+ ...ctx.runtimeScopeIdentity ? { runtimeScopeIdentity: ctx.runtimeScopeIdentity } : {}
1533
1909
  };
1534
1910
  }
1535
1911
  function sessionCacheKey(sessionId, ctx) {
1536
- return JSON.stringify([sessionId, ctx.workspaceId ?? "", ctx.userId ?? ""]);
1537
- }
1538
- function readSettingsFileIfPresent(path) {
1539
- return existsSync(path) ? readFileSync2(path, "utf-8") : void 0;
1540
- }
1541
- function mergeInjectedProjectPackages(settingsJson, piPackages) {
1542
- const settings = settingsJson ? JSON.parse(settingsJson) : {};
1543
- const configuredPackages = Array.isArray(settings.packages) ? settings.packages : [];
1544
- return JSON.stringify({
1545
- ...settings,
1546
- packages: mergePiPackageSources(configuredPackages, piPackages)
1547
- });
1548
- }
1549
- function createResourceSettingsManager(cwd, agentDir, piPackages) {
1550
- if (piPackages.length === 0) return SettingsManager2.create(cwd, agentDir);
1551
- const globalSettingsPath = join2(agentDir, "settings.json");
1552
- const projectSettingsPath = join2(cwd, ".pi", "settings.json");
1553
- let globalSettingsOverrideJson;
1554
- let projectSettingsOverrideJson;
1555
- const storage = {
1556
- withLock(scope, fn) {
1557
- if (scope === "global") {
1558
- const current2 = globalSettingsOverrideJson ?? readSettingsFileIfPresent(globalSettingsPath);
1559
- const next2 = fn(current2);
1560
- if (next2 !== void 0) globalSettingsOverrideJson = next2;
1561
- return;
1562
- }
1563
- const current = projectSettingsOverrideJson ?? mergeInjectedProjectPackages(readSettingsFileIfPresent(projectSettingsPath), piPackages);
1564
- const next = fn(current);
1565
- if (next !== void 0) projectSettingsOverrideJson = next;
1566
- }
1567
- };
1568
- return SettingsManager2.fromStorage(storage);
1912
+ return JSON.stringify([sessionId, ctx.workspaceId ?? "", ctx.runtimeScopeIdentity ?? ""]);
1569
1913
  }
1570
1914
  async function applyRequestedSessionOptions(handle, input, options = {}) {
1571
1915
  const requestedModel = resolveRequestedModel(handle.modelRegistry, input, { strict: options.strictModelResolution });
@@ -1662,6 +2006,8 @@ function createPiCodingAgentHarness(opts) {
1662
2006
  };
1663
2007
  refreshEffectiveResources();
1664
2008
  const piSessionCreations = /* @__PURE__ */ new Map();
2009
+ const sessionGenerations = /* @__PURE__ */ new Map();
2010
+ const generationOf = (sessionKey) => sessionGenerations.get(sessionKey) ?? 0;
1665
2011
  async function getOrCreatePiSession(sessionId, input, ctx) {
1666
2012
  const sessionCtx = sessionCtxForInput(input, ctx);
1667
2013
  const sessionKey = sessionCacheKey(sessionId, sessionCtx);
@@ -1676,8 +2022,18 @@ function createPiCodingAgentHarness(opts) {
1676
2022
  await applyRequestedSessionOptions(handle, input, { strictModelResolution: pi.strictModelResolution });
1677
2023
  return handle;
1678
2024
  }
1679
- const creation = createPiSession(sessionId, sessionCtx, input, ctx);
2025
+ const generation = generationOf(sessionKey);
2026
+ const creation = createPiSession(sessionId, sessionCtx, input, ctx).then((handle) => {
2027
+ if (generationOf(sessionKey) === generation) return handle;
2028
+ disposeHandleAt(sessionKey, handle);
2029
+ throw Object.assign(new Error("session not found"), {
2030
+ code: ErrorCode.enum.SESSION_NOT_FOUND,
2031
+ statusCode: 404
2032
+ });
2033
+ });
1680
2034
  piSessionCreations.set(sessionKey, creation);
2035
+ creation.catch(() => {
2036
+ });
1681
2037
  try {
1682
2038
  return await creation;
1683
2039
  } finally {
@@ -1704,24 +2060,25 @@ function createPiCodingAgentHarness(opts) {
1704
2060
  const authStorage = AuthStorage.create();
1705
2061
  const modelRegistry = ModelRegistry.create(authStorage);
1706
2062
  registerConfiguredModelProviders(modelRegistry);
1707
- const savedPiFile = sessionStore.loadPiSessionFileSync(sessionCtx, sessionId);
1708
- let sessionManager;
1709
- let isNewPiSession = false;
1710
- const runtimeCwd = opts.runtimeCwd ?? ctx.workdir;
1711
- const nativeSessionDir = sessionStore.getSessionDir();
1712
- if (savedPiFile) {
1713
- try {
1714
- sessionManager = SessionManager.open(savedPiFile, void 0, runtimeCwd);
1715
- } catch {
1716
- sessionManager = SessionManager.create(runtimeCwd, nativeSessionDir);
1717
- isNewPiSession = true;
1718
- }
1719
- } else {
1720
- sessionManager = SessionManager.create(runtimeCwd, nativeSessionDir);
1721
- isNewPiSession = true;
1722
- }
1723
2063
  const resolvedModel = resolveRequestedModel(modelRegistry, input, { strict: pi.strictModelResolution });
1724
2064
  const model = resolvedModel ?? resolveDefaultModel(modelRegistry, pi.defaultModel, pi.strictModelResolution);
2065
+ const savedPiFile = await sessionStore.loadPiSessionFile(sessionCtx, sessionId);
2066
+ const runtimeCwd = opts.runtimeCwd ?? ctx.workdir;
2067
+ if (!savedPiFile) {
2068
+ throw Object.assign(
2069
+ new Error(`Session not found: ${sessionId}`),
2070
+ { code: ErrorCode.enum.SESSION_NOT_FOUND, statusCode: 404 }
2071
+ );
2072
+ }
2073
+ let sessionManager;
2074
+ try {
2075
+ sessionManager = SessionManager2.open(savedPiFile, void 0, runtimeCwd);
2076
+ } catch (error) {
2077
+ throw Object.assign(
2078
+ new Error(`failed to open pi session transcript for '${sessionId}'`),
2079
+ { code: ErrorCode.enum.SESSION_TRANSCRIPT_UNREADABLE, statusCode: 500, cause: error }
2080
+ );
2081
+ }
1725
2082
  refreshEffectiveResources();
1726
2083
  const composedSystemPromptAppend = composeSystemPromptAppend(opts.systemPromptAppend);
1727
2084
  const dynamicPromptExtension = opts.systemPromptDynamic ? buildDynamicPromptExtension(opts.systemPromptDynamic) : void 0;
@@ -1746,6 +2103,7 @@ function createPiCodingAgentHarness(opts) {
1746
2103
  ...extensionFactories.length ? { extensionFactories } : {},
1747
2104
  ...pi.noContextFiles ? { noContextFiles: true } : {},
1748
2105
  ...pi.noSkills ? { noSkills: true } : {},
2106
+ ...pi.noExtensions ? { noExtensions: true } : {},
1749
2107
  ...effectiveSkillPaths.length ? { additionalSkillPaths: effectiveSkillPaths } : {},
1750
2108
  // skillsOverride REPLACES Pi's resolved skill set, which includes
1751
2109
  // skills contributed by host-declared pi packages (e.g.
@@ -1771,7 +2129,7 @@ function createPiCodingAgentHarness(opts) {
1771
2129
  // adapted tool catalog active. Do NOT pass an explicit empty tool-name
1772
2130
  // allowlist: in the current Pi SDK that disables custom tools too.
1773
2131
  noTools: "builtin",
1774
- customTools: adaptToolsForPi(opts.tools, input.sessionId, opts.telemetry, () => runContextStorage.getStore()),
2132
+ customTools: adaptToolsForPi(opts.tools, sessionId, opts.telemetry, () => runContextStorage.getStore()),
1775
2133
  model,
1776
2134
  thinkingLevel: input.thinkingLevel ?? "off",
1777
2135
  sessionManager,
@@ -1779,13 +2137,6 @@ function createPiCodingAgentHarness(opts) {
1779
2137
  modelRegistry,
1780
2138
  ...resourceLoader ? { resourceLoader } : {}
1781
2139
  });
1782
- if (isNewPiSession) {
1783
- const piFile = sessionManager.getSessionFile();
1784
- if (piFile) {
1785
- sessionStore.savePiSessionFile(sessionCtx, sessionId, piFile).catch(() => {
1786
- });
1787
- }
1788
- }
1789
2140
  const restoreFollowUpContextWrapper = rememberQueuedFollowUpRunContexts(piSession, runContextState, () => runContextStorage.getStore());
1790
2141
  const unsubscribePiRunContextListener = piSession.subscribe((event) => updateRunContextStateFromPiEvent(runContextState, event, (ctx2) => runContextStorage.enterWith(ctx2)));
1791
2142
  const unsubscribeRunContextListener = () => {
@@ -1812,34 +2163,36 @@ function createPiCodingAgentHarness(opts) {
1812
2163
  await Promise.all(handles.map((handle) => handle.piSession.reload()));
1813
2164
  return true;
1814
2165
  }
2166
+ function disposeHandleAt(key, handle) {
2167
+ handle.unsubscribeRunContextListener();
2168
+ handle.piSession.dispose();
2169
+ if (piSessions.get(key) === handle) piSessions.delete(key);
2170
+ }
1815
2171
  function disposePiSession(sessionId, ctx) {
1816
2172
  if (ctx) {
1817
2173
  const key = sessionCacheKey(sessionId, ctx);
1818
2174
  const handle = piSessions.get(key);
1819
2175
  if (!handle) return;
1820
- handle.unsubscribeRunContextListener();
1821
- handle.piSession.dispose();
1822
- piSessions.delete(key);
2176
+ disposeHandleAt(key, handle);
1823
2177
  return;
1824
2178
  }
1825
2179
  for (const [key, handle] of piSessions) {
1826
2180
  if (handle.sessionId !== sessionId) continue;
1827
- handle.unsubscribeRunContextListener();
1828
- handle.piSession.dispose();
1829
- piSessions.delete(key);
2181
+ disposeHandleAt(key, handle);
1830
2182
  }
1831
2183
  }
1832
2184
  function piSessionHandlesFor(sessionId) {
1833
2185
  return [...piSessions.values()].filter((handle) => handle.sessionId === sessionId);
1834
2186
  }
1835
2187
  async function getOrCreatePiSessionForCommand(sessionId, ctx) {
1836
- const sessionCtx = sessionCtxFromRunContext(ctx);
1837
- const existing = piSessions.get(sessionCacheKey(sessionId, sessionCtx));
1838
- if (existing) return existing;
2188
+ const sessionCtx = normalizeSessionCtx2(ctx.sessionCtx) ?? sessionCtxFromRunContext(ctx);
1839
2189
  return getOrCreatePiSession(sessionId, { sessionId, content: "", ctx: sessionCtx }, ctx);
1840
2190
  }
1841
2191
  const originalDelete = sessionStore.delete.bind(sessionStore);
1842
2192
  sessionStore.delete = async (ctx, sessionId) => {
2193
+ const key = sessionCacheKey(sessionId, ctx);
2194
+ sessionGenerations.set(key, generationOf(key) + 1);
2195
+ await Promise.allSettled([piSessionCreations.get(key)]);
1843
2196
  await originalDelete(ctx, sessionId);
1844
2197
  disposePiSession(sessionId, ctx);
1845
2198
  };
@@ -1932,8 +2285,8 @@ export {
1932
2285
  piPackageSourceKey,
1933
2286
  compactPiPackages,
1934
2287
  mergePiPackageSources,
1935
- withPiHarnessDefaults,
1936
2288
  createResourceSettingsManager,
2289
+ withPiHarnessDefaults,
1937
2290
  deriveSourcePlugin,
1938
2291
  createPiCodingAgentHarness
1939
2292
  };