@pi-chamber/web 0.1.9 → 0.2.1
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.
- package/README.md +1 -1
- package/bin/cli.test.js +48 -0
- package/bin/lib/DOCUMENTATION.md +2 -2
- package/bin/lib/cli-args.js +38 -11
- package/bin/lib/cli-startup.js +60 -13
- package/bin/lib/commands-startup.js +20 -5
- package/bin/lib/commands-update.js +91 -34
- package/bin/lib/commands-update.test.js +57 -42
- package/dist/assets/CommitSection-BQBmpPVE.js +1 -0
- package/dist/assets/DiagramEditor-Cp22TGpq.js +1 -0
- package/dist/assets/DiagramView-E4HJ4ZPU.js +1 -0
- package/dist/assets/{DiffView-k3lX2PiV.js → DiffView-d23o9CKP.js} +2 -2
- package/dist/assets/{FilesView-C39IOFfs.js → FilesView-DIDB-xK7.js} +6 -6
- package/dist/assets/{GitView-CkztSaT7.js → GitView-CkUONO1-.js} +4 -4
- package/dist/assets/JsonTreeView-By2oGMNK.js +1 -0
- package/dist/assets/{MarkdownRendererImpl-CF12jEtB.js → MarkdownRendererImpl-DUghE55t.js} +2 -2
- package/dist/assets/PatchDiff-BjN6zMtK.js +1 -0
- package/dist/assets/{PierreDiffViewer-DSQwk05R.js → PierreDiffViewer-B38_7NnU.js} +2 -2
- package/dist/assets/{useAppFontEffects-DqOhrZWx.js → SessionSwitcherDropdown-NbVfnqQQ.js} +102 -102
- package/dist/assets/{SettingsView-Bf4vWwVK.js → SettingsView-C2dCv0r7.js} +1 -1
- package/dist/assets/SettingsWindow-DsMhdX5H.js +1 -0
- package/dist/assets/{ToolOutputDialog-Brg2Humk.js → ToolOutputDialog-DVesARmI.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-C2N_oIO-.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-BELx71mz.js → VirtualizedFileDiff-B7eVR6Pt.js} +1 -1
- package/dist/assets/collapsible-CPkWB1VR.js +1 -0
- package/dist/assets/computeVirtualFileMetrics-BsZulefS.js +3 -0
- package/dist/assets/contextFileOpenGuard-BmmlK5E3.js +1 -0
- package/dist/assets/index-BZXqJcua.css +1 -0
- package/dist/assets/{index-Dnby-sDS.js → index-BhXma4dv.js} +2 -2
- package/dist/assets/index-C84KrQoy.js +1 -0
- package/dist/assets/isDiffPlainText-DsuyxFXo.js +1 -0
- package/dist/assets/isFilePlainText-CCYoe7us.js +1 -0
- package/dist/assets/main-ClYgYVm1.js +2 -0
- package/dist/assets/main-CmrF_CaW.js +133 -0
- package/dist/assets/{markdownCore-BK98alzY.js → markdownCore-D2uqals1.js} +2 -2
- package/dist/assets/miniChat-DmX9SjxM.js +2 -0
- package/dist/assets/mobile-C-PCxN62.js +2 -0
- package/dist/assets/renderElectronMiniChatApp-BhXZtZlU.js +1 -0
- package/dist/assets/renderMobileApp-DUSTAPWw.js +23 -0
- package/dist/assets/{runtimeSurface-DMXukysz.js → runtimeSurface-CouGjZ3A.js} +1 -1
- package/dist/assets/{useInlineCommentController-ruw8wb2Y.js → useInlineCommentController-buNJ3xB4.js} +2 -2
- package/dist/assets/usePanelSlide-BRwyEEzi.js +47 -0
- package/dist/assets/{useWindowTitle-qt23iibv.js → useWindowTitle-CnF4pKz0.js} +3 -3
- package/dist/assets/{vendor-codemirror-language-data-DVYP3hf_.js → vendor-codemirror-language-data-qBLdzwMc.js} +2 -2
- package/dist/assets/{web-components-P2py0R8j.js → web-components-BhO08KaW.js} +4 -4
- package/dist/index.html +4 -4
- package/dist/mini-chat.html +3 -3
- package/dist/mobile.html +3 -3
- package/package.json +1 -1
- package/server/index.js +6 -1
- package/server/lib/package-manager.js +44 -65
- package/server/lib/package-manager.test.js +27 -0
- package/server/lib/pi/routes.js +19 -1
- package/server/lib/pi/routes.test.js +36 -0
- package/server/lib/pi/session-daemon/DOCUMENTATION.md +6 -5
- package/server/lib/pi/session-daemon/daemon-process.js +4 -1
- package/server/lib/pi/session-daemon/session-daemon.js +149 -24
- package/server/lib/pi/session-daemon/session-daemon.test.js +232 -0
- package/server/lib/pi/session-daemon/session-jsonl.js +258 -6
- package/server/lib/pi/session-daemon/session-jsonl.test.js +69 -1
- package/server/lib/pi/session-daemon/supervisor.js +41 -21
- package/server/lib/pi/session-daemon/supervisor.test.js +28 -0
- package/dist/assets/CommitSection-DSFuOWuL.js +0 -1
- package/dist/assets/DiagramEditor-D6W6zeV7.js +0 -1
- package/dist/assets/DiagramView-r89Qi5ON.js +0 -1
- package/dist/assets/JsonTreeView-C6qt2uIj.js +0 -1
- package/dist/assets/PatchDiff-D6In_lUU.js +0 -1
- package/dist/assets/SessionSwitcherDropdown-CvE7hsxG.js +0 -1
- package/dist/assets/SettingsWindow-B8IV35H4.js +0 -1
- package/dist/assets/ToolPartDiffPreview-rlz2ipa2.js +0 -8
- package/dist/assets/collapsible-QoPrpZP_.js +0 -1
- package/dist/assets/computeVirtualFileMetrics-CInbUZiB.js +0 -3
- package/dist/assets/contextFileOpenGuard-BOtkNMZy.js +0 -1
- package/dist/assets/index-4sRd3SSM.js +0 -1
- package/dist/assets/index-NYG3k04a.css +0 -1
- package/dist/assets/isDiffPlainText-BquUayus.js +0 -1
- package/dist/assets/isFilePlainText-D4iUTBdr.js +0 -1
- package/dist/assets/main-Cp3YZZm1.js +0 -2
- package/dist/assets/main-CyshB9VI.js +0 -134
- package/dist/assets/miniChat-C_A4lnrT.js +0 -2
- package/dist/assets/mobile-2dXx6cUI.js +0 -2
- package/dist/assets/renderElectronMiniChatApp-HL15h5xo.js +0 -1
- package/dist/assets/renderMobileApp-w6YFCJhq.js +0 -23
- package/dist/assets/runtimeEndpointReset-DiVqZFZB.js +0 -46
- package/dist/assets/useOpenInAppsStore-DVr2oGEm.js +0 -1
- /package/dist/assets/{runtimeEndpointReset-BV3KOtdA.css → usePanelSlide-BV3KOtdA.css} +0 -0
|
@@ -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 {
|
|
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 }) =>
|
|
84
|
-
sessionCwd,
|
|
85
|
-
|
|
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
|
|
339
|
-
|
|
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) {
|
|
@@ -409,7 +425,7 @@ export function createSessionDaemon({
|
|
|
409
425
|
throw new SessionDaemonProtocolError('INVALID_SESSION', 'Pi returned an invalid active session.');
|
|
410
426
|
}
|
|
411
427
|
manager.appendSessionInfo(title);
|
|
412
|
-
publish('session.updated', { title }, sessionId, targetDir);
|
|
428
|
+
publish('session.updated', { title: redactAttachmentPaths(title) }, sessionId, targetDir);
|
|
413
429
|
return;
|
|
414
430
|
}
|
|
415
431
|
|
|
@@ -421,13 +437,28 @@ export function createSessionDaemon({
|
|
|
421
437
|
}
|
|
422
438
|
await validatePiSessionJsonlFile(target.path);
|
|
423
439
|
renamePersistedSession({ sessionFile: target.path, title, cwd: targetDir });
|
|
424
|
-
publish('session.updated', { title }, sessionId, targetDir);
|
|
440
|
+
publish('session.updated', { title: redactAttachmentPaths(title) }, sessionId, targetDir);
|
|
425
441
|
};
|
|
426
442
|
|
|
427
443
|
const findPersistedSession = async (sessionId, requestedDirectory) => {
|
|
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
|
|
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.
|
|
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
|
|
533
|
-
|
|
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
|
|
576
|
-
|
|
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)
|
|
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
|
|
|
@@ -664,7 +781,14 @@ export function createSessionDaemon({
|
|
|
664
781
|
runtime = newRuntime;
|
|
665
782
|
activeDirectory = targetCwd;
|
|
666
783
|
rememberRuntimeSession();
|
|
667
|
-
|
|
784
|
+
const created = projectActiveSession(newRuntime, targetCwd);
|
|
785
|
+
const createdTitle = created.session.title
|
|
786
|
+
|| (typeof payload.title === 'string' ? payload.title.trim() : '')
|
|
787
|
+
|| newRuntime.session?.sessionManager?.getSessionName?.();
|
|
788
|
+
if (createdTitle) {
|
|
789
|
+
publish('session.updated', { title: redactAttachmentPaths(createdTitle) }, created.session.id, targetCwd);
|
|
790
|
+
}
|
|
791
|
+
return created;
|
|
668
792
|
};
|
|
669
793
|
|
|
670
794
|
const listProviders = async (requestedDirectory) => {
|
|
@@ -1320,7 +1444,7 @@ export function createSessionDaemon({
|
|
|
1320
1444
|
const derived = deriveSessionTitle(payload.text);
|
|
1321
1445
|
if (derived && typeof manager?.appendSessionInfo === 'function') {
|
|
1322
1446
|
manager.appendSessionInfo(derived);
|
|
1323
|
-
publish('session.updated', { title: derived }, payload.sessionId, activeRuntime.cwd);
|
|
1447
|
+
publish('session.updated', { title: redactAttachmentPaths(derived) }, payload.sessionId, activeRuntime.cwd);
|
|
1324
1448
|
}
|
|
1325
1449
|
}
|
|
1326
1450
|
|
|
@@ -1371,17 +1495,18 @@ export function createSessionDaemon({
|
|
|
1371
1495
|
const deleteSession = async (sessionId, requestedDirectory) => {
|
|
1372
1496
|
const active = runtimeRegistry?.findBySessionId(sessionId);
|
|
1373
1497
|
let targetDir = requestedDirectory ? await resolveDirectory(requestedDirectory) : active?.cwd || activeDirectory || cwd;
|
|
1498
|
+
const activeSessionFile = active?.session?.sessionManager?.getSessionFile?.();
|
|
1374
1499
|
if (active) {
|
|
1375
1500
|
if (active.session?.isStreaming) await active.session.abort();
|
|
1376
1501
|
await runtimeRegistry?.dispose(active);
|
|
1377
1502
|
if (runtime === active) runtime = undefined;
|
|
1378
1503
|
}
|
|
1379
|
-
|
|
1504
|
+
if (active && typeof activeSessionFile === 'string' && activeSessionFile.length > 0) {
|
|
1505
|
+
await rm(activeSessionFile, { force: true });
|
|
1506
|
+
} else if (!active) {
|
|
1380
1507
|
const { target, directory } = await findPersistedSession(sessionId, targetDir);
|
|
1381
1508
|
targetDir = directory;
|
|
1382
1509
|
await rm(target.path, { force: false });
|
|
1383
|
-
} catch (error) {
|
|
1384
|
-
if (!active || error?.code !== 'INVALID_SESSION') throw error;
|
|
1385
1510
|
}
|
|
1386
1511
|
publish('session.lifecycle', { state: 'idle', deleted: true }, sessionId, targetDir);
|
|
1387
1512
|
};
|
|
@@ -45,6 +45,7 @@ class FakeSession {
|
|
|
45
45
|
getLeafId: () => 'fake-entry',
|
|
46
46
|
getTree: () => [{ entry: { id: 'fake-entry', parentId: undefined, timestamp: '2026-01-01T00:00:00.000Z' }, children: [] }],
|
|
47
47
|
appendSessionInfo: (name) => this.names.push(name),
|
|
48
|
+
getSessionName: () => this.names[this.names.length - 1],
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -389,6 +390,93 @@ describe('Pi session daemon spike', () => {
|
|
|
389
390
|
await client.close();
|
|
390
391
|
});
|
|
391
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
|
+
|
|
392
480
|
it('does not include in-memory sessions from another directory when listing a newly selected project directory', async () => {
|
|
393
481
|
const rootA = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-a-'));
|
|
394
482
|
const rootB = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-b-'));
|
|
@@ -465,6 +553,29 @@ describe('Pi session daemon spike', () => {
|
|
|
465
553
|
await client.close();
|
|
466
554
|
});
|
|
467
555
|
|
|
556
|
+
it('publishes session.updated when a session is first prompted', async () => {
|
|
557
|
+
const root = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-'));
|
|
558
|
+
const endpoint = testDaemonEndpoint(root);
|
|
559
|
+
daemon = createSessionDaemon({
|
|
560
|
+
endpoint,
|
|
561
|
+
credential,
|
|
562
|
+
cwd: root,
|
|
563
|
+
createRuntime: async () => new FakeRuntime({ cwd: root, session: new FakeSession('pi-session-old') }),
|
|
564
|
+
});
|
|
565
|
+
await daemon.start();
|
|
566
|
+
|
|
567
|
+
const client = connectClient(endpoint);
|
|
568
|
+
await client.authenticate();
|
|
569
|
+
await client.request('sessions.create', { cwd: root });
|
|
570
|
+
const promptedUpdated = client.next((frame) => frame.event === 'session.updated' && frame.payload?.title === 'Inspect this report');
|
|
571
|
+
await client.request('sessions.prompt', { sessionId: 'pi-session-new', text: 'Inspect this report\n\nDetails' });
|
|
572
|
+
await expect(promptedUpdated).resolves.toMatchObject({
|
|
573
|
+
event: 'session.updated',
|
|
574
|
+
payload: { sessionId: 'pi-session-new', title: 'Inspect this report' },
|
|
575
|
+
});
|
|
576
|
+
await client.close();
|
|
577
|
+
});
|
|
578
|
+
|
|
468
579
|
it('creates and selects a persisted Pi session with supported creation metadata', async () => {
|
|
469
580
|
const root = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-'));
|
|
470
581
|
const endpoint = testDaemonEndpoint(root);
|
|
@@ -1174,6 +1285,127 @@ describe('Pi session daemon spike', () => {
|
|
|
1174
1285
|
await client.close();
|
|
1175
1286
|
});
|
|
1176
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
|
+
|
|
1177
1409
|
it('projects Pi usage in getSession messages and message_end events, omitting malformed payloads', async () => {
|
|
1178
1410
|
const root = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-usage-'));
|
|
1179
1411
|
const endpoint = testDaemonEndpoint(root);
|