@pi-chamber/web 0.3.0 → 0.3.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.
- package/bin/lib/cli-startup.js +53 -10
- package/bin/lib/cli-startup.test.js +42 -0
- package/dist/assets/{CommitSection-BpbMC0et.js → CommitSection-DLIvjYbM.js} +1 -1
- package/dist/assets/{DiagramEditor-dFTMKHuO.js → DiagramEditor-Tw3ZWHML.js} +1 -1
- package/dist/assets/{DiagramView-DVw_8Px1.js → DiagramView-CK6R7taL.js} +1 -1
- package/dist/assets/{DiffView-CkoY1oAM.js → DiffView-vrxi5oWp.js} +2 -2
- package/dist/assets/{FilesView-C0hyg2pa.js → FilesView-BD_h3EVr.js} +6 -6
- package/dist/assets/{GitView-D36hpDsb.js → GitView-BwNKqK0K.js} +4 -4
- package/dist/assets/{JsonTreeView-BF8S-bPN.js → JsonTreeView-CmY7QgEX.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-BVNChdig.js → MarkdownRendererImpl-CpI-25Xk.js} +1 -1
- package/dist/assets/{PatchDiff-B--OaNB1.js → PatchDiff-CrIWbYj7.js} +1 -1
- package/dist/assets/{PierreDiffViewer-0Qkp9ICt.js → PierreDiffViewer-B410mdbq.js} +2 -2
- package/dist/assets/{SessionSwitcherDropdown-cFWgy1td.js → SessionSwitcherDropdown-DROjbyIu.js} +100 -100
- package/dist/assets/SettingsView-CsJItYZh.js +4 -0
- package/dist/assets/{SettingsWindow-BuJCvYoR.js → SettingsWindow-C9s4AX72.js} +1 -1
- package/dist/assets/{ToolOutputDialog-Bi9BoEe7.js → ToolOutputDialog-b_R4tAZu.js} +5 -5
- package/dist/assets/{ToolPartDiffPreview-DGX7N4zr.js → ToolPartDiffPreview-CfnliaE9.js} +1 -1
- package/dist/assets/{VirtualizedFileDiff-CxraYoDZ.js → VirtualizedFileDiff-CQz2fSZI.js} +4 -4
- package/dist/assets/{collapsible-d4-HChej.js → collapsible-CR4U9tjw.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-BuD14qFL.js → computeVirtualFileMetrics-C7vXPUYT.js} +2 -2
- package/dist/assets/{contextFileOpenGuard-DirV-hCs.js → contextFileOpenGuard-DtTHRnq1.js} +1 -1
- package/dist/assets/{index-DEYblXCW.js → index-BAqpUCjs.js} +5 -5
- package/dist/assets/{index-BWmmf5yJ.js → index-DZG82DE1.js} +1 -1
- package/dist/assets/{index-DLKBsLKY.css → index-riABQmQ_.css} +1 -1
- package/dist/assets/{isDiffPlainText-LoMmNzmX.js → isDiffPlainText-zTq-4A04.js} +1 -1
- package/dist/assets/{isFilePlainText-3Hj9i02t.js → isFilePlainText-CLgIdzE5.js} +1 -1
- package/dist/assets/{main-BsszlAdQ.js → main-BVWtJYSk.js} +2 -2
- package/dist/assets/main-DXYHsqzA.js +133 -0
- package/dist/assets/{markdownCore-sGxk8jrc.js → markdownCore-CN0pApQE.js} +2 -2
- package/dist/assets/{miniChat-C6SDQL4G.js → miniChat-BrbRq5NQ.js} +2 -2
- package/dist/assets/{mobile-DBW3XDmZ.js → mobile-DTPtCn5N.js} +2 -2
- package/dist/assets/{renderElectronMiniChatApp-B1_fTdN-.js → renderElectronMiniChatApp-CM8qBaye.js} +1 -1
- package/dist/assets/{renderMobileApp-CFxZc_1a.js → renderMobileApp-KS_pt-qc.js} +4 -4
- package/dist/assets/{runtimeSurface-9aJdtQbh.js → runtimeSurface-CkHYYg0E.js} +1 -1
- package/dist/assets/{useInlineCommentController-Cl--s6vE.js → useInlineCommentController-CwNNjgc7.js} +2 -2
- package/dist/assets/usePanelSlide-DkorgQJ5.js +56 -0
- package/dist/assets/{useWindowTitle-C-AZD713.js → useWindowTitle-B9wt_3ZQ.js} +3 -3
- package/dist/assets/{vendor-codemirror-language-data-HC9JT4I3.js → vendor-codemirror-language-data-x89A7Wy0.js} +2 -2
- package/dist/assets/{vendor-sonner-dWA7qvjr.js → vendor-sonner-CW8w438f.js} +1 -1
- package/dist/assets/{web-components-skJ_VpnT.js → web-components-BEPfE4kU.js} +2 -2
- 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 +33 -0
- package/server/lib/pi/routes.js +24 -2
- package/server/lib/pi/routes.test.js +14 -6
- package/server/lib/pi/session-daemon/DOCUMENTATION.md +4 -3
- package/server/lib/pi/session-daemon/message-entry-aliases.js +100 -0
- package/server/lib/pi/session-daemon/session-daemon.js +176 -46
- package/server/lib/pi/session-daemon/session-daemon.test.js +364 -6
- package/server/lib/pi/session-daemon/supervisor.js +20 -1
- package/server/lib/pi/session-daemon/supervisor.test.js +43 -0
- package/server/lib/pi/settings-store.js +17 -0
- package/server/lib/pi/settings-store.test.js +33 -0
- package/server/lib/server/core-routes.js +36 -4
- package/server/lib/server/tunnel-service.DOCUMENTATION.md +74 -0
- package/server/lib/server/tunnel-service.js +228 -0
- package/server/lib/server/tunnel-service.test.js +61 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +2 -0
- package/server/lib/ui-auth/ui-auth.js +5 -1
- package/server/lib/ui-auth/ui-auth.test.js +17 -0
- package/dist/assets/SettingsView-D3ZZyjU3.js +0 -4
- package/dist/assets/main-CUvZIN16.js +0 -133
- package/dist/assets/usePanelSlide-D5ffPTGM.js +0 -56
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const scopeKey = (cwd, sessionId) => JSON.stringify([cwd, sessionId]);
|
|
2
|
+
|
|
3
|
+
const stripPartSuffix = (messageId) => {
|
|
4
|
+
const suffixIndex = messageId.indexOf(':');
|
|
5
|
+
return suffixIndex === -1 ? messageId : messageId.slice(0, suffixIndex);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const findPersistedMessageEntry = (sessionManager, message) => {
|
|
9
|
+
const entries = sessionManager?.getEntries?.();
|
|
10
|
+
if (!Array.isArray(entries)) return undefined;
|
|
11
|
+
return entries.find((entry) => entry?.type === 'message' && entry.message === message);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Keeps daemon-published live message ids resolvable after Pi persists those
|
|
16
|
+
* messages under its own session-entry ids.
|
|
17
|
+
*/
|
|
18
|
+
export function createMessageEntryAliases({ scheduleMicrotask = queueMicrotask } = {}) {
|
|
19
|
+
const aliasesByScope = new Map();
|
|
20
|
+
const pendingByMessage = new WeakMap();
|
|
21
|
+
|
|
22
|
+
const getScope = (cwd, sessionId, create = false) => {
|
|
23
|
+
const key = scopeKey(cwd, sessionId);
|
|
24
|
+
let scope = aliasesByScope.get(key);
|
|
25
|
+
if (!scope && create) {
|
|
26
|
+
scope = new Map();
|
|
27
|
+
aliasesByScope.set(key, scope);
|
|
28
|
+
}
|
|
29
|
+
return scope;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const retain = ({ cwd, sessionId, syntheticMessageId, message }) => {
|
|
33
|
+
if (typeof cwd !== 'string' || typeof sessionId !== 'string' || typeof syntheticMessageId !== 'string'
|
|
34
|
+
|| !message || typeof message !== 'object') return;
|
|
35
|
+
const alias = { message };
|
|
36
|
+
getScope(cwd, sessionId, true).set(syntheticMessageId, alias);
|
|
37
|
+
pendingByMessage.set(message, { alias });
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const releasePersisted = (pending, sessionManager) => {
|
|
41
|
+
const entry = findPersistedMessageEntry(sessionManager, pending.alias.message);
|
|
42
|
+
if (!entry || typeof entry.id !== 'string' || entry.id.length === 0) return undefined;
|
|
43
|
+
pending.alias.entryId = entry.id;
|
|
44
|
+
delete pending.alias.message;
|
|
45
|
+
pendingByMessage.delete(entry.message);
|
|
46
|
+
return entry.id;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
retain,
|
|
51
|
+
|
|
52
|
+
observeMessageEnd({ cwd, sessionId, syntheticMessageId, message, sessionManager }) {
|
|
53
|
+
if (typeof syntheticMessageId !== 'string' || !message || typeof message !== 'object') return;
|
|
54
|
+
const alias = getScope(cwd, sessionId)?.get(syntheticMessageId);
|
|
55
|
+
if (!alias) return;
|
|
56
|
+
if (alias.message && alias.message !== message) pendingByMessage.delete(alias.message);
|
|
57
|
+
alias.message = message;
|
|
58
|
+
const pending = { alias };
|
|
59
|
+
pendingByMessage.set(message, pending);
|
|
60
|
+
scheduleMicrotask(() => {
|
|
61
|
+
releasePersisted(pending, sessionManager);
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
resolve({ cwd, sessionId, requestedId, sessionManager }) {
|
|
66
|
+
if (typeof requestedId !== 'string' || requestedId.length === 0) return requestedId;
|
|
67
|
+
const entryId = stripPartSuffix(requestedId);
|
|
68
|
+
if (sessionManager?.getEntry?.(entryId)) return entryId;
|
|
69
|
+
|
|
70
|
+
const alias = getScope(cwd, sessionId)?.get(entryId);
|
|
71
|
+
if (!alias) return entryId;
|
|
72
|
+
if (typeof alias.entryId === 'string' && alias.entryId.length > 0) return alias.entryId;
|
|
73
|
+
if (alias.message) {
|
|
74
|
+
const pending = pendingByMessage.get(alias.message);
|
|
75
|
+
const persistedEntryId = pending ? releasePersisted(pending, sessionManager) : undefined;
|
|
76
|
+
if (persistedEntryId) return persistedEntryId;
|
|
77
|
+
}
|
|
78
|
+
return entryId;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
clearSession({ cwd, sessionId }) {
|
|
82
|
+
const key = scopeKey(cwd, sessionId);
|
|
83
|
+
const scope = aliasesByScope.get(key);
|
|
84
|
+
if (!scope) return;
|
|
85
|
+
for (const alias of scope.values()) {
|
|
86
|
+
if (alias.message) pendingByMessage.delete(alias.message);
|
|
87
|
+
}
|
|
88
|
+
aliasesByScope.delete(key);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
clear() {
|
|
92
|
+
for (const scope of aliasesByScope.values()) {
|
|
93
|
+
for (const alias of scope.values()) {
|
|
94
|
+
if (alias.message) pendingByMessage.delete(alias.message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
aliasesByScope.clear();
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -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,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
validatePiSessionJsonlDirectory,
|
|
27
28
|
validatePiSessionJsonlFile,
|
|
28
29
|
} from './session-jsonl.js';
|
|
30
|
+
import { resolvePiChamberDataDir } from '../pichamber-data-dir.js';
|
|
29
31
|
|
|
30
32
|
const PROTOCOL_VERSION = 1;
|
|
31
33
|
const MAX_FRAME_BYTES = 16 * 1024 * 1024;
|
|
@@ -136,9 +138,12 @@ export function createSessionDaemon({
|
|
|
136
138
|
const messageStartedAt = new Map();
|
|
137
139
|
const toolStartedAt = new Map();
|
|
138
140
|
const latestUserMessageIds = new Map();
|
|
141
|
+
const retryStateBySession = new Map();
|
|
142
|
+
const activeRunStartedAt = new Map();
|
|
139
143
|
const sendGenerationBySession = new Map();
|
|
140
144
|
const streamingRedactionBuffers = new Map();
|
|
141
145
|
const loginAttempts = new Map();
|
|
146
|
+
const messageEntryAliases = createMessageEntryAliases();
|
|
142
147
|
const MAX_REPLAY_EVENTS = 1_024;
|
|
143
148
|
|
|
144
149
|
const validateDirectoryPath = async (dir) => {
|
|
@@ -238,6 +243,7 @@ export function createSessionDaemon({
|
|
|
238
243
|
: getSessionState();
|
|
239
244
|
if (requestedSessionId && requestedSessionId !== session.sessionId) return;
|
|
240
245
|
const activeSession = targetRuntime?.session || runtime?.session;
|
|
246
|
+
const retry = session.sessionId ? retryStateBySession.get(session.sessionId) : undefined;
|
|
241
247
|
const targetDirectory = targetRuntime?.cwd || activeDirectory || cwd;
|
|
242
248
|
const messages = activeSession ? projectMessageEntries(activeSession, targetDirectory) : [];
|
|
243
249
|
const lastAssistant = [...messages].reverse().find((entry) => entry.message.role === 'assistant')?.message;
|
|
@@ -252,7 +258,8 @@ export function createSessionDaemon({
|
|
|
252
258
|
...(session.sessionId ? { sessionId: session.sessionId } : {}),
|
|
253
259
|
directory: targetDirectory,
|
|
254
260
|
isStreaming: session.isStreaming ?? false,
|
|
255
|
-
lifecycle: session.isStreaming ? 'busy' : 'idle',
|
|
261
|
+
lifecycle: retry ? 'retry' : session.isStreaming ? 'busy' : 'idle',
|
|
262
|
+
...(retry ? { retry } : {}),
|
|
256
263
|
queue: activeSession ? {
|
|
257
264
|
steering: activeSession.getSteeringMessages?.().length ?? 0,
|
|
258
265
|
followUp: activeSession.getFollowUpMessages?.().length ?? 0,
|
|
@@ -261,6 +268,8 @@ export function createSessionDaemon({
|
|
|
261
268
|
...(activeSession?.thinkingLevel ? { thinking: activeSession.thinkingLevel } : {}),
|
|
262
269
|
...(typeof lastAssistant?.text === 'string' ? { lastText: lastAssistant.text } : {}),
|
|
263
270
|
...(typeof lastAssistant?.thinking === 'string' ? { lastThinking: lastAssistant.thinking } : {}),
|
|
271
|
+
...(session.sessionId && activeRunStartedAt.has(session.sessionId) ? { runStartedAt: activeRunStartedAt.get(session.sessionId) } : {}),
|
|
272
|
+
serverNow: Date.now(),
|
|
264
273
|
lastSequence: snapshotSequence,
|
|
265
274
|
},
|
|
266
275
|
});
|
|
@@ -727,6 +736,7 @@ export function createSessionDaemon({
|
|
|
727
736
|
?? (model?.provider && model?.id ? { providerId: model.provider, modelId: model.id } : undefined);
|
|
728
737
|
const sessionThinking = lastAssistant?.thinkingLevel || session.thinkingLevel;
|
|
729
738
|
const isStreaming = session.isStreaming === true;
|
|
739
|
+
const retry = retryStateBySession.get(session.sessionId);
|
|
730
740
|
return {
|
|
731
741
|
session: {
|
|
732
742
|
id: session.sessionId, directory: targetDir, createdAt, updatedAt: createdAt,
|
|
@@ -738,11 +748,18 @@ export function createSessionDaemon({
|
|
|
738
748
|
messages,
|
|
739
749
|
lastSequence: sequence,
|
|
740
750
|
isStreaming,
|
|
741
|
-
lifecycle: isStreaming ? 'busy' : 'idle',
|
|
751
|
+
lifecycle: retry ? 'retry' : isStreaming ? 'busy' : 'idle',
|
|
752
|
+
...(retry ? { retry } : {}),
|
|
753
|
+
...(activeRunStartedAt.has(session.sessionId) ? { runStartedAt: activeRunStartedAt.get(session.sessionId) } : {}),
|
|
754
|
+
serverNow: Date.now(),
|
|
742
755
|
};
|
|
743
756
|
};
|
|
744
757
|
|
|
745
758
|
const createSession = async (payload) => {
|
|
759
|
+
const explicitRetryLimit = payload && typeof payload === 'object' ? (payload.maxRetries ?? payload.retryLimit ?? payload.defaultRetryLimit) : undefined;
|
|
760
|
+
if (explicitRetryLimit !== undefined && (!Number.isInteger(explicitRetryLimit) || explicitRetryLimit < 0 || explicitRetryLimit > 10)) {
|
|
761
|
+
throw new SessionDaemonProtocolError('INVALID_ARGUMENT', 'The retry limit must be an integer between 0 and 10.');
|
|
762
|
+
}
|
|
746
763
|
if (!payload || typeof payload !== 'object'
|
|
747
764
|
|| (payload.cwd !== undefined && (typeof payload.cwd !== 'string' || payload.cwd.length === 0))
|
|
748
765
|
|| (payload.title !== undefined && (typeof payload.title !== 'string' || payload.title.trim().length === 0 || payload.title.length > 256))
|
|
@@ -764,6 +781,32 @@ export function createSessionDaemon({
|
|
|
764
781
|
...(parent ? { sessionFile: parent.path } : {}),
|
|
765
782
|
});
|
|
766
783
|
if (!newRuntime.cwd) newRuntime.cwd = targetCwd;
|
|
784
|
+
// Apply default retry limit for new sessions. Explicit per-run overrides win.
|
|
785
|
+
try {
|
|
786
|
+
const settingsManager = newRuntime.services?.settingsManager;
|
|
787
|
+
if (settingsManager && typeof settingsManager.getRetrySettings === 'function') {
|
|
788
|
+
let effective = explicitRetryLimit;
|
|
789
|
+
if (effective === undefined) {
|
|
790
|
+
try {
|
|
791
|
+
const raw = await readFile(join(resolvePiChamberDataDir(), 'pi', 'settings.json'), 'utf8');
|
|
792
|
+
const parsed = JSON.parse(raw);
|
|
793
|
+
if (Number.isInteger(parsed?.defaultRetryLimit)) effective = parsed.defaultRetryLimit;
|
|
794
|
+
} catch {}
|
|
795
|
+
if (effective === undefined) effective = 3;
|
|
796
|
+
}
|
|
797
|
+
if (Number.isInteger(effective) && effective >= 0 && effective <= 10) {
|
|
798
|
+
const current = settingsManager.getRetrySettings().maxRetries;
|
|
799
|
+
if (current !== effective) {
|
|
800
|
+
if (typeof settingsManager.applyOverrides === 'function') {
|
|
801
|
+
settingsManager.applyOverrides({ retry: { maxRetries: effective } });
|
|
802
|
+
if (explicitRetryLimit === undefined) await settingsManager.flush().catch(() => {});
|
|
803
|
+
} else if (settingsManager.globalSettings) {
|
|
804
|
+
settingsManager.globalSettings.retry = { ...(settingsManager.globalSettings.retry ?? {}), maxRetries: effective };
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
} catch {}
|
|
767
810
|
let result = { cancelled: false };
|
|
768
811
|
if (typeof newRuntime.newSession === 'function') {
|
|
769
812
|
result = await newRuntime.newSession({
|
|
@@ -1349,11 +1392,72 @@ export function createSessionDaemon({
|
|
|
1349
1392
|
}
|
|
1350
1393
|
};
|
|
1351
1394
|
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1395
|
+
const attachmentMarkerPattern = /pi-clipboard-/i;
|
|
1396
|
+
const attachmentMarkerSearchPattern = /pi-clipboard-/gi;
|
|
1397
|
+
const attachmentIdPattern = /^[0-9a-f-]{36}$/i;
|
|
1398
|
+
const attachmentBracketStartPattern = /\[Attachment/gi;
|
|
1399
|
+
const attachmentTokenPattern = /pi-clipboard-[0-9a-f-]{36}/i;
|
|
1400
|
+
const isAttachmentPathDelimiter = (character) => /[\s[\](){}"'`,;]/u.test(character);
|
|
1401
|
+
|
|
1402
|
+
const redactAttachmentBrackets = (text) => {
|
|
1403
|
+
let cursor = 0;
|
|
1404
|
+
let output = '';
|
|
1405
|
+
while (cursor < text.length) {
|
|
1406
|
+
attachmentBracketStartPattern.lastIndex = cursor;
|
|
1407
|
+
const bracketStart = attachmentBracketStartPattern.exec(text);
|
|
1408
|
+
if (!bracketStart) {
|
|
1409
|
+
output += text.slice(cursor);
|
|
1410
|
+
break;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
output += text.slice(cursor, bracketStart.index);
|
|
1414
|
+
let bracketEnd = bracketStart.index + bracketStart[0].length;
|
|
1415
|
+
while (bracketEnd < text.length && text[bracketEnd] !== ']' && text[bracketEnd] !== '\r' && text[bracketEnd] !== '\n') bracketEnd += 1;
|
|
1416
|
+
if (text[bracketEnd] === ']') {
|
|
1417
|
+
const candidate = text.slice(bracketStart.index, bracketEnd + 1);
|
|
1418
|
+
output += attachmentTokenPattern.test(candidate) ? '[attachment]' : candidate;
|
|
1419
|
+
cursor = bracketEnd + 1;
|
|
1420
|
+
} else {
|
|
1421
|
+
output += text.slice(bracketStart.index, bracketEnd);
|
|
1422
|
+
cursor = bracketEnd;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
return output;
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
const redactAttachmentPaths = (text) => {
|
|
1429
|
+
if (typeof text !== 'string') return '';
|
|
1430
|
+
|
|
1431
|
+
if (!attachmentMarkerPattern.test(text)) return text;
|
|
1432
|
+
const bracketRedacted = redactAttachmentBrackets(text);
|
|
1433
|
+
let cursor = 0;
|
|
1434
|
+
let output = '';
|
|
1435
|
+
while (cursor < bracketRedacted.length) {
|
|
1436
|
+
attachmentMarkerSearchPattern.lastIndex = cursor;
|
|
1437
|
+
const markerMatch = attachmentMarkerSearchPattern.exec(bracketRedacted);
|
|
1438
|
+
if (!markerMatch) {
|
|
1439
|
+
output += bracketRedacted.slice(cursor);
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const markerIndex = markerMatch.index;
|
|
1444
|
+
const idStart = markerIndex + markerMatch[0].length;
|
|
1445
|
+
const idEnd = idStart + 36;
|
|
1446
|
+
if (!attachmentIdPattern.test(bracketRedacted.slice(idStart, idEnd))) {
|
|
1447
|
+
output += bracketRedacted.slice(cursor, idStart);
|
|
1448
|
+
cursor = idStart;
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
let tokenStart = markerIndex;
|
|
1453
|
+
while (tokenStart > cursor && !isAttachmentPathDelimiter(bracketRedacted[tokenStart - 1])) tokenStart -= 1;
|
|
1454
|
+
let tokenEnd = idEnd;
|
|
1455
|
+
while (tokenEnd < bracketRedacted.length && !isAttachmentPathDelimiter(bracketRedacted[tokenEnd])) tokenEnd += 1;
|
|
1456
|
+
output += `${bracketRedacted.slice(cursor, tokenStart)}[attachment]`;
|
|
1457
|
+
cursor = tokenEnd;
|
|
1458
|
+
}
|
|
1459
|
+
return output;
|
|
1460
|
+
};
|
|
1357
1461
|
|
|
1358
1462
|
const redactAttachmentValues = (value) => {
|
|
1359
1463
|
if (typeof value === 'string') return redactAttachmentPaths(value);
|
|
@@ -1602,7 +1706,12 @@ export function createSessionDaemon({
|
|
|
1602
1706
|
targetDir = directory;
|
|
1603
1707
|
await rm(target.path, { force: false });
|
|
1604
1708
|
}
|
|
1605
|
-
|
|
1709
|
+
messageEntryAliases.clearSession({ cwd: active?.cwd || targetDir, sessionId });
|
|
1710
|
+
retryStateBySession.delete(sessionId);
|
|
1711
|
+
activeRunStartedAt.delete(sessionId);
|
|
1712
|
+
latestUserMessageIds.delete(sessionId);
|
|
1713
|
+
latestAssistantMessageIds.delete(sessionId);
|
|
1714
|
+
publish('session.lifecycle', { state: 'idle', deleted: true, serverNow: Date.now() }, sessionId, targetDir);
|
|
1606
1715
|
};
|
|
1607
1716
|
|
|
1608
1717
|
const publishSessionEvent = (sessionId, event, directory = activeDirectory || cwd) => {
|
|
@@ -1616,6 +1725,7 @@ export function createSessionDaemon({
|
|
|
1616
1725
|
? content.filter((part) => part?.type === 'text').map((part) => part.text).join('')
|
|
1617
1726
|
: '');
|
|
1618
1727
|
const messageId = `user-${sessionId}-${sequence + 1}`;
|
|
1728
|
+
messageEntryAliases.retain({ cwd: directory, sessionId, syntheticMessageId: messageId, message: event.message });
|
|
1619
1729
|
latestUserMessageIds.set(sessionId, messageId);
|
|
1620
1730
|
publish('assistant.message.start', {
|
|
1621
1731
|
messageId,
|
|
@@ -1625,6 +1735,7 @@ export function createSessionDaemon({
|
|
|
1625
1735
|
}, sessionId, directory);
|
|
1626
1736
|
} else if (event.message?.role === 'assistant') {
|
|
1627
1737
|
const messageId = `assistant-${sessionId}-${sequence + 1}`;
|
|
1738
|
+
messageEntryAliases.retain({ cwd: directory, sessionId, syntheticMessageId: messageId, message: event.message });
|
|
1628
1739
|
clearStreamingRedactionBuffers(sessionId);
|
|
1629
1740
|
streamingMessageIds.set(sessionId, messageId);
|
|
1630
1741
|
latestAssistantMessageIds.set(sessionId, messageId);
|
|
@@ -1653,6 +1764,19 @@ export function createSessionDaemon({
|
|
|
1653
1764
|
break;
|
|
1654
1765
|
}
|
|
1655
1766
|
case 'message_end': {
|
|
1767
|
+
const eventRuntime = runtimeRegistry?.get({ cwd: directory, sessionId });
|
|
1768
|
+
const syntheticMessageId = event.message?.role === 'assistant'
|
|
1769
|
+
? streamingMessageIds.get(sessionId) ?? latestAssistantMessageIds.get(sessionId)
|
|
1770
|
+
: event.message?.role === 'user'
|
|
1771
|
+
? latestUserMessageIds.get(sessionId)
|
|
1772
|
+
: undefined;
|
|
1773
|
+
messageEntryAliases.observeMessageEnd({
|
|
1774
|
+
cwd: directory,
|
|
1775
|
+
sessionId,
|
|
1776
|
+
syntheticMessageId,
|
|
1777
|
+
message: event.message,
|
|
1778
|
+
sessionManager: eventRuntime?.session?.sessionManager,
|
|
1779
|
+
});
|
|
1656
1780
|
if (event.message?.role === 'assistant') {
|
|
1657
1781
|
const content = Array.isArray(event.message.content) ? event.message.content : [];
|
|
1658
1782
|
const messageId = streamingMessageIds.get(sessionId) ?? latestAssistantMessageIds.get(sessionId) ?? `assistant-${sessionId}`;
|
|
@@ -1665,6 +1789,7 @@ export function createSessionDaemon({
|
|
|
1665
1789
|
text: redactAttachmentPaths(content.filter((part) => part?.type === 'text').map((part) => part.text).join('')),
|
|
1666
1790
|
thinking: redactAttachmentPaths(content.filter((part) => part?.type === 'thinking').map((part) => part.thinking).join('')),
|
|
1667
1791
|
durationMs,
|
|
1792
|
+
...(content.some((part) => part?.type === 'toolCall') ? { continuing: true } : {}),
|
|
1668
1793
|
...(event.message.errorMessage ? { error: { code: 'ASSISTANT_ERROR', message: redactAttachmentPaths(event.message.errorMessage) } } : {}),
|
|
1669
1794
|
...(usage ? { usage } : {}),
|
|
1670
1795
|
}, sessionId, directory);
|
|
@@ -1728,11 +1853,11 @@ export function createSessionDaemon({
|
|
|
1728
1853
|
break;
|
|
1729
1854
|
case 'agent_start':
|
|
1730
1855
|
clearIdleDisposal(sessionId);
|
|
1731
|
-
|
|
1856
|
+
retryStateBySession.delete(sessionId);
|
|
1857
|
+
if (!activeRunStartedAt.has(sessionId)) activeRunStartedAt.set(sessionId, Date.now());
|
|
1858
|
+
publish('session.lifecycle', { state: 'busy', runStartedAt: activeRunStartedAt.get(sessionId), serverNow: Date.now() }, sessionId, directory);
|
|
1732
1859
|
break;
|
|
1733
1860
|
case 'agent_end': {
|
|
1734
|
-
latestUserMessageIds.delete(sessionId);
|
|
1735
|
-
latestAssistantMessageIds.delete(sessionId);
|
|
1736
1861
|
const finalMessage = event.messages?.at?.(-1);
|
|
1737
1862
|
if (finalMessage?.role === 'assistant' && finalMessage.stopReason === 'aborted') {
|
|
1738
1863
|
publish('session.interrupted', { reason: 'user-abort', streaming: false }, sessionId, directory);
|
|
@@ -1741,16 +1866,26 @@ export function createSessionDaemon({
|
|
|
1741
1866
|
}
|
|
1742
1867
|
break;
|
|
1743
1868
|
}
|
|
1744
|
-
case 'auto_retry_start':
|
|
1745
|
-
|
|
1746
|
-
state: 'retry',
|
|
1869
|
+
case 'auto_retry_start': {
|
|
1870
|
+
const retry = {
|
|
1747
1871
|
attempt: event.attempt,
|
|
1748
1872
|
next: Date.now() + event.delayMs,
|
|
1749
1873
|
message: redactAttachmentPaths(event.errorMessage),
|
|
1750
|
-
}
|
|
1874
|
+
};
|
|
1875
|
+
retryStateBySession.set(sessionId, retry);
|
|
1876
|
+
if (!activeRunStartedAt.has(sessionId)) activeRunStartedAt.set(sessionId, Date.now());
|
|
1877
|
+
publish('session.lifecycle', { state: 'retry', ...retry, runStartedAt: activeRunStartedAt.get(sessionId), serverNow: Date.now() }, sessionId, directory);
|
|
1878
|
+
break;
|
|
1879
|
+
}
|
|
1880
|
+
case 'auto_retry_end':
|
|
1881
|
+
retryStateBySession.delete(sessionId);
|
|
1751
1882
|
break;
|
|
1752
1883
|
case 'agent_settled':
|
|
1753
|
-
|
|
1884
|
+
retryStateBySession.delete(sessionId);
|
|
1885
|
+
activeRunStartedAt.delete(sessionId);
|
|
1886
|
+
latestUserMessageIds.delete(sessionId);
|
|
1887
|
+
latestAssistantMessageIds.delete(sessionId);
|
|
1888
|
+
publish('session.lifecycle', { state: 'idle', serverNow: Date.now() }, sessionId, directory);
|
|
1754
1889
|
scheduleIdleDisposal(sessionId);
|
|
1755
1890
|
break;
|
|
1756
1891
|
case 'thinking_level_changed':
|
|
@@ -1924,35 +2059,16 @@ export function createSessionDaemon({
|
|
|
1924
2059
|
}
|
|
1925
2060
|
case 'sessions.navigate': {
|
|
1926
2061
|
const activeRuntime = await activateSession(message.payload?.sessionId, message.payload?.directory || message.payload?.cwd);
|
|
1927
|
-
|
|
1928
|
-
if (typeof
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
}
|
|
2062
|
+
const requestedId = message.payload?.messageId;
|
|
2063
|
+
if (typeof requestedId !== 'string' || requestedId.length === 0) throw new SessionDaemonProtocolError('INVALID_ARGUMENT', 'The requested tree entry is invalid.');
|
|
2064
|
+
const messageId = messageEntryAliases.resolve({
|
|
2065
|
+
cwd: activeRuntime.cwd,
|
|
2066
|
+
sessionId: message.payload.sessionId,
|
|
2067
|
+
requestedId,
|
|
2068
|
+
sessionManager: activeRuntime.session.sessionManager,
|
|
2069
|
+
});
|
|
1936
2070
|
const previousLeafId = activeRuntime.session.sessionManager?.getLeafId?.() ?? null;
|
|
1937
|
-
|
|
1938
|
-
let lastError;
|
|
1939
|
-
for (const tryId of tryIds) {
|
|
1940
|
-
try {
|
|
1941
|
-
result = await activeRuntime.session.navigateTree(tryId);
|
|
1942
|
-
messageId = tryId;
|
|
1943
|
-
lastError = undefined;
|
|
1944
|
-
break;
|
|
1945
|
-
} catch (error) {
|
|
1946
|
-
lastError = error;
|
|
1947
|
-
// Only retry on "Entry not found" with a stripped id; otherwise surface immediately.
|
|
1948
|
-
const isEntryNotFound = String(error?.message ?? '').includes('not found');
|
|
1949
|
-
if (!isEntryNotFound || tryId === tryIds[tryIds.length - 1]) {
|
|
1950
|
-
console.error(`[pi:navigate] target=${tryId} leaf=${previousLeafId} branch=${activeRuntime.session.sessionManager?.getBranch?.()?.map((e) => e.id).join(',') ?? 'unknown'} error=${error?.message ?? String(error)}`);
|
|
1951
|
-
throw error;
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
if (lastError) throw lastError;
|
|
2071
|
+
const result = await activeRuntime.session.navigateTree(messageId);
|
|
1956
2072
|
if (result?.cancelled) throw new SessionDaemonProtocolError('SESSION_TREE_NOT_FOUND', 'Pi cancelled tree navigation.');
|
|
1957
2073
|
const newLeafId = activeRuntime.session.sessionManager?.getLeafId?.() ?? null;
|
|
1958
2074
|
const navigation = {
|
|
@@ -1967,8 +2083,16 @@ export function createSessionDaemon({
|
|
|
1967
2083
|
case 'sessions.fork':
|
|
1968
2084
|
case 'sessions.clone': {
|
|
1969
2085
|
const activeRuntime = await activateSession(message.payload?.sessionId, message.payload?.directory || message.payload?.cwd);
|
|
1970
|
-
const
|
|
1971
|
-
if (typeof
|
|
2086
|
+
const requestedId = message.command === 'sessions.fork' ? message.payload?.messageId : activeRuntime.session.sessionManager?.getLeafId?.();
|
|
2087
|
+
if (typeof requestedId !== 'string' || requestedId.length === 0) throw new SessionDaemonProtocolError('SESSION_TREE_NOT_FOUND', 'The Pi session has no fork point.');
|
|
2088
|
+
const entryId = message.command === 'sessions.fork'
|
|
2089
|
+
? messageEntryAliases.resolve({
|
|
2090
|
+
cwd: activeRuntime.cwd,
|
|
2091
|
+
sessionId: message.payload.sessionId,
|
|
2092
|
+
requestedId,
|
|
2093
|
+
sessionManager: activeRuntime.session.sessionManager,
|
|
2094
|
+
})
|
|
2095
|
+
: requestedId;
|
|
1972
2096
|
const result = await activeRuntime.fork(entryId, { position: 'at' });
|
|
1973
2097
|
if (result.cancelled) throw new SessionDaemonProtocolError('SESSION_CREATE_CANCELLED', 'Pi cancelled session creation.');
|
|
1974
2098
|
rememberRuntimeSession();
|
|
@@ -2136,6 +2260,7 @@ export function createSessionDaemon({
|
|
|
2136
2260
|
if (platform !== 'win32') await chmod(endpoint, 0o600);
|
|
2137
2261
|
started = true;
|
|
2138
2262
|
} catch (error) {
|
|
2263
|
+
messageEntryAliases.clear();
|
|
2139
2264
|
await disposeRuntime();
|
|
2140
2265
|
server = undefined;
|
|
2141
2266
|
throw error;
|
|
@@ -2149,8 +2274,13 @@ export function createSessionDaemon({
|
|
|
2149
2274
|
clearTimeout(attempt.expiry);
|
|
2150
2275
|
}
|
|
2151
2276
|
loginAttempts.clear();
|
|
2277
|
+
retryStateBySession.clear();
|
|
2278
|
+
activeRunStartedAt.clear();
|
|
2279
|
+
latestUserMessageIds.clear();
|
|
2280
|
+
latestAssistantMessageIds.clear();
|
|
2152
2281
|
for (const client of clients) client.destroy();
|
|
2153
2282
|
clients.clear();
|
|
2283
|
+
messageEntryAliases.clear();
|
|
2154
2284
|
await new Promise((resolve, reject) => {
|
|
2155
2285
|
server.close((error) => error ? reject(error) : resolve());
|
|
2156
2286
|
});
|