@pi-chamber/web 0.2.2 → 0.3.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.
Files changed (71) hide show
  1. package/bin/cli.test.js +6 -8
  2. package/bin/lib/cli-tunnel-capabilities.js +0 -2
  3. package/dist/assets/{CommitSection-DPjTv8fA.js → CommitSection-C336377Z.js} +1 -1
  4. package/dist/assets/{DiagramEditor-ChOor9l9.js → DiagramEditor-gHLCXHtr.js} +1 -1
  5. package/dist/assets/{DiagramView-3hAT86v_.js → DiagramView-DlHzRRBc.js} +1 -1
  6. package/dist/assets/{DiffView-D1dyOiw2.js → DiffView-5wHwK-lg.js} +1 -1
  7. package/dist/assets/{FilesView-BCzghrc7.js → FilesView-5bswI3JZ.js} +6 -6
  8. package/dist/assets/{GitView-CYF1E1p4.js → GitView-BfykGpGh.js} +4 -4
  9. package/dist/assets/{JsonTreeView-CrJO6LNu.js → JsonTreeView-HVwQS7qG.js} +1 -1
  10. package/dist/assets/{MarkdownRendererImpl-BmKjrkGD.js → MarkdownRendererImpl-kjd4e0N8.js} +1 -1
  11. package/dist/assets/{PatchDiff-DgvPLx6x.js → PatchDiff-Dlm4RU8n.js} +1 -1
  12. package/dist/assets/{PierreDiffViewer-InnT0JVA.js → PierreDiffViewer-St32rXlD.js} +1 -1
  13. package/dist/assets/{SessionSwitcherDropdown-C0OdvirK.js → SessionSwitcherDropdown-D5bWfZNA.js} +100 -101
  14. package/dist/assets/SettingsView-FGe5TEQq.js +4 -0
  15. package/dist/assets/SettingsWindow-Ck_uhTnx.js +1 -0
  16. package/dist/assets/{ToolOutputDialog-gzwcb8nI.js → ToolOutputDialog-BCONUv3r.js} +5 -5
  17. package/dist/assets/{ToolPartDiffPreview-C45V5bhy.js → ToolPartDiffPreview-BdVuk2kc.js} +1 -1
  18. package/dist/assets/{VirtualizedFileDiff-uKfnkQWb.js → VirtualizedFileDiff-BsAZM0jo.js} +1 -1
  19. package/dist/assets/{collapsible-Bq8sDiKS.js → collapsible-Wnmq-13H.js} +1 -1
  20. package/dist/assets/{computeVirtualFileMetrics-BPgZ_RSe.js → computeVirtualFileMetrics-skbyL2Az.js} +2 -2
  21. package/dist/assets/{contextFileOpenGuard-CydO7PkI.js → contextFileOpenGuard-BsxQITMZ.js} +1 -1
  22. package/dist/assets/index-8TL2HlaR.js +8 -0
  23. package/dist/assets/{index-BSuNNtBh.js → index-Bfp6eYap.js} +1 -1
  24. package/dist/assets/index-riABQmQ_.css +1 -0
  25. package/dist/assets/{isDiffPlainText-f9b4cKNo.js → isDiffPlainText-dZkfk3yG.js} +1 -1
  26. package/dist/assets/{isFilePlainText-BjESb-gU.js → isFilePlainText-UL_tMQZx.js} +1 -1
  27. package/dist/assets/{main-B508FBXr.js → main-CHQfoyRP.js} +2 -2
  28. package/dist/assets/main-DbnYXZeK.js +133 -0
  29. package/dist/assets/{markdownCore-BFmf8a-O.js → markdownCore-qX3mJFkL.js} +2 -2
  30. package/dist/assets/{miniChat-BTlz8IPL.js → miniChat-3QtCNwHl.js} +2 -2
  31. package/dist/assets/{mobile-DBkrRt80.js → mobile-B_CyfRAX.js} +2 -2
  32. package/dist/assets/renderElectronMiniChatApp-OToslrv-.js +1 -0
  33. package/dist/assets/renderMobileApp-BD_wep_I.js +14 -0
  34. package/dist/assets/{runtimeSurface-Bec4O8FD.js → runtimeSurface-CkdOpqo7.js} +1 -1
  35. package/dist/assets/{useInlineCommentController-CHmIm0Rr.js → useInlineCommentController-DVif5Fr8.js} +2 -2
  36. package/dist/assets/usePanelSlide-C_PJmPXW.js +56 -0
  37. package/dist/assets/{useWindowTitle-CE5N_NeU.js → useWindowTitle-BZVlLMCy.js} +3 -3
  38. package/dist/assets/{vendor-base-ui-DwfTg5wD.js → vendor-base-ui-DL8r2fgQ.js} +1 -1
  39. package/dist/assets/{vendor-codemirror-language-data-q1yHU1R9.js → vendor-codemirror-language-data-C-924Z4o.js} +2 -2
  40. package/dist/assets/{vendor-sonner-dWA7qvjr.js → vendor-sonner-CW8w438f.js} +1 -1
  41. package/dist/assets/{web-components-CLirSfwt.js → web-components-BG-6PEN4.js} +1 -1
  42. package/dist/index.html +4 -4
  43. package/dist/mini-chat.html +3 -3
  44. package/dist/mobile.html +3 -3
  45. package/package.json +1 -1
  46. package/server/lib/package-manager.js +0 -34
  47. package/server/lib/pi/routes.js +52 -4
  48. package/server/lib/pi/routes.test.js +14 -6
  49. package/server/lib/pi/session-daemon/DOCUMENTATION.md +4 -3
  50. package/server/lib/pi/session-daemon/message-entry-aliases.js +100 -0
  51. package/server/lib/pi/session-daemon/runtime-registry.js +3 -0
  52. package/server/lib/pi/session-daemon/session-daemon.js +184 -16
  53. package/server/lib/pi/session-daemon/session-daemon.test.js +310 -5
  54. package/server/lib/pi/session-daemon/supervisor.js +20 -1
  55. package/server/lib/pi/session-daemon/supervisor.test.js +43 -0
  56. package/server/lib/tunnels/DOCUMENTATION.md +6 -7
  57. package/server/lib/tunnels/executable-search.test.js +4 -4
  58. package/server/lib/tunnels/install-help.js +0 -10
  59. package/server/lib/tunnels/install-help.test.js +0 -9
  60. package/server/lib/tunnels/types.js +0 -1
  61. package/dist/assets/SettingsView-JAfC_cBL.js +0 -3
  62. package/dist/assets/SettingsWindow-BBEsjcJE.js +0 -1
  63. package/dist/assets/index-CH7o4-_q.js +0 -8
  64. package/dist/assets/index-o4Eea60G.css +0 -1
  65. package/dist/assets/main-nG4uFyAy.js +0 -133
  66. package/dist/assets/renderElectronMiniChatApp-DW3fSR5w.js +0 -1
  67. package/dist/assets/renderMobileApp-9qkroKty.js +0 -14
  68. package/dist/assets/usePanelSlide-DCX8qQ4K.js +0 -56
  69. package/server/lib/ngrok-tunnel.js +0 -343
  70. package/server/lib/ngrok-tunnel.test.js +0 -50
  71. package/server/lib/tunnels/providers/ngrok.js +0 -32
@@ -18,6 +18,7 @@ import {
18
18
 
19
19
  import { createPiModelConfigStore } from '../model-config-store.js';
20
20
  import { clampThinkingLevel, getSupportedThinkingLevels, isPiThinkingLevel } from '../thinking-levels.js';
21
+ import { createMessageEntryAliases } from './message-entry-aliases.js';
21
22
  import { createSessionRuntimeRegistry } from './runtime-registry.js';
22
23
  import {
23
24
  findPiSessionJsonlById,
@@ -136,9 +137,11 @@ export function createSessionDaemon({
136
137
  const messageStartedAt = new Map();
137
138
  const toolStartedAt = new Map();
138
139
  const latestUserMessageIds = new Map();
140
+ const retryStateBySession = new Map();
139
141
  const sendGenerationBySession = new Map();
140
142
  const streamingRedactionBuffers = new Map();
141
143
  const loginAttempts = new Map();
144
+ const messageEntryAliases = createMessageEntryAliases();
142
145
  const MAX_REPLAY_EVENTS = 1_024;
143
146
 
144
147
  const validateDirectoryPath = async (dir) => {
@@ -238,6 +241,7 @@ export function createSessionDaemon({
238
241
  : getSessionState();
239
242
  if (requestedSessionId && requestedSessionId !== session.sessionId) return;
240
243
  const activeSession = targetRuntime?.session || runtime?.session;
244
+ const retry = session.sessionId ? retryStateBySession.get(session.sessionId) : undefined;
241
245
  const targetDirectory = targetRuntime?.cwd || activeDirectory || cwd;
242
246
  const messages = activeSession ? projectMessageEntries(activeSession, targetDirectory) : [];
243
247
  const lastAssistant = [...messages].reverse().find((entry) => entry.message.role === 'assistant')?.message;
@@ -252,7 +256,8 @@ export function createSessionDaemon({
252
256
  ...(session.sessionId ? { sessionId: session.sessionId } : {}),
253
257
  directory: targetDirectory,
254
258
  isStreaming: session.isStreaming ?? false,
255
- lifecycle: session.isStreaming ? 'busy' : 'idle',
259
+ lifecycle: retry ? 'retry' : session.isStreaming ? 'busy' : 'idle',
260
+ ...(retry ? { retry } : {}),
256
261
  queue: activeSession ? {
257
262
  steering: activeSession.getSteeringMessages?.().length ?? 0,
258
263
  followUp: activeSession.getFollowUpMessages?.().length ?? 0,
@@ -631,7 +636,11 @@ export function createSessionDaemon({
631
636
  };
632
637
 
633
638
  const projectMessageEntries = (session, targetDir = activeDirectory || cwd) => {
634
- const persisted = session?.sessionManager?.getEntries?.();
639
+ // Use the active branch, not the full file. `getEntries()` returns every
640
+ // entry ever written, so a bare `branch()`/`resetLeaf()` would appear to
641
+ // do nothing. `getBranch()` follows the current leaf and is what
642
+ // `navigateTree` and `buildSessionContext` use for the model context.
643
+ const persisted = session?.sessionManager?.getBranch?.() ?? session?.sessionManager?.getEntries?.();
635
644
  const entries = liveProjectionEntries(session, Array.isArray(persisted) ? persisted : []);
636
645
  if (entries.length === 0) return [];
637
646
  const streaming = session?.isStreaming === true;
@@ -723,6 +732,7 @@ export function createSessionDaemon({
723
732
  ?? (model?.provider && model?.id ? { providerId: model.provider, modelId: model.id } : undefined);
724
733
  const sessionThinking = lastAssistant?.thinkingLevel || session.thinkingLevel;
725
734
  const isStreaming = session.isStreaming === true;
735
+ const retry = retryStateBySession.get(session.sessionId);
726
736
  return {
727
737
  session: {
728
738
  id: session.sessionId, directory: targetDir, createdAt, updatedAt: createdAt,
@@ -734,7 +744,8 @@ export function createSessionDaemon({
734
744
  messages,
735
745
  lastSequence: sequence,
736
746
  isStreaming,
737
- lifecycle: isStreaming ? 'busy' : 'idle',
747
+ lifecycle: retry ? 'retry' : isStreaming ? 'busy' : 'idle',
748
+ ...(retry ? { retry } : {}),
738
749
  };
739
750
  };
740
751
 
@@ -820,6 +831,84 @@ export function createSessionDaemon({
820
831
  return { providers: [...providers.values()] };
821
832
  };
822
833
 
834
+ let refreshProvidersInflight = null;
835
+ const refreshProviders = async (requestedDirectory) => {
836
+ if (refreshProvidersInflight) return refreshProvidersInflight;
837
+ const task = (async () => {
838
+ const signal = typeof AbortSignal.timeout === 'function' ? AbortSignal.timeout(15_000) : undefined;
839
+ const runtimes = new Set();
840
+ for (const services of servicesCache.values()) {
841
+ const mr = services?.modelRuntime;
842
+ if (mr) runtimes.add(mr);
843
+ }
844
+ if (runtime?.session?.modelRuntime) runtimes.add(runtime.session.modelRuntime);
845
+ if (runtime?.services?.modelRuntime) runtimes.add(runtime.services.modelRuntime);
846
+ if (runtimeRegistry?.listAll) {
847
+ try {
848
+ for (const tracked of runtimeRegistry.listAll()) {
849
+ const mr = tracked?.session?.modelRuntime ?? tracked?.services?.modelRuntime;
850
+ if (mr) runtimes.add(mr);
851
+ }
852
+ } catch {}
853
+ }
854
+ if (runtimes.size === 0) {
855
+ const active = await ensureRuntime(requestedDirectory ? await resolveDirectory(requestedDirectory) : undefined);
856
+ const mr = active?.session?.modelRuntime ?? active?.services?.modelRuntime;
857
+ if (mr) runtimes.add(mr);
858
+ }
859
+ const dummyMap = new Map();
860
+ for (const mr of runtimes) {
861
+ try {
862
+ const providers = typeof mr.getProviders === 'function' ? mr.getProviders() : [];
863
+ for (const provider of providers) {
864
+ const auth = mr.getProviderAuthStatus?.(provider.id);
865
+ if (auth?.configured === true) continue;
866
+ try {
867
+ await mr.setRuntimeApiKey(provider.id, 'pichamber-catalog-refresh');
868
+ let list = dummyMap.get(mr);
869
+ if (!list) { list = []; dummyMap.set(mr, list); }
870
+ list.push(provider.id);
871
+ } catch {}
872
+ }
873
+ } catch {}
874
+ }
875
+ const errors = new Map();
876
+ let aborted = false;
877
+ try {
878
+ await Promise.all([...runtimes].map(async (mr) => {
879
+ try {
880
+ const result = await mr.refresh({ allowNetwork: true, force: true, ...(signal ? { signal } : {}) });
881
+ if (result?.aborted) aborted = true;
882
+ if (result?.errors) {
883
+ for (const [providerId, err] of result.errors) errors.set(providerId, err);
884
+ }
885
+ } catch (error) {
886
+ if (error?.code === 'PI_MODEL_CONFIG_INVALID') throw error;
887
+ errors.set('_global', error);
888
+ }
889
+ }));
890
+ } finally {
891
+ for (const [mr, ids] of dummyMap.entries()) {
892
+ for (const id of ids) {
893
+ try { await mr.removeRuntimeApiKey(id); } catch {}
894
+ }
895
+ }
896
+ }
897
+ if (errors.has('_global') && runtimes.size > 0) {
898
+ const globalError = errors.get('_global');
899
+ if (globalError?.code === 'PI_MODEL_CONFIG_INVALID') {
900
+ throw new SessionDaemonProtocolError('PI_MODEL_CONFIG_INVALID', 'Pi models configuration is invalid.');
901
+ }
902
+ }
903
+ const catalog = await listProviders(requestedDirectory);
904
+ return catalog;
905
+ })().finally(() => {
906
+ refreshProvidersInflight = null;
907
+ });
908
+ refreshProvidersInflight = task;
909
+ return task;
910
+ };
911
+
823
912
  const getProviderConfiguration = async (providerId) => {
824
913
  if (typeof providerId !== 'string' || providerId.length === 0 || providerId.length > 256) {
825
914
  throw new SessionDaemonProtocolError('INVALID_ARGUMENT', 'The requested provider is invalid.');
@@ -1101,9 +1190,21 @@ export function createSessionDaemon({
1101
1190
  if (!loader || typeof loader.getSkills !== 'function' || typeof loader.getPrompts !== 'function' || typeof loader.getAgentsFiles !== 'function') {
1102
1191
  throw new SessionDaemonProtocolError('DAEMON_REQUEST_FAILED', 'Pi resource discovery is unavailable.');
1103
1192
  }
1104
- const skills = loader.getSkills().skills.map((skill) => ({
1105
- id: resourceId('skill', skill.filePath),
1106
- kind: 'skill', name: skill.name, ...(skill.description ? { description: skill.description } : {}), location: resourceLocation(skill.sourceInfo), editable: false,
1193
+ const skills = await Promise.all(loader.getSkills().skills.map(async (skill) => {
1194
+ let content = '';
1195
+ try {
1196
+ content = await readFile(skill.filePath, 'utf8');
1197
+ } catch {}
1198
+ return {
1199
+ id: resourceId('skill', skill.filePath),
1200
+ kind: 'skill',
1201
+ name: skill.name,
1202
+ ...(skill.description ? { description: skill.description } : {}),
1203
+ location: resourceLocation(skill.sourceInfo),
1204
+ editable: false,
1205
+ ...(content ? { content } : {}),
1206
+ filePath: skill.filePath,
1207
+ };
1107
1208
  }));
1108
1209
  const prompts = loader.getPrompts().prompts.map((prompt) => ({
1109
1210
  id: resourceId('prompt', prompt.filePath),
@@ -1508,6 +1609,10 @@ export function createSessionDaemon({
1508
1609
  targetDir = directory;
1509
1610
  await rm(target.path, { force: false });
1510
1611
  }
1612
+ messageEntryAliases.clearSession({ cwd: active?.cwd || targetDir, sessionId });
1613
+ retryStateBySession.delete(sessionId);
1614
+ latestUserMessageIds.delete(sessionId);
1615
+ latestAssistantMessageIds.delete(sessionId);
1511
1616
  publish('session.lifecycle', { state: 'idle', deleted: true }, sessionId, targetDir);
1512
1617
  };
1513
1618
 
@@ -1522,6 +1627,7 @@ export function createSessionDaemon({
1522
1627
  ? content.filter((part) => part?.type === 'text').map((part) => part.text).join('')
1523
1628
  : '');
1524
1629
  const messageId = `user-${sessionId}-${sequence + 1}`;
1630
+ messageEntryAliases.retain({ cwd: directory, sessionId, syntheticMessageId: messageId, message: event.message });
1525
1631
  latestUserMessageIds.set(sessionId, messageId);
1526
1632
  publish('assistant.message.start', {
1527
1633
  messageId,
@@ -1531,6 +1637,7 @@ export function createSessionDaemon({
1531
1637
  }, sessionId, directory);
1532
1638
  } else if (event.message?.role === 'assistant') {
1533
1639
  const messageId = `assistant-${sessionId}-${sequence + 1}`;
1640
+ messageEntryAliases.retain({ cwd: directory, sessionId, syntheticMessageId: messageId, message: event.message });
1534
1641
  clearStreamingRedactionBuffers(sessionId);
1535
1642
  streamingMessageIds.set(sessionId, messageId);
1536
1643
  latestAssistantMessageIds.set(sessionId, messageId);
@@ -1559,6 +1666,19 @@ export function createSessionDaemon({
1559
1666
  break;
1560
1667
  }
1561
1668
  case 'message_end': {
1669
+ const eventRuntime = runtimeRegistry?.get({ cwd: directory, sessionId });
1670
+ const syntheticMessageId = event.message?.role === 'assistant'
1671
+ ? streamingMessageIds.get(sessionId) ?? latestAssistantMessageIds.get(sessionId)
1672
+ : event.message?.role === 'user'
1673
+ ? latestUserMessageIds.get(sessionId)
1674
+ : undefined;
1675
+ messageEntryAliases.observeMessageEnd({
1676
+ cwd: directory,
1677
+ sessionId,
1678
+ syntheticMessageId,
1679
+ message: event.message,
1680
+ sessionManager: eventRuntime?.session?.sessionManager,
1681
+ });
1562
1682
  if (event.message?.role === 'assistant') {
1563
1683
  const content = Array.isArray(event.message.content) ? event.message.content : [];
1564
1684
  const messageId = streamingMessageIds.get(sessionId) ?? latestAssistantMessageIds.get(sessionId) ?? `assistant-${sessionId}`;
@@ -1571,6 +1691,7 @@ export function createSessionDaemon({
1571
1691
  text: redactAttachmentPaths(content.filter((part) => part?.type === 'text').map((part) => part.text).join('')),
1572
1692
  thinking: redactAttachmentPaths(content.filter((part) => part?.type === 'thinking').map((part) => part.thinking).join('')),
1573
1693
  durationMs,
1694
+ ...(content.some((part) => part?.type === 'toolCall') ? { continuing: true } : {}),
1574
1695
  ...(event.message.errorMessage ? { error: { code: 'ASSISTANT_ERROR', message: redactAttachmentPaths(event.message.errorMessage) } } : {}),
1575
1696
  ...(usage ? { usage } : {}),
1576
1697
  }, sessionId, directory);
@@ -1634,20 +1755,35 @@ export function createSessionDaemon({
1634
1755
  break;
1635
1756
  case 'agent_start':
1636
1757
  clearIdleDisposal(sessionId);
1758
+ retryStateBySession.delete(sessionId);
1637
1759
  publish('session.lifecycle', { state: 'busy' }, sessionId, directory);
1638
1760
  break;
1639
1761
  case 'agent_end': {
1640
- latestUserMessageIds.delete(sessionId);
1641
- latestAssistantMessageIds.delete(sessionId);
1642
1762
  const finalMessage = event.messages?.at?.(-1);
1643
1763
  if (finalMessage?.role === 'assistant' && finalMessage.stopReason === 'aborted') {
1644
1764
  publish('session.interrupted', { reason: 'user-abort', streaming: false }, sessionId, directory);
1645
- } else if (finalMessage?.role === 'assistant' && typeof finalMessage.errorMessage === 'string') {
1765
+ } else if (finalMessage?.role === 'assistant' && typeof finalMessage.errorMessage === 'string' && event.willRetry !== true) {
1646
1766
  publish('session.error', { code: 'ASSISTANT_ERROR', message: redactAttachmentPaths(finalMessage.errorMessage) }, sessionId, directory);
1647
1767
  }
1648
1768
  break;
1649
1769
  }
1770
+ case 'auto_retry_start': {
1771
+ const retry = {
1772
+ attempt: event.attempt,
1773
+ next: Date.now() + event.delayMs,
1774
+ message: redactAttachmentPaths(event.errorMessage),
1775
+ };
1776
+ retryStateBySession.set(sessionId, retry);
1777
+ publish('session.lifecycle', { state: 'retry', ...retry }, sessionId, directory);
1778
+ break;
1779
+ }
1780
+ case 'auto_retry_end':
1781
+ retryStateBySession.delete(sessionId);
1782
+ break;
1650
1783
  case 'agent_settled':
1784
+ retryStateBySession.delete(sessionId);
1785
+ latestUserMessageIds.delete(sessionId);
1786
+ latestAssistantMessageIds.delete(sessionId);
1651
1787
  publish('session.lifecycle', { state: 'idle' }, sessionId, directory);
1652
1788
  scheduleIdleDisposal(sessionId);
1653
1789
  break;
@@ -1684,7 +1820,7 @@ export function createSessionDaemon({
1684
1820
  'projects.list', 'projects.select', 'sessions.list', 'sessions.create', 'sessions.open', 'sessions.rename', 'sessions.delete',
1685
1821
  'sessions.tree', 'sessions.navigate', 'sessions.fork', 'sessions.clone', 'sessions.prompt',
1686
1822
  'sessions.steer', 'sessions.followUp', 'sessions.abort', 'sessions.setModel',
1687
- 'sessions.setThinking', 'sessions.compact', 'providers.list', 'providers.config.get', 'providers.models.set', 'providers.status', 'providers.login',
1823
+ 'sessions.setThinking', 'sessions.compact', 'providers.list', 'providers.refresh', 'providers.config.get', 'providers.models.set', 'providers.status', 'providers.login',
1688
1824
  'providers.login.respond', 'providers.login.status', 'providers.logout', 'settings.get', 'settings.set',
1689
1825
  'resources.list', 'resources.update', 'resources.prompts.create', 'resources.prompts.delete',
1690
1826
  ],
@@ -1716,6 +1852,11 @@ export function createSessionDaemon({
1716
1852
  writeFrame(socket, { protocolVersion: PROTOCOL_VERSION, kind: 'response', requestId: message.requestId, result });
1717
1853
  return;
1718
1854
  }
1855
+ case 'providers.refresh': {
1856
+ const result = await refreshProviders(message.payload?.directory);
1857
+ writeFrame(socket, { protocolVersion: PROTOCOL_VERSION, kind: 'response', requestId: message.requestId, result });
1858
+ return;
1859
+ }
1719
1860
  case 'providers.config.get': {
1720
1861
  const result = await getProviderConfiguration(message.payload?.providerId);
1721
1862
  writeFrame(socket, { protocolVersion: PROTOCOL_VERSION, kind: 'response', requestId: message.requestId, result });
@@ -1817,18 +1958,40 @@ export function createSessionDaemon({
1817
1958
  }
1818
1959
  case 'sessions.navigate': {
1819
1960
  const activeRuntime = await activateSession(message.payload?.sessionId, message.payload?.directory || message.payload?.cwd);
1820
- const messageId = message.payload?.messageId;
1821
- if (typeof messageId !== 'string' || messageId.length === 0) throw new SessionDaemonProtocolError('INVALID_ARGUMENT', 'The requested tree entry is invalid.');
1961
+ const requestedId = message.payload?.messageId;
1962
+ if (typeof requestedId !== 'string' || requestedId.length === 0) throw new SessionDaemonProtocolError('INVALID_ARGUMENT', 'The requested tree entry is invalid.');
1963
+ const messageId = messageEntryAliases.resolve({
1964
+ cwd: activeRuntime.cwd,
1965
+ sessionId: message.payload.sessionId,
1966
+ requestedId,
1967
+ sessionManager: activeRuntime.session.sessionManager,
1968
+ });
1969
+ const previousLeafId = activeRuntime.session.sessionManager?.getLeafId?.() ?? null;
1822
1970
  const result = await activeRuntime.session.navigateTree(messageId);
1823
- if (result.cancelled) throw new SessionDaemonProtocolError('SESSION_TREE_NOT_FOUND', 'Pi cancelled tree navigation.');
1824
- writeFrame(socket, { protocolVersion: PROTOCOL_VERSION, kind: 'response', requestId: message.requestId, result: projectActiveSession(activeRuntime, activeRuntime.cwd) });
1971
+ if (result?.cancelled) throw new SessionDaemonProtocolError('SESSION_TREE_NOT_FOUND', 'Pi cancelled tree navigation.');
1972
+ const newLeafId = activeRuntime.session.sessionManager?.getLeafId?.() ?? null;
1973
+ const navigation = {
1974
+ targetEntryId: messageId,
1975
+ previousLeafId: typeof previousLeafId === 'string' ? previousLeafId : null,
1976
+ newLeafId: typeof newLeafId === 'string' ? newLeafId : null,
1977
+ ...(typeof result?.editorText === 'string' && result.editorText.length > 0 ? { editorText: result.editorText } : {}),
1978
+ };
1979
+ writeFrame(socket, { protocolVersion: PROTOCOL_VERSION, kind: 'response', requestId: message.requestId, result: { ...projectActiveSession(activeRuntime, activeRuntime.cwd), navigation } });
1825
1980
  return;
1826
1981
  }
1827
1982
  case 'sessions.fork':
1828
1983
  case 'sessions.clone': {
1829
1984
  const activeRuntime = await activateSession(message.payload?.sessionId, message.payload?.directory || message.payload?.cwd);
1830
- const entryId = message.command === 'sessions.fork' ? message.payload?.messageId : activeRuntime.session.sessionManager?.getLeafId?.();
1831
- if (typeof entryId !== 'string' || entryId.length === 0) throw new SessionDaemonProtocolError('SESSION_TREE_NOT_FOUND', 'The Pi session has no fork point.');
1985
+ const requestedId = message.command === 'sessions.fork' ? message.payload?.messageId : activeRuntime.session.sessionManager?.getLeafId?.();
1986
+ if (typeof requestedId !== 'string' || requestedId.length === 0) throw new SessionDaemonProtocolError('SESSION_TREE_NOT_FOUND', 'The Pi session has no fork point.');
1987
+ const entryId = message.command === 'sessions.fork'
1988
+ ? messageEntryAliases.resolve({
1989
+ cwd: activeRuntime.cwd,
1990
+ sessionId: message.payload.sessionId,
1991
+ requestedId,
1992
+ sessionManager: activeRuntime.session.sessionManager,
1993
+ })
1994
+ : requestedId;
1832
1995
  const result = await activeRuntime.fork(entryId, { position: 'at' });
1833
1996
  if (result.cancelled) throw new SessionDaemonProtocolError('SESSION_CREATE_CANCELLED', 'Pi cancelled session creation.');
1834
1997
  rememberRuntimeSession();
@@ -1996,6 +2159,7 @@ export function createSessionDaemon({
1996
2159
  if (platform !== 'win32') await chmod(endpoint, 0o600);
1997
2160
  started = true;
1998
2161
  } catch (error) {
2162
+ messageEntryAliases.clear();
1999
2163
  await disposeRuntime();
2000
2164
  server = undefined;
2001
2165
  throw error;
@@ -2009,8 +2173,12 @@ export function createSessionDaemon({
2009
2173
  clearTimeout(attempt.expiry);
2010
2174
  }
2011
2175
  loginAttempts.clear();
2176
+ retryStateBySession.clear();
2177
+ latestUserMessageIds.clear();
2178
+ latestAssistantMessageIds.clear();
2012
2179
  for (const client of clients) client.destroy();
2013
2180
  clients.clear();
2181
+ messageEntryAliases.clear();
2014
2182
  await new Promise((resolve, reject) => {
2015
2183
  server.close((error) => error ? reject(error) : resolve());
2016
2184
  });