@pi-chamber/web 0.2.0 → 0.2.2

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 (87) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{CommitSection-LewzHfDH.js → CommitSection-DPjTv8fA.js} +1 -1
  3. package/dist/assets/{DiagramEditor-BOIJVnh3.js → DiagramEditor-ChOor9l9.js} +1 -1
  4. package/dist/assets/{DiagramView-CdNHezna.js → DiagramView-3hAT86v_.js} +1 -1
  5. package/dist/assets/{DiffView-Cmw-RHIH.js → DiffView-D1dyOiw2.js} +2 -2
  6. package/dist/assets/FilesView-BCzghrc7.js +14 -0
  7. package/dist/assets/{GitView-DqYF-n_h.js → GitView-CYF1E1p4.js} +4 -4
  8. package/dist/assets/{JsonTreeView-DzEjA1ht.js → JsonTreeView-CrJO6LNu.js} +1 -1
  9. package/dist/assets/{MarkdownRendererImpl-Bfq0bOSM.js → MarkdownRendererImpl-BmKjrkGD.js} +2 -2
  10. package/dist/assets/{PatchDiff-CtVByTC4.js → PatchDiff-DgvPLx6x.js} +1 -1
  11. package/dist/assets/{PierreDiffViewer-Cwoewmiu.js → PierreDiffViewer-InnT0JVA.js} +2 -2
  12. package/dist/assets/{SessionSwitcherDropdown-DNjCJjzF.js → SessionSwitcherDropdown-C0OdvirK.js} +102 -102
  13. package/dist/assets/{SettingsView-BtLvHlj4.js → SettingsView-JAfC_cBL.js} +1 -1
  14. package/dist/assets/{SettingsWindow-CPd7JmJs.js → SettingsWindow-BBEsjcJE.js} +1 -1
  15. package/dist/assets/{ToolOutputDialog-CFFC5eI-.js → ToolOutputDialog-gzwcb8nI.js} +4 -4
  16. package/dist/assets/{ToolPartDiffPreview-yFq1DKSA.js → ToolPartDiffPreview-C45V5bhy.js} +1 -1
  17. package/dist/assets/{VirtualizedFileDiff-CC5lTPJS.js → VirtualizedFileDiff-uKfnkQWb.js} +1 -1
  18. package/dist/assets/{collapsible-D_555Hw_.js → collapsible-Bq8sDiKS.js} +1 -1
  19. package/dist/assets/{computeVirtualFileMetrics-BcVuV0Ic.js → computeVirtualFileMetrics-BPgZ_RSe.js} +2 -2
  20. package/dist/assets/{contextFileOpenGuard-BygvjHuE.js → contextFileOpenGuard-CydO7PkI.js} +1 -1
  21. package/dist/assets/{index-C9CLY-is.js → index-BSuNNtBh.js} +1 -1
  22. package/dist/assets/{index-DQr8-8RF.js → index-CH7o4-_q.js} +1 -1
  23. package/dist/assets/index-o4Eea60G.css +1 -0
  24. package/dist/assets/{isDiffPlainText-Bher0UxW.js → isDiffPlainText-f9b4cKNo.js} +1 -1
  25. package/dist/assets/{isFilePlainText-EIViqOlI.js → isFilePlainText-BjESb-gU.js} +1 -1
  26. package/dist/assets/{main-1bIgjP48.js → main-B508FBXr.js} +2 -2
  27. package/dist/assets/main-nG4uFyAy.js +133 -0
  28. package/dist/assets/{markdownCore-Bm4FxtK1.js → markdownCore-BFmf8a-O.js} +2 -2
  29. package/dist/assets/{miniChat-B7kKqKHH.js → miniChat-BTlz8IPL.js} +2 -2
  30. package/dist/assets/{mobile-DZPRce5z.js → mobile-DBkrRt80.js} +2 -2
  31. package/dist/assets/renderElectronMiniChatApp-DW3fSR5w.js +1 -0
  32. package/dist/assets/renderMobileApp-9qkroKty.js +14 -0
  33. package/dist/assets/{runtimeSurface-B6sKTjo7.js → runtimeSurface-Bec4O8FD.js} +1 -1
  34. package/dist/assets/{useInlineCommentController-CSR_Ju0a.js → useInlineCommentController-CHmIm0Rr.js} +2 -2
  35. package/dist/assets/usePanelSlide-DCX8qQ4K.js +56 -0
  36. package/dist/assets/{useWindowTitle-rt_8xbd9.js → useWindowTitle-CE5N_NeU.js} +3 -3
  37. package/dist/assets/{vendor-codemirror-autocomplete-3xMyV7a2.js → vendor-codemirror-autocomplete-BS2SpSAZ.js} +1 -1
  38. package/dist/assets/{vendor-codemirror-commands-Bzf12ODH.js → vendor-codemirror-commands-RrgJ8H_6.js} +1 -1
  39. package/dist/assets/{vendor-codemirror-lang-angular-0K6s6cvb.js → vendor-codemirror-lang-angular-ADhO_TUK.js} +1 -1
  40. package/dist/assets/{vendor-codemirror-lang-cpp-B0aL03z-.js → vendor-codemirror-lang-cpp-BGCWj7Om.js} +1 -1
  41. package/dist/assets/{vendor-codemirror-lang-css-C4jEScp7.js → vendor-codemirror-lang-css-Ba8JZ6fp.js} +1 -1
  42. package/dist/assets/{vendor-codemirror-lang-go-CgrfVBPr.js → vendor-codemirror-lang-go-DyTiHXo4.js} +1 -1
  43. package/dist/assets/{vendor-codemirror-lang-html-C1Svvdey.js → vendor-codemirror-lang-html-DK27qQri.js} +1 -1
  44. package/dist/assets/{vendor-codemirror-lang-java-DNPz7_Bx.js → vendor-codemirror-lang-java-CvFfnhPQ.js} +1 -1
  45. package/dist/assets/{vendor-codemirror-lang-javascript-CrbeaEAU.js → vendor-codemirror-lang-javascript-BUlYhQ3o.js} +1 -1
  46. package/dist/assets/{vendor-codemirror-lang-jinja-DL8BR_RG.js → vendor-codemirror-lang-jinja-wBMtAEvT.js} +1 -1
  47. package/dist/assets/{vendor-codemirror-lang-json-BTvAjwkJ.js → vendor-codemirror-lang-json-CqayksDf.js} +1 -1
  48. package/dist/assets/{vendor-codemirror-lang-less-CPABt6NZ.js → vendor-codemirror-lang-less-Ct3-H1PB.js} +1 -1
  49. package/dist/assets/{vendor-codemirror-lang-liquid-BLOLOBMg.js → vendor-codemirror-lang-liquid-XspREksq.js} +1 -1
  50. package/dist/assets/{vendor-codemirror-lang-markdown-BloD9GN-.js → vendor-codemirror-lang-markdown-CvO36Y1p.js} +1 -1
  51. package/dist/assets/{vendor-codemirror-lang-php-FIZMTuWM.js → vendor-codemirror-lang-php-BlUF9cCe.js} +1 -1
  52. package/dist/assets/{vendor-codemirror-lang-python-CR2g6ukK.js → vendor-codemirror-lang-python-_2bIYTiJ.js} +1 -1
  53. package/dist/assets/{vendor-codemirror-lang-rust-lsPqn4k4.js → vendor-codemirror-lang-rust-BdTYih_U.js} +1 -1
  54. package/dist/assets/{vendor-codemirror-lang-sass-CtECMwzH.js → vendor-codemirror-lang-sass-E6MhfOFk.js} +1 -1
  55. package/dist/assets/{vendor-codemirror-lang-sql-BtmZrrZH.js → vendor-codemirror-lang-sql-B1BRhQSF.js} +1 -1
  56. package/dist/assets/{vendor-codemirror-lang-vue-Br6DrntY.js → vendor-codemirror-lang-vue-BYQJRa1Q.js} +1 -1
  57. package/dist/assets/{vendor-codemirror-lang-wast-DT7IlCnb.js → vendor-codemirror-lang-wast-D706yuyC.js} +1 -1
  58. package/dist/assets/{vendor-codemirror-lang-xml-wNr-LZOf.js → vendor-codemirror-lang-xml--MfWU6Dj.js} +1 -1
  59. package/dist/assets/{vendor-codemirror-lang-yaml-BTKFwfc2.js → vendor-codemirror-lang-yaml-CRUFIbRX.js} +1 -1
  60. package/dist/assets/{vendor-codemirror-language-BuDklWi8.js → vendor-codemirror-language-E2vgV9K3.js} +1 -1
  61. package/dist/assets/{vendor-codemirror-language-data-Be-zDX0u.js → vendor-codemirror-language-data-q1yHU1R9.js} +2 -2
  62. package/dist/assets/{vendor-codemirror-search-BzyfPoRb.js → vendor-codemirror-search-8kyQdB3f.js} +1 -1
  63. package/dist/assets/{vendor-codemirror-state-Wdz6Qsow.js → vendor-codemirror-state-CNWM3Q8V.js} +1 -1
  64. package/dist/assets/{vendor-codemirror-view-pXtfpqbT.js → vendor-codemirror-view-DSzLbZiS.js} +1 -1
  65. package/dist/assets/{vendor-replit-codemirror-vim-BUqe2Slv.js → vendor-replit-codemirror-vim-BJY5fGLS.js} +1 -1
  66. package/dist/assets/{web-components-DgFD1Jms.js → web-components-CLirSfwt.js} +1 -1
  67. package/dist/index.html +4 -4
  68. package/dist/mini-chat.html +3 -3
  69. package/dist/mobile.html +3 -3
  70. package/package.json +1 -1
  71. package/server/index.js +6 -1
  72. package/server/lib/pi/routes.js +13 -1
  73. package/server/lib/pi/routes.test.js +4 -0
  74. package/server/lib/pi/session-daemon/DOCUMENTATION.md +6 -5
  75. package/server/lib/pi/session-daemon/daemon-process.js +4 -1
  76. package/server/lib/pi/session-daemon/session-daemon.js +138 -20
  77. package/server/lib/pi/session-daemon/session-daemon.test.js +208 -0
  78. package/server/lib/pi/session-daemon/session-jsonl.js +258 -6
  79. package/server/lib/pi/session-daemon/session-jsonl.test.js +69 -1
  80. package/server/lib/pi/session-daemon/supervisor.js +41 -21
  81. package/server/lib/pi/session-daemon/supervisor.test.js +28 -0
  82. package/dist/assets/FilesView-BODbL0Xm.js +0 -14
  83. package/dist/assets/index-DYrJXRkU.css +0 -1
  84. package/dist/assets/main-JDtt2bQS.js +0 -133
  85. package/dist/assets/renderElectronMiniChatApp-Dq0GdThJ.js +0 -1
  86. package/dist/assets/renderMobileApp-DlKnjObC.js +0 -23
  87. package/dist/assets/usePanelSlide-BkonU4LS.js +0 -47
@@ -1,8 +1,10 @@
1
1
  import { readFile, chmod, rename, rm, writeFile } from 'node:fs/promises';
2
- import { dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
3
 
4
4
  import { createSessionDaemon } from './session-daemon.js';
5
5
 
6
+ const DAEMON_ENTRYPOINT = fileURLToPath(import.meta.url);
7
+
6
8
  const argument = (name) => {
7
9
  const index = process.argv.indexOf(name);
8
10
  return index === -1 ? undefined : process.argv[index + 1];
@@ -41,6 +43,7 @@ const writeReadyState = () => writeState({
41
43
  protocolVersion: 1,
42
44
  pid: process.pid,
43
45
  endpoint,
46
+ entrypoint: DAEMON_ENTRYPOINT,
44
47
  startedAt: new Date().toISOString(),
45
48
  });
46
49
 
@@ -19,7 +19,13 @@ import {
19
19
  import { createPiModelConfigStore } from '../model-config-store.js';
20
20
  import { clampThinkingLevel, getSupportedThinkingLevels, isPiThinkingLevel } from '../thinking-levels.js';
21
21
  import { createSessionRuntimeRegistry } from './runtime-registry.js';
22
- import { getPiSessionDirectory, validatePiSessionJsonlDirectory, validatePiSessionJsonlFile } from './session-jsonl.js';
22
+ import {
23
+ findPiSessionJsonlById,
24
+ getPiSessionDirectory,
25
+ listPiSessionJsonlDirectory,
26
+ validatePiSessionJsonlDirectory,
27
+ validatePiSessionJsonlFile,
28
+ } from './session-jsonl.js';
23
29
 
24
30
  const PROTOCOL_VERSION = 1;
25
31
  const MAX_FRAME_BYTES = 16 * 1024 * 1024;
@@ -80,10 +86,10 @@ export function createSessionDaemon({
80
86
  createRuntime = createPiSessionRuntime,
81
87
  healthMetadata = {},
82
88
  idleTimeoutMs = 5 * 60 * 1_000,
83
- listSessions = ({ cwd: sessionCwd, agentDir: sessionAgentDir = agentDir }) => SessionManager.list(
84
- sessionCwd,
85
- getPiSessionDirectory({ cwd: sessionCwd, agentDir: sessionAgentDir }),
86
- ),
89
+ listSessions = ({ cwd: sessionCwd, agentDir: sessionAgentDir = agentDir }) => listPiSessionJsonlDirectory({
90
+ cwd: sessionCwd,
91
+ agentDir: sessionAgentDir,
92
+ }),
87
93
  createSettingsManager = ({ cwd: settingsCwd, agentDir: settingsAgentDir = agentDir, projectTrusted }) => SettingsManager.create(
88
94
  settingsCwd,
89
95
  settingsAgentDir,
@@ -335,9 +341,8 @@ export function createSessionDaemon({
335
341
  idleDisposeTimers.set(sessionId, timer);
336
342
  };
337
343
 
338
- const listSessionItems = async (requestedDirectory) => {
339
- const targetDir = requestedDirectory ? await resolveDirectory(requestedDirectory) : (activeDirectory || cwd);
340
- await validatePiSessionJsonlDirectory({ cwd: targetDir, agentDir });
344
+ const listInflightByDirectory = new Map();
345
+ const listSessionItemsUnshared = async (targetDir) => {
341
346
  const sessions = await listSessions({ cwd: targetDir, agentDir });
342
347
  if (!Array.isArray(sessions)) {
343
348
  throw new SessionDaemonProtocolError('INVALID_SESSION', 'Pi returned an invalid session collection.');
@@ -392,6 +397,17 @@ export function createSessionDaemon({
392
397
  });
393
398
  };
394
399
 
400
+ const listSessionItems = async (requestedDirectory) => {
401
+ const targetDir = requestedDirectory ? await resolveDirectory(requestedDirectory) : (activeDirectory || cwd);
402
+ const inflight = listInflightByDirectory.get(targetDir);
403
+ if (inflight) return inflight;
404
+ const pending = listSessionItemsUnshared(targetDir).finally(() => {
405
+ if (listInflightByDirectory.get(targetDir) === pending) listInflightByDirectory.delete(targetDir);
406
+ });
407
+ listInflightByDirectory.set(targetDir, pending);
408
+ return pending;
409
+ };
410
+
395
411
  const renameSession = async (payload) => {
396
412
  if (!payload || typeof payload !== 'object' || typeof payload.sessionId !== 'string' || payload.sessionId.length === 0
397
413
  || typeof payload.title !== 'string' || payload.title.trim().length === 0 || payload.title.length > 256) {
@@ -428,6 +444,21 @@ export function createSessionDaemon({
428
444
  if (typeof sessionId !== 'string' || sessionId.length === 0) {
429
445
  throw new SessionDaemonProtocolError('INVALID_SESSION', 'The Pi session does not exist.');
430
446
  }
447
+ // Filename identity is enough to 404 a missing id without listing or
448
+ // fully reading every transcript in every cwd. Deep-linking a stale
449
+ // `?session=` used to hang here while `validatePiSessionJsonlDirectory`
450
+ // scanned multi-megabyte JSONL files that cannot be the target.
451
+ try {
452
+ const named = await findPiSessionJsonlById({ sessionId, agentDir });
453
+ if (named?.path && named.cwd) {
454
+ await validatePiSessionJsonlFile(named.path);
455
+ const directory = await resolveDirectory(named.cwd);
456
+ knownDirectories.add(directory);
457
+ return { target: { id: sessionId, path: named.path }, directory };
458
+ }
459
+ } catch {
460
+ // Fall through to SDK list / header scan for non-standard filenames.
461
+ }
431
462
  const candidateDirs = new Set();
432
463
  if (requestedDirectory) {
433
464
  try {
@@ -440,7 +471,6 @@ export function createSessionDaemon({
440
471
 
441
472
  for (const dir of candidateDirs) {
442
473
  try {
443
- await validatePiSessionJsonlDirectory({ cwd: dir, agentDir });
444
474
  const sessions = await listSessions({ cwd: dir, agentDir });
445
475
  const target = Array.isArray(sessions) ? sessions.find((session) => session?.id === sessionId) : undefined;
446
476
  if (target && typeof target.path === 'string' && target.path.length > 0) {
@@ -492,7 +522,8 @@ export function createSessionDaemon({
492
522
  throw new SessionDaemonProtocolError('INVALID_SESSION', 'The Pi session does not exist.');
493
523
  };
494
524
 
495
- const activateSession = async (sessionId, requestedDirectory) => {
525
+ const activateInflightBySessionId = new Map();
526
+ const activateSessionUnshared = async (sessionId, requestedDirectory) => {
496
527
  if (!runtimeRegistry) {
497
528
  runtimeRegistry = createSessionRuntimeRegistry({
498
529
  onSessionEvent: ({ cwd: eventCwd, sessionId: eventSessionId }, event) => publishSessionEvent(eventSessionId, event, eventCwd),
@@ -521,16 +552,89 @@ export function createSessionDaemon({
521
552
  if (newRuntime.session?.sessionId !== sessionId && typeof newRuntime.switchSession === 'function') {
522
553
  await newRuntime.switchSession(target.path);
523
554
  }
524
- runtimeRegistry.register(newRuntime, { cwd: directory });
555
+ const raced = runtimeRegistry.findBySessionId(sessionId);
556
+ if (raced) {
557
+ try { await newRuntime.dispose?.(); } catch { /* keep the winner */ }
558
+ runtime = raced;
559
+ if (raced.cwd) activeDirectory = raced.cwd;
560
+ return raced;
561
+ }
562
+ try {
563
+ runtimeRegistry.register(newRuntime, { cwd: directory });
564
+ } catch (error) {
565
+ if (error?.code === 'SESSION_RUNTIME_CONFLICT') {
566
+ try { await newRuntime.dispose?.(); } catch { /* keep the winner */ }
567
+ const winner = runtimeRegistry.findBySessionId(sessionId)
568
+ || runtimeRegistry.get({ cwd: directory, sessionId });
569
+ if (winner) {
570
+ runtime = winner;
571
+ if (winner.cwd) activeDirectory = winner.cwd;
572
+ return winner;
573
+ }
574
+ }
575
+ throw error;
576
+ }
525
577
  runtime = newRuntime;
526
578
  activeDirectory = directory;
527
579
  rememberRuntimeSession();
528
580
  return newRuntime;
529
581
  };
530
582
 
583
+ const activateSession = async (sessionId, requestedDirectory) => {
584
+ const inflight = activateInflightBySessionId.get(sessionId);
585
+ if (inflight) return inflight;
586
+ const pending = activateSessionUnshared(sessionId, requestedDirectory).finally(() => {
587
+ if (activateInflightBySessionId.get(sessionId) === pending) activateInflightBySessionId.delete(sessionId);
588
+ });
589
+ activateInflightBySessionId.set(sessionId, pending);
590
+ return pending;
591
+ };
592
+
593
+ const liveProjectionEntries = (session, persisted) => {
594
+ if (!session?.isStreaming) return persisted;
595
+ const liveMessages = [];
596
+ if (Array.isArray(session.messages)) liveMessages.push(...session.messages);
597
+ const streamingMessage = session.state?.streamingMessage;
598
+ if (streamingMessage && liveMessages[liveMessages.length - 1] !== streamingMessage) {
599
+ liveMessages.push(streamingMessage);
600
+ }
601
+ if (liveMessages.length === 0) return persisted;
602
+
603
+ const persistedKeys = new Set();
604
+ for (const entry of persisted) {
605
+ if (entry?.type !== 'message' || !entry.message) continue;
606
+ const timestamp = typeof entry.message.timestamp === 'number' ? entry.message.timestamp : Date.parse(entry.timestamp);
607
+ persistedKeys.add(`${entry.message.role}:${Number.isFinite(timestamp) ? timestamp : ''}`);
608
+ }
609
+
610
+ const entries = [...persisted];
611
+ let liveIndex = 0;
612
+ const liveAssistantId = streamingMessageIds.get(session.sessionId);
613
+ for (const message of liveMessages) {
614
+ if (!message || (message.role !== 'user' && message.role !== 'assistant' && message.role !== 'toolResult')) continue;
615
+ const timestamp = typeof message.timestamp === 'number' ? message.timestamp : undefined;
616
+ const key = `${message.role}:${timestamp ?? ''}`;
617
+ if (timestamp !== undefined && persistedKeys.has(key)) continue;
618
+ persistedKeys.add(key);
619
+ const id = message.role === 'assistant' && liveAssistantId
620
+ ? liveAssistantId
621
+ : `live-${session.sessionId}-${liveIndex}`;
622
+ liveIndex += 1;
623
+ entries.push({
624
+ type: 'message',
625
+ id,
626
+ timestamp: new Date(timestamp || Date.now()).toISOString(),
627
+ message,
628
+ });
629
+ }
630
+ return entries;
631
+ };
632
+
531
633
  const projectMessageEntries = (session, targetDir = activeDirectory || cwd) => {
532
- const entries = session?.sessionManager?.getEntries?.();
533
- if (!Array.isArray(entries)) return [];
634
+ const persisted = session?.sessionManager?.getEntries?.();
635
+ const entries = liveProjectionEntries(session, Array.isArray(persisted) ? persisted : []);
636
+ if (entries.length === 0) return [];
637
+ const streaming = session?.isStreaming === true;
534
638
  const toolResults = new Map();
535
639
  for (const entry of entries) {
536
640
  if (entry?.type !== 'message' || !entry.message || entry.message.role !== 'toolResult' || typeof entry.message.toolCallId !== 'string') continue;
@@ -563,6 +667,8 @@ export function createSessionDaemon({
563
667
  if (part?.type === 'thinking') return [{ type: 'thinking', id: `${entry.id}:thinking:${index}`, index, text: redactAttachmentPaths(part.thinking) }];
564
668
  if (part?.type === 'toolCall') {
565
669
  const result = toolResults.get(part.id);
670
+ const running = streaming && !result;
671
+ const interrupted = !running && !result;
566
672
  return [{
567
673
  type: 'tool',
568
674
  id: `${entry.id}:tool:${part.id}`,
@@ -570,12 +676,20 @@ export function createSessionDaemon({
570
676
  toolCallId: part.id,
571
677
  name: part.name,
572
678
  input: redactAttachmentValues(part.arguments),
573
- state: result?.isError ? 'error' : 'completed',
679
+ state: result?.isError || interrupted ? 'error' : running ? 'running' : 'completed',
574
680
  ...(result?.output ? { output: result.output } : {}),
575
- ...(result?.error ? { error: result.error } : {}),
576
- ...(result?.isError ? { isError: true } : {}),
681
+ ...(result?.error
682
+ ? { error: result.error }
683
+ : interrupted
684
+ ? { error: 'Tool was interrupted before completion.' }
685
+ : {}),
686
+ ...(result?.isError || interrupted ? { isError: true } : {}),
577
687
  ...(result?.metadata ? { metadata: result.metadata } : {}),
578
- ...(Number.isFinite(result?.endedAt) ? { endedAt: result.endedAt } : {}),
688
+ ...(Number.isFinite(result?.endedAt)
689
+ ? { endedAt: result.endedAt }
690
+ : interrupted
691
+ ? { endedAt: createdAt }
692
+ : {}),
579
693
  }];
580
694
  }
581
695
  return [];
@@ -608,6 +722,7 @@ export function createSessionDaemon({
608
722
  const sessionModel = lastAssistant?.model
609
723
  ?? (model?.provider && model?.id ? { providerId: model.provider, modelId: model.id } : undefined);
610
724
  const sessionThinking = lastAssistant?.thinkingLevel || session.thinkingLevel;
725
+ const isStreaming = session.isStreaming === true;
611
726
  return {
612
727
  session: {
613
728
  id: session.sessionId, directory: targetDir, createdAt, updatedAt: createdAt,
@@ -618,6 +733,8 @@ export function createSessionDaemon({
618
733
  },
619
734
  messages,
620
735
  lastSequence: sequence,
736
+ isStreaming,
737
+ lifecycle: isStreaming ? 'busy' : 'idle',
621
738
  };
622
739
  };
623
740
 
@@ -1378,17 +1495,18 @@ export function createSessionDaemon({
1378
1495
  const deleteSession = async (sessionId, requestedDirectory) => {
1379
1496
  const active = runtimeRegistry?.findBySessionId(sessionId);
1380
1497
  let targetDir = requestedDirectory ? await resolveDirectory(requestedDirectory) : active?.cwd || activeDirectory || cwd;
1498
+ const activeSessionFile = active?.session?.sessionManager?.getSessionFile?.();
1381
1499
  if (active) {
1382
1500
  if (active.session?.isStreaming) await active.session.abort();
1383
1501
  await runtimeRegistry?.dispose(active);
1384
1502
  if (runtime === active) runtime = undefined;
1385
1503
  }
1386
- try {
1504
+ if (active && typeof activeSessionFile === 'string' && activeSessionFile.length > 0) {
1505
+ await rm(activeSessionFile, { force: true });
1506
+ } else if (!active) {
1387
1507
  const { target, directory } = await findPersistedSession(sessionId, targetDir);
1388
1508
  targetDir = directory;
1389
1509
  await rm(target.path, { force: false });
1390
- } catch (error) {
1391
- if (!active || error?.code !== 'INVALID_SESSION') throw error;
1392
1510
  }
1393
1511
  publish('session.lifecycle', { state: 'idle', deleted: true }, sessionId, targetDir);
1394
1512
  };
@@ -390,6 +390,93 @@ describe('Pi session daemon spike', () => {
390
390
  await client.close();
391
391
  });
392
392
 
393
+ it('shares an in-flight sessions.list for the same directory', async () => {
394
+ const root = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-share-'));
395
+ const endpoint = testDaemonEndpoint(root);
396
+ const agentDir = join(root, 'agent');
397
+ await mkdir(agentDir, { recursive: true });
398
+ let calls = 0;
399
+ daemon = createSessionDaemon({
400
+ endpoint,
401
+ credential,
402
+ cwd: root,
403
+ agentDir,
404
+ createRuntime: async () => ({ session: new FakeSession(), async dispose() {} }),
405
+ listSessions: async () => {
406
+ calls += 1;
407
+ await new Promise((resolve) => setTimeout(resolve, 80));
408
+ return [{
409
+ path: join(root, 'session.jsonl'),
410
+ id: 'pi-session-1',
411
+ cwd: root,
412
+ created: new Date('2026-01-01T00:00:00.000Z'),
413
+ modified: new Date('2026-01-02T00:00:00.000Z'),
414
+ }];
415
+ },
416
+ });
417
+ await daemon.start();
418
+
419
+ const first = connectClient(endpoint);
420
+ await first.authenticate();
421
+ const second = connectClient(endpoint);
422
+ await second.authenticate();
423
+ const [left, right] = await Promise.all([
424
+ first.request('sessions.list', { directory: root }),
425
+ second.request('sessions.list', { directory: root }),
426
+ ]);
427
+ expect(calls).toBe(1);
428
+ expect(left.result.sessions[0].session.id).toBe('pi-session-1');
429
+ expect(right.result.sessions[0].session.id).toBe('pi-session-1');
430
+ await first.close();
431
+ await second.close();
432
+ });
433
+
434
+ it('shares an in-flight sessions.open for the same session id', async () => {
435
+ const root = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-open-share-'));
436
+ const endpoint = testDaemonEndpoint(root);
437
+ const agentDir = join(root, 'agent');
438
+ await mkdir(agentDir, { recursive: true });
439
+ const sessionFile = join(root, 'session.jsonl');
440
+ await writeFile(sessionFile, `{"type":"session","id":"pi-session-1","cwd":${JSON.stringify(root)}}\n`);
441
+ let calls = 0;
442
+ daemon = createSessionDaemon({
443
+ endpoint,
444
+ credential,
445
+ cwd: root,
446
+ agentDir,
447
+ listSessions: async () => [{
448
+ path: sessionFile,
449
+ id: 'pi-session-1',
450
+ cwd: root,
451
+ created: new Date('2026-01-01T00:00:00.000Z'),
452
+ modified: new Date('2026-01-02T00:00:00.000Z'),
453
+ }],
454
+ createRuntime: async (options) => {
455
+ calls += 1;
456
+ await new Promise((resolve) => setTimeout(resolve, 80));
457
+ return new FakeRuntime({
458
+ cwd: options.cwd,
459
+ session: new FakeSession('pi-session-1', options.sessionFile),
460
+ });
461
+ },
462
+ });
463
+ await daemon.start();
464
+
465
+ const first = connectClient(endpoint);
466
+ await first.authenticate();
467
+ const second = connectClient(endpoint);
468
+ await second.authenticate();
469
+ const [left, right] = await Promise.all([
470
+ first.request('sessions.open', { sessionId: 'pi-session-1', directory: root }),
471
+ second.request('sessions.open', { sessionId: 'pi-session-1', directory: root }),
472
+ ]);
473
+ expect(calls).toBe(1);
474
+ expect(left.result.session.id).toBe('pi-session-1');
475
+ expect(right.result.session.id).toBe('pi-session-1');
476
+ await first.close();
477
+ await second.close();
478
+ });
479
+
393
480
  it('does not include in-memory sessions from another directory when listing a newly selected project directory', async () => {
394
481
  const rootA = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-a-'));
395
482
  const rootB = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-b-'));
@@ -1198,6 +1285,127 @@ describe('Pi session daemon spike', () => {
1198
1285
  await client.close();
1199
1286
  });
1200
1287
 
1288
+ it('projects unmatched tools as running only while the session is authoritative-busy', async () => {
1289
+ const root = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-live-hydrate-'));
1290
+ const endpoint = testDaemonEndpoint(root);
1291
+ const projectDir = join(root, 'project');
1292
+ const agentDir = join(root, 'agent');
1293
+ await mkdir(projectDir, { recursive: true });
1294
+ await mkdir(agentDir, { recursive: true });
1295
+ const session = new FakeSession('pi-session-live');
1296
+ session.isStreaming = true;
1297
+ session.entries = [
1298
+ {
1299
+ type: 'message',
1300
+ id: 'user-1',
1301
+ timestamp: '2026-01-01T00:00:01.000Z',
1302
+ message: { role: 'user', content: 'run it', timestamp: 1_000 },
1303
+ },
1304
+ {
1305
+ type: 'message',
1306
+ id: 'assistant-1',
1307
+ timestamp: '2026-01-01T00:00:01.100Z',
1308
+ message: {
1309
+ role: 'assistant',
1310
+ content: [
1311
+ { type: 'text', text: 'calling bash' },
1312
+ { type: 'toolCall', id: 'tool-live', name: 'bash', arguments: { command: 'ls' } },
1313
+ ],
1314
+ provider: 'test',
1315
+ model: 'model',
1316
+ timestamp: 1_100,
1317
+ },
1318
+ },
1319
+ ];
1320
+ session.messages = [
1321
+ { role: 'user', content: 'run it', timestamp: 1_000 },
1322
+ {
1323
+ role: 'assistant',
1324
+ content: [
1325
+ { type: 'text', text: 'calling bash' },
1326
+ { type: 'toolCall', id: 'tool-live', name: 'bash', arguments: { command: 'ls' } },
1327
+ ],
1328
+ provider: 'test',
1329
+ model: 'model',
1330
+ timestamp: 1_100,
1331
+ },
1332
+ ];
1333
+
1334
+ daemon = createSessionDaemon({
1335
+ endpoint,
1336
+ credential,
1337
+ cwd: projectDir,
1338
+ agentDir,
1339
+ createRuntime: async () => ({ session, async dispose() {} }),
1340
+ listSessions: async () => [],
1341
+ });
1342
+ await daemon.start();
1343
+
1344
+ const client = connectClient(endpoint);
1345
+ await client.authenticate();
1346
+ const detail = await client.request('sessions.create', { cwd: projectDir });
1347
+ expect(detail.result.isStreaming).toBe(true);
1348
+ expect(detail.result.lifecycle).toBe('busy');
1349
+ expect(detail.result.messages).toHaveLength(2);
1350
+ expect(detail.result.messages[1].parts).toEqual(expect.arrayContaining([
1351
+ expect.objectContaining({ type: 'text', text: 'calling bash' }),
1352
+ expect.objectContaining({ type: 'tool', name: 'bash', state: 'running', toolCallId: 'tool-live' }),
1353
+ ]));
1354
+
1355
+ session.isStreaming = false;
1356
+ const settled = await client.request('sessions.open', { sessionId: session.sessionId, cwd: projectDir });
1357
+ expect(settled.result.lifecycle).toBe('idle');
1358
+ expect(settled.result.messages[1].parts).toEqual(expect.arrayContaining([
1359
+ expect.objectContaining({
1360
+ type: 'tool',
1361
+ name: 'bash',
1362
+ state: 'error',
1363
+ toolCallId: 'tool-live',
1364
+ isError: true,
1365
+ error: 'Tool was interrupted before completion.',
1366
+ endedAt: expect.any(Number),
1367
+ }),
1368
+ ]));
1369
+ await client.close();
1370
+ });
1371
+
1372
+ it('overlays an unpersisted live user prompt onto getSession while streaming', async () => {
1373
+ const root = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-live-user-'));
1374
+ const endpoint = testDaemonEndpoint(root);
1375
+ const projectDir = join(root, 'project');
1376
+ const agentDir = join(root, 'agent');
1377
+ await mkdir(projectDir, { recursive: true });
1378
+ await mkdir(agentDir, { recursive: true });
1379
+ const session = new FakeSession('pi-session-live-user');
1380
+ session.isStreaming = true;
1381
+ session.entries = [];
1382
+ session.messages = [
1383
+ { role: 'user', content: 'just sent', timestamp: 2_000 },
1384
+ ];
1385
+
1386
+ daemon = createSessionDaemon({
1387
+ endpoint,
1388
+ credential,
1389
+ cwd: projectDir,
1390
+ agentDir,
1391
+ createRuntime: async () => ({ session, async dispose() {} }),
1392
+ listSessions: async () => [],
1393
+ });
1394
+ await daemon.start();
1395
+
1396
+ const client = connectClient(endpoint);
1397
+ await client.authenticate();
1398
+ const detail = await client.request('sessions.create', { cwd: projectDir });
1399
+ expect(detail.result.isStreaming).toBe(true);
1400
+ expect(detail.result.lifecycle).toBe('busy');
1401
+ expect(detail.result.messages).toEqual([
1402
+ expect.objectContaining({
1403
+ message: expect.objectContaining({ role: 'user', text: 'just sent' }),
1404
+ }),
1405
+ ]);
1406
+ await client.close();
1407
+ });
1408
+
1201
1409
  it('projects Pi usage in getSession messages and message_end events, omitting malformed payloads', async () => {
1202
1410
  const root = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-usage-'));
1203
1411
  const endpoint = testDaemonEndpoint(root);