pilotswarm-cli 0.1.4 → 0.1.6
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/cli/tui.js +316 -273
- package/package.json +1 -1
package/cli/tui.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* npx pilotswarm remote --env .env.remote # client-only (AKS)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { PilotSwarmClient, PilotSwarmWorker, PilotSwarmManagementClient, SessionBlobStore, systemAgentUUID } from "pilotswarm-sdk";
|
|
16
|
+
import { PilotSwarmClient, PilotSwarmWorker, PilotSwarmManagementClient, SessionBlobStore, FilesystemArtifactStore, systemAgentUUID, loadAgentFiles } from "pilotswarm-sdk";
|
|
17
17
|
import { createRequire } from "node:module";
|
|
18
18
|
import { marked } from "marked";
|
|
19
19
|
import { markedTerminal } from "marked-terminal";
|
|
@@ -340,15 +340,19 @@ const sessionArtifacts = new Map();
|
|
|
340
340
|
const _registeredArtifacts = new Set();
|
|
341
341
|
const MAX_ARTIFACT_REGISTRY = 500;
|
|
342
342
|
|
|
343
|
-
/** TUI-level
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
343
|
+
/** TUI-level artifact store for on-demand downloads. Created lazily.
|
|
344
|
+
* Uses Azure Blob when configured, otherwise falls back to local filesystem. */
|
|
345
|
+
let _tuiArtifactStore = null;
|
|
346
|
+
function getTuiArtifactStore() {
|
|
347
|
+
if (_tuiArtifactStore) return _tuiArtifactStore;
|
|
347
348
|
const connStr = process.env.AZURE_STORAGE_CONNECTION_STRING;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
if (connStr) {
|
|
350
|
+
const container = process.env.AZURE_STORAGE_CONTAINER || "copilot-sessions";
|
|
351
|
+
_tuiArtifactStore = new SessionBlobStore(connStr, container);
|
|
352
|
+
} else {
|
|
353
|
+
_tuiArtifactStore = new FilesystemArtifactStore();
|
|
354
|
+
}
|
|
355
|
+
return _tuiArtifactStore;
|
|
352
356
|
}
|
|
353
357
|
|
|
354
358
|
/**
|
|
@@ -381,17 +385,13 @@ function detectArtifactLinks(text, orchId) {
|
|
|
381
385
|
* Returns the local path on success, null on failure.
|
|
382
386
|
*/
|
|
383
387
|
async function downloadArtifact(sessionId, filename) {
|
|
384
|
-
const
|
|
385
|
-
if (!bs) {
|
|
386
|
-
appendLog("{red-fg}📥 No blob storage configured — cannot download artifacts.{/red-fg}");
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
388
|
+
const store = getTuiArtifactStore();
|
|
389
389
|
const sessionDir = path.join(EXPORTS_DIR, sessionId.slice(0, 8));
|
|
390
390
|
fs.mkdirSync(sessionDir, { recursive: true });
|
|
391
391
|
const localPath = path.join(sessionDir, filename);
|
|
392
392
|
|
|
393
393
|
try {
|
|
394
|
-
const content = await
|
|
394
|
+
const content = await store.downloadArtifact(sessionId, filename);
|
|
395
395
|
fs.writeFileSync(localPath, content, "utf-8");
|
|
396
396
|
appendLog(`{green-fg}📥 Downloaded: ~/${path.relative(os.homedir(), localPath)} (${(content.length / 1024).toFixed(1)}KB){/green-fg}`);
|
|
397
397
|
return localPath;
|
|
@@ -2703,11 +2703,10 @@ const seqCmsSeededSessions = new Set();
|
|
|
2703
2703
|
* Load conversation history from CMS and rebuild chat buffer for the session.
|
|
2704
2704
|
* Includes ALL persisted events (not truncated) so switching sessions is deterministic.
|
|
2705
2705
|
*/
|
|
2706
|
-
async function loadCmsHistory(orchId) {
|
|
2706
|
+
async function loadCmsHistory(orchId, options = {}) {
|
|
2707
2707
|
const _ph = perfStart("loadCmsHistory");
|
|
2708
2708
|
const sid = orchId.startsWith("session-") ? orchId.slice(8) : orchId;
|
|
2709
|
-
const
|
|
2710
|
-
sessionHistoryLoadGeneration.set(orchId, generation);
|
|
2709
|
+
const force = options.force === true;
|
|
2711
2710
|
let eventCount = 0;
|
|
2712
2711
|
let loadFailed = false;
|
|
2713
2712
|
|
|
@@ -2716,297 +2715,320 @@ async function loadCmsHistory(orchId) {
|
|
|
2716
2715
|
// so reloading on every session switch just adds latency.
|
|
2717
2716
|
const cached = sessionChatBuffers.get(orchId);
|
|
2718
2717
|
const loadedAt = sessionHistoryLoadedAt.get(orchId) ?? 0;
|
|
2719
|
-
if (cached && cached.length > 1 && (Date.now() - loadedAt) < 30_000 && !orchHasChanges.has(orchId)) {
|
|
2718
|
+
if (!force && cached && cached.length > 1 && (Date.now() - loadedAt) < 30_000 && !orchHasChanges.has(orchId)) {
|
|
2719
|
+
perfEnd(_ph, { orchId: orchId.slice(0, 12), cached: true });
|
|
2720
2720
|
return;
|
|
2721
2721
|
}
|
|
2722
2722
|
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
sess = await client.resumeSession(sid);
|
|
2728
|
-
sessions.set(sid, sess);
|
|
2729
|
-
} catch (err) {
|
|
2730
|
-
appendLog(`{yellow-fg}Could not resume session ${shortId(sid)}: ${err.message}{/yellow-fg}`);
|
|
2731
|
-
return;
|
|
2732
|
-
}
|
|
2723
|
+
const inFlight = sessionHistoryLoadPromises.get(orchId);
|
|
2724
|
+
if (inFlight && !force) {
|
|
2725
|
+
perfEnd(_ph, { orchId: orchId.slice(0, 12), deduped: true });
|
|
2726
|
+
return inFlight;
|
|
2733
2727
|
}
|
|
2734
2728
|
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
const CMS_HISTORY_FETCH_LIMIT = expand >= 2 ? 2000 : expand >= 1 ? 500 : 250;
|
|
2738
|
-
const MAX_RENDERED_EVENTS = expand >= 2 ? 2000 : expand >= 1 ? 500 : 120;
|
|
2739
|
-
const MAX_TOTAL_RENDER_CHARS = expand >= 2 ? 500_000 : expand >= 1 ? 200_000 : 50_000;
|
|
2740
|
-
const MAX_ASSISTANT_MESSAGE_CHARS = expand >= 1 ? 20_000 : 4_000;
|
|
2741
|
-
const dc = getDc();
|
|
2729
|
+
const generation = (sessionHistoryLoadGeneration.get(orchId) ?? 0) + 1;
|
|
2730
|
+
sessionHistoryLoadGeneration.set(orchId, generation);
|
|
2742
2731
|
|
|
2743
|
-
|
|
2744
|
-
//
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
sess.getMessages(CMS_HISTORY_FETCH_LIMIT),
|
|
2748
|
-
(!sessionModels.has(orchId)) ? sess.getInfo().catch(() => null) : Promise.resolve(null),
|
|
2749
|
-
dc ? dc.getStatus(orchId).catch(() => null) : Promise.resolve(null),
|
|
2750
|
-
]);
|
|
2751
|
-
eventCount = events?.length || 0;
|
|
2752
|
-
|
|
2753
|
-
let liveCustomStatus = null;
|
|
2754
|
-
let liveResponsePayload = null;
|
|
2755
|
-
if (liveStatus?.customStatus) {
|
|
2732
|
+
const loadPromise = (async () => {
|
|
2733
|
+
// Ensure we have a PilotSwarmSession handle (may not exist for sessions from previous TUI runs)
|
|
2734
|
+
let sess = sessions.get(sid);
|
|
2735
|
+
if (!sess) {
|
|
2756
2736
|
try {
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
liveResponsePayload = await fetchLatestResponsePayload(orchId, dc);
|
|
2737
|
+
sess = await client.resumeSession(sid);
|
|
2738
|
+
sessions.set(sid, sess);
|
|
2739
|
+
} catch (err) {
|
|
2740
|
+
appendLog(`{yellow-fg}Could not resume session ${shortId(sid)}: ${err.message}{/yellow-fg}`);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2764
2743
|
}
|
|
2765
2744
|
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
:
|
|
2769
|
-
|
|
2770
|
-
:
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
if (info?.model) {
|
|
2774
|
-
sessionModels.set(orchId, info.model);
|
|
2775
|
-
if (orchId === activeOrchId) updateChatLabel();
|
|
2776
|
-
}
|
|
2745
|
+
try {
|
|
2746
|
+
const expand = sessionExpandLevel.get(orchId) || 0;
|
|
2747
|
+
const CMS_HISTORY_FETCH_LIMIT = expand >= 2 ? 2000 : expand >= 1 ? 500 : 250;
|
|
2748
|
+
const MAX_RENDERED_EVENTS = expand >= 2 ? 2000 : expand >= 1 ? 500 : 120;
|
|
2749
|
+
const MAX_TOTAL_RENDER_CHARS = expand >= 2 ? 500_000 : expand >= 1 ? 200_000 : 50_000;
|
|
2750
|
+
const MAX_ASSISTANT_MESSAGE_CHARS = expand >= 1 ? 20_000 : 4_000;
|
|
2751
|
+
const dc = getDc();
|
|
2777
2752
|
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2753
|
+
// Fetch events, session info, and live status in parallel.
|
|
2754
|
+
// The live custom status may contain the latest `turnResult` even when
|
|
2755
|
+
// the CMS history does not yet have a persisted `assistant.message`.
|
|
2756
|
+
const [events, info, liveStatus] = await Promise.all([
|
|
2757
|
+
sess.getMessages(CMS_HISTORY_FETCH_LIMIT),
|
|
2758
|
+
(!sessionModels.has(orchId)) ? sess.getInfo().catch(() => null) : Promise.resolve(null),
|
|
2759
|
+
dc ? dc.getStatus(orchId).catch(() => null) : Promise.resolve(null),
|
|
2760
|
+
]);
|
|
2761
|
+
eventCount = events?.length || 0;
|
|
2762
|
+
|
|
2763
|
+
let liveCustomStatus = null;
|
|
2764
|
+
let liveResponsePayload = null;
|
|
2765
|
+
if (liveStatus?.customStatus) {
|
|
2766
|
+
try {
|
|
2767
|
+
liveCustomStatus = typeof liveStatus.customStatus === "string"
|
|
2768
|
+
? JSON.parse(liveStatus.customStatus)
|
|
2769
|
+
: liveStatus.customStatus;
|
|
2770
|
+
} catch {}
|
|
2781
2771
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
// arrived while we were fetching from CMS (race condition that
|
|
2785
|
-
// causes empty chat on first switch to a session).
|
|
2786
|
-
const existing = sessionChatBuffers.get(orchId);
|
|
2787
|
-
const isLoadingPlaceholder = existing
|
|
2788
|
-
&& existing.length === 1
|
|
2789
|
-
&& /Loading/.test(existing[0]);
|
|
2790
|
-
if (!existing || existing.length === 0 || isLoadingPlaceholder) {
|
|
2791
|
-
const splashLines = ensureSessionSplashBuffer(orchId);
|
|
2792
|
-
if (!splashLines) {
|
|
2793
|
-
sessionChatBuffers.set(orchId, []);
|
|
2794
|
-
}
|
|
2772
|
+
if (liveCustomStatus?.responseVersion) {
|
|
2773
|
+
liveResponsePayload = await fetchLatestResponsePayload(orchId, dc);
|
|
2795
2774
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2775
|
+
|
|
2776
|
+
const liveTurnContent = liveCustomStatus?.turnResult?.type === "completed"
|
|
2777
|
+
? liveCustomStatus.turnResult.content
|
|
2778
|
+
: liveResponsePayload?.type === "completed"
|
|
2779
|
+
? liveResponsePayload.content
|
|
2780
|
+
: "";
|
|
2781
|
+
|
|
2782
|
+
// Populate session model if not already known
|
|
2783
|
+
if (info?.model) {
|
|
2784
|
+
sessionModels.set(orchId, info.model);
|
|
2785
|
+
if (orchId === activeOrchId) updateChatLabel();
|
|
2798
2786
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2787
|
+
|
|
2788
|
+
if ((!events || events.length === 0) && !liveTurnContent) {
|
|
2789
|
+
if (sessionHistoryLoadGeneration.get(orchId) !== generation) {
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
// Only blank the buffer if the observer hasn't already written
|
|
2793
|
+
// content into it. Otherwise we'd nuke live turn output that
|
|
2794
|
+
// arrived while we were fetching from CMS (race condition that
|
|
2795
|
+
// causes empty chat on first switch to a session).
|
|
2796
|
+
const existing = sessionChatBuffers.get(orchId);
|
|
2797
|
+
const isLoadingPlaceholder = existing
|
|
2798
|
+
&& existing.length === 1
|
|
2799
|
+
&& /Loading/.test(existing[0]);
|
|
2800
|
+
if (!existing || existing.length === 0 || isLoadingPlaceholder) {
|
|
2801
|
+
const splashLines = ensureSessionSplashBuffer(orchId);
|
|
2802
|
+
if (!splashLines) {
|
|
2803
|
+
sessionChatBuffers.set(orchId, ["{gray-fg}(no recent chat history yet){/gray-fg}"]);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
const existingActivity = sessionActivityBuffers.get(orchId);
|
|
2807
|
+
if (!existingActivity || existingActivity.length === 0) {
|
|
2808
|
+
sessionActivityBuffers.set(orchId, ["{gray-fg}(no recent activity yet){/gray-fg}"]);
|
|
2809
|
+
}
|
|
2810
|
+
sessionHistoryLoadedAt.set(orchId, Date.now());
|
|
2811
|
+
sessionRenderedCmsSeq.set(orchId, 0);
|
|
2812
|
+
sessionRecoveredTurnResult.delete(orchId);
|
|
2813
|
+
if (orchId === activeOrchId) {
|
|
2814
|
+
invalidateChat();
|
|
2815
|
+
invalidateActivity();
|
|
2816
|
+
}
|
|
2817
|
+
return;
|
|
2805
2818
|
}
|
|
2806
|
-
return;
|
|
2807
|
-
}
|
|
2808
2819
|
|
|
2809
|
-
|
|
2810
|
-
|
|
2820
|
+
// Strip the [SYSTEM: Running on host ...] prefix from user prompts
|
|
2821
|
+
const stripHostPrefix = (text) => text?.replace(/^\[SYSTEM: Running on host "[^"]*"\.\]\n\n/, "") || text;
|
|
2811
2822
|
|
|
2812
|
-
|
|
2813
|
-
|
|
2823
|
+
// Filter out internal timer continuation prompts — these aren't real user messages
|
|
2824
|
+
const isTimerPrompt = (text) => /^The \d+ second wait is now complete\./i.test(text);
|
|
2814
2825
|
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2826
|
+
const lines = [];
|
|
2827
|
+
const fmtTime = (value) => {
|
|
2828
|
+
if (!value) return "--:--:--";
|
|
2829
|
+
return formatDisplayTime(value, {
|
|
2830
|
+
hour: "2-digit",
|
|
2831
|
+
minute: "2-digit",
|
|
2832
|
+
second: "2-digit",
|
|
2833
|
+
});
|
|
2834
|
+
};
|
|
2835
|
+
const normalizeContent = (text) => (text || "").replace(/\r\n/g, "\n").trim();
|
|
2836
|
+
|
|
2837
|
+
// Cap rendered events to the most recent N to keep switching fast.
|
|
2838
|
+
const renderEvents = (events || []).length > MAX_RENDERED_EVENTS
|
|
2839
|
+
? events.slice(-MAX_RENDERED_EVENTS)
|
|
2840
|
+
: (events || []);
|
|
2841
|
+
const truncated = (events || []).length > MAX_RENDERED_EVENTS;
|
|
2842
|
+
|
|
2843
|
+
// Build display lines from persisted events
|
|
2844
|
+
// Chat lines = user messages + assistant responses
|
|
2845
|
+
// Activity lines = tool calls, reasoning, status changes
|
|
2846
|
+
const activityLines = [];
|
|
2847
|
+
let renderedChars = 0;
|
|
2848
|
+
let lastAssistantContent = "";
|
|
2849
|
+
if (truncated) {
|
|
2850
|
+
lines.push(`{gray-fg}── ${events.length - MAX_RENDERED_EVENTS} older events omitted (${events.length} total) · press {bold}e{/bold} to expand ──{/gray-fg}`);
|
|
2851
|
+
lines.push("");
|
|
2852
|
+
}
|
|
2853
|
+
for (const evt of renderEvents) {
|
|
2854
|
+
const type = evt.eventType;
|
|
2855
|
+
const timeStr = fmtTime(evt.createdAt);
|
|
2856
|
+
if (type === "user.message") {
|
|
2857
|
+
const content = stripHostPrefix(evt.data?.content);
|
|
2858
|
+
if (content && !content.startsWith("[SYSTEM:") && !isTimerPrompt(content) && !isBootstrapPromptForSession(content, orchId)) {
|
|
2859
|
+
// Format CHILD_UPDATE messages as distinct cards
|
|
2860
|
+
const childMatch = content.match(/^\[CHILD_UPDATE from=(\S+) type=(\S+)(?:\s+iter=(\d+))?\]\n?(.*)$/s);
|
|
2861
|
+
if (childMatch) {
|
|
2862
|
+
const childId = childMatch[1].slice(0, 8);
|
|
2863
|
+
const updateType = childMatch[2];
|
|
2864
|
+
const body = (childMatch[4] || "").trim();
|
|
2865
|
+
const childTitle = sessionHeadings.get(`session-${childMatch[1]}`) || `Agent ${childId}`;
|
|
2866
|
+
const typeColor = updateType === "completed" ? "green" : updateType === "error" ? "red" : "magenta";
|
|
2867
|
+
lines.push(`{white-fg}[${timeStr}]{/white-fg}`);
|
|
2868
|
+
lines.push(`{${typeColor}-fg}┌─ {bold}${childTitle}{/bold} · ${updateType} ─┐{/${typeColor}-fg}`);
|
|
2869
|
+
if (body) {
|
|
2870
|
+
const bodyLines = body.split("\n");
|
|
2871
|
+
for (const bl of bodyLines.slice(0, 8)) {
|
|
2872
|
+
lines.push(`{${typeColor}-fg}│{/${typeColor}-fg} ${bl}`);
|
|
2873
|
+
}
|
|
2874
|
+
if (bodyLines.length > 8) {
|
|
2875
|
+
lines.push(`{${typeColor}-fg}│{/${typeColor}-fg} {gray-fg}… ${bodyLines.length - 8} more lines{/gray-fg}`);
|
|
2876
|
+
}
|
|
2865
2877
|
}
|
|
2878
|
+
lines.push(`{${typeColor}-fg}└${"─".repeat(30)}┘{/${typeColor}-fg}`);
|
|
2879
|
+
lines.push("");
|
|
2880
|
+
} else {
|
|
2881
|
+
lines.push(`{white-fg}[${timeStr}]{/white-fg} {bold}You:{/bold} ${content}`);
|
|
2866
2882
|
}
|
|
2867
|
-
lines.push(`{${typeColor}-fg}└${'─'.repeat(30)}┘{/${typeColor}-fg}`);
|
|
2868
|
-
lines.push("");
|
|
2869
|
-
} else {
|
|
2870
|
-
lines.push(`{white-fg}[${timeStr}]{/white-fg} {bold}You:{/bold} ${content}`);
|
|
2871
2883
|
}
|
|
2872
|
-
}
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2884
|
+
} else if (type === "assistant.message") {
|
|
2885
|
+
const content = evt.data?.content;
|
|
2886
|
+
if (content) {
|
|
2887
|
+
lastAssistantContent = content;
|
|
2888
|
+
detectArtifactLinks(content, orchId);
|
|
2889
|
+
if (renderedChars >= MAX_TOTAL_RENDER_CHARS) {
|
|
2890
|
+
lines.push(`{gray-fg}── additional assistant output omitted to keep session switching fast ──{/gray-fg}`);
|
|
2891
|
+
lines.push("");
|
|
2892
|
+
break;
|
|
2893
|
+
}
|
|
2894
|
+
const clipped = content.length > MAX_ASSISTANT_MESSAGE_CHARS
|
|
2895
|
+
? content.slice(0, MAX_ASSISTANT_MESSAGE_CHARS) + "\n\n[output truncated in TUI history view]"
|
|
2896
|
+
: content;
|
|
2897
|
+
const displayClipped = clipped.replace(
|
|
2898
|
+
/artifact:\/\/[a-f0-9-]+\/([^\s"'{}]+)/g,
|
|
2899
|
+
"📎 **$1** _(press 'a' to download)_",
|
|
2900
|
+
);
|
|
2901
|
+
lines.push(`{white-fg}[${timeStr}]{/white-fg} {cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`);
|
|
2902
|
+
const rendered = renderMarkdown(displayClipped);
|
|
2903
|
+
renderedChars += clipped.length;
|
|
2904
|
+
for (const line of rendered.split("\n")) {
|
|
2905
|
+
lines.push(line);
|
|
2906
|
+
}
|
|
2881
2907
|
lines.push("");
|
|
2882
|
-
break;
|
|
2883
2908
|
}
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
}
|
|
2898
|
-
lines.push("");
|
|
2909
|
+
} else if (type === "tool.execution_start") {
|
|
2910
|
+
const toolName = evt.data?.toolName || "tool";
|
|
2911
|
+
const dsid = evt.data?.durableSessionId ? ` {gray-fg}[${shortId(evt.data.durableSessionId)}]{/gray-fg}` : "";
|
|
2912
|
+
activityLines.push(`{white-fg}[${timeStr}]{/white-fg} {yellow-fg}▶ ${toolName}{/yellow-fg}${dsid}`);
|
|
2913
|
+
} else if (type === "tool.execution_complete") {
|
|
2914
|
+
const toolName = evt.data?.toolName || "tool";
|
|
2915
|
+
activityLines.push(`{white-fg}[${timeStr}]{/white-fg} {green-fg}✓ ${toolName}{/green-fg}`);
|
|
2916
|
+
} else if (type === "abort" || type === "session.info" || type === "session.idle"
|
|
2917
|
+
|| type === "session.usage_info" || type === "pending_messages.modified"
|
|
2918
|
+
|| type === "assistant.usage") {
|
|
2919
|
+
// skip internal/noisy events
|
|
2920
|
+
} else {
|
|
2921
|
+
activityLines.push(`{white-fg}[${timeStr}] [${type}]{/white-fg}`);
|
|
2899
2922
|
}
|
|
2900
|
-
} else if (type === "tool.execution_start") {
|
|
2901
|
-
const toolName = evt.data?.toolName || "tool";
|
|
2902
|
-
const dsid = evt.data?.durableSessionId ? ` {gray-fg}[${shortId(evt.data.durableSessionId)}]{/gray-fg}` : "";
|
|
2903
|
-
activityLines.push(`{white-fg}[${timeStr}]{/white-fg} {yellow-fg}▶ ${toolName}{/yellow-fg}${dsid}`);
|
|
2904
|
-
} else if (type === "tool.execution_complete") {
|
|
2905
|
-
const toolName = evt.data?.toolName || "tool";
|
|
2906
|
-
activityLines.push(`{white-fg}[${timeStr}]{/white-fg} {green-fg}✓ ${toolName}{/green-fg}`);
|
|
2907
|
-
} else if (type === "abort" || type === "session.info" || type === "session.idle"
|
|
2908
|
-
|| type === "session.usage_info" || type === "pending_messages.modified"
|
|
2909
|
-
|| type === "assistant.usage") {
|
|
2910
|
-
// skip internal/noisy events
|
|
2911
|
-
} else {
|
|
2912
|
-
activityLines.push(`{white-fg}[${timeStr}] [${type}]{/white-fg}`);
|
|
2913
2923
|
}
|
|
2914
|
-
}
|
|
2915
2924
|
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2925
|
+
const normalizedLiveTurn = normalizeContent(liveTurnContent);
|
|
2926
|
+
const normalizedLastAssistant = normalizeContent(lastAssistantContent);
|
|
2927
|
+
const liveTurnMissingFromHistory = normalizedLiveTurn
|
|
2928
|
+
&& normalizedLiveTurn !== normalizedLastAssistant;
|
|
2920
2929
|
|
|
2921
|
-
|
|
2922
|
-
|
|
2930
|
+
if (liveTurnMissingFromHistory) {
|
|
2931
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
2932
|
+
lines.push("");
|
|
2933
|
+
}
|
|
2934
|
+
lines.push("{gray-fg}── latest turn result recovered from live status ──{/gray-fg}");
|
|
2923
2935
|
lines.push("");
|
|
2936
|
+
|
|
2937
|
+
const clippedLiveTurn = liveTurnContent.length > MAX_ASSISTANT_MESSAGE_CHARS
|
|
2938
|
+
? liveTurnContent.slice(0, MAX_ASSISTANT_MESSAGE_CHARS) + "\n\n[output truncated in TUI history view]"
|
|
2939
|
+
: liveTurnContent;
|
|
2940
|
+
lines.push(`{white-fg}[${fmtTime(Date.now())}]{/white-fg} {cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`);
|
|
2941
|
+
const renderedLiveTurn = renderMarkdown(clippedLiveTurn);
|
|
2942
|
+
renderedChars += clippedLiveTurn.length;
|
|
2943
|
+
for (const line of renderedLiveTurn.split("\n")) {
|
|
2944
|
+
lines.push(line);
|
|
2945
|
+
}
|
|
2946
|
+
lines.push("");
|
|
2947
|
+
sessionRecoveredTurnResult.set(orchId, normalizedLiveTurn);
|
|
2948
|
+
noteSeenResponseVersion(orchId, liveResponsePayload?.version);
|
|
2949
|
+
} else {
|
|
2950
|
+
sessionRecoveredTurnResult.delete(orchId);
|
|
2924
2951
|
}
|
|
2925
|
-
lines.push("{gray-fg}── latest turn result recovered from live status ──{/gray-fg}");
|
|
2926
|
-
lines.push("");
|
|
2927
|
-
|
|
2928
|
-
const clippedLiveTurn = liveTurnContent.length > MAX_ASSISTANT_MESSAGE_CHARS
|
|
2929
|
-
? liveTurnContent.slice(0, MAX_ASSISTANT_MESSAGE_CHARS) + "\n\n[output truncated in TUI history view]"
|
|
2930
|
-
: liveTurnContent;
|
|
2931
|
-
lines.push(`{white-fg}[${fmtTime(Date.now())}]{/white-fg} {cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`);
|
|
2932
|
-
const renderedLiveTurn = renderMarkdown(clippedLiveTurn);
|
|
2933
|
-
renderedChars += clippedLiveTurn.length;
|
|
2934
|
-
for (const line of renderedLiveTurn.split("\n")) {
|
|
2935
|
-
lines.push(line);
|
|
2936
|
-
}
|
|
2937
|
-
lines.push("");
|
|
2938
|
-
sessionRecoveredTurnResult.set(orchId, normalizedLiveTurn);
|
|
2939
|
-
noteSeenResponseVersion(orchId, liveResponsePayload?.version);
|
|
2940
|
-
} else {
|
|
2941
|
-
sessionRecoveredTurnResult.delete(orchId);
|
|
2942
|
-
}
|
|
2943
2952
|
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2953
|
+
if (eventCount > 0) {
|
|
2954
|
+
lines.push(`{white-fg}── recent history loaded from database (${eventCount} events fetched) ──{/white-fg}`);
|
|
2955
|
+
lines.push("");
|
|
2956
|
+
}
|
|
2948
2957
|
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2958
|
+
if (systemSplashText.has(orchId)) {
|
|
2959
|
+
const splashText = systemSplashText.get(orchId);
|
|
2960
|
+
const splashLines = splashText.split("\n");
|
|
2961
|
+
const hasSplashPrefix = lines.length >= splashLines.length
|
|
2962
|
+
&& splashLines.every((line, idx) => lines[idx] === line);
|
|
2963
|
+
if (!hasSplashPrefix) {
|
|
2964
|
+
lines.unshift(...splashLines, "");
|
|
2965
|
+
}
|
|
2966
|
+
sessionSplashApplied.add(orchId);
|
|
2957
2967
|
}
|
|
2958
|
-
sessionSplashApplied.add(orchId);
|
|
2959
|
-
}
|
|
2960
2968
|
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2969
|
+
if (sessionHistoryLoadGeneration.get(orchId) !== generation) {
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2964
2972
|
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2973
|
+
const maxRenderedSeq = (events || []).reduce((max, evt) => Math.max(max, evt.seq || 0), 0);
|
|
2974
|
+
sessionChatBuffers.set(orchId, lines);
|
|
2975
|
+
sessionActivityBuffers.set(orchId, activityLines);
|
|
2976
|
+
sessionHistoryLoadedAt.set(orchId, Date.now());
|
|
2977
|
+
sessionRenderedCmsSeq.set(orchId, maxRenderedSeq);
|
|
2970
2978
|
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
}
|
|
2979
|
+
if (orchId === activeOrchId) {
|
|
2980
|
+
invalidateChat();
|
|
2981
|
+
invalidateActivity();
|
|
2982
|
+
}
|
|
2976
2983
|
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
}
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2984
|
+
if (!seqCmsSeededSessions.has(orchId)) {
|
|
2985
|
+
const existingSeq = seqEventBuffers.get(orchId) ?? [];
|
|
2986
|
+
if (existingSeq.length === 0) {
|
|
2987
|
+
const cmsNode = addSeqNode("cms");
|
|
2988
|
+
const seeded = [];
|
|
2989
|
+
for (const evt of events) {
|
|
2990
|
+
const t = fmtTime(evt.createdAt);
|
|
2991
|
+
if (evt.eventType === "user.message") {
|
|
2992
|
+
const txt = stripHostPrefix(evt.data?.content || "");
|
|
2993
|
+
if (txt && !isTimerPrompt(txt)) {
|
|
2994
|
+
seeded.push({ type: "user_msg_synth", time: t, orchNode: cmsNode, actNode: cmsNode, label: txt });
|
|
2995
|
+
}
|
|
2996
|
+
} else if (evt.eventType === "assistant.message") {
|
|
2997
|
+
const txt = evt.data?.content || "";
|
|
2998
|
+
if (txt) {
|
|
2999
|
+
seeded.push({ type: "response", time: t, orchNode: cmsNode, actNode: cmsNode, snippet: txt.slice(0, 40) });
|
|
3000
|
+
}
|
|
3001
|
+
} else if (evt.eventType === "tool.execution_start") {
|
|
3002
|
+
seeded.push({ type: "activity_start", time: t, orchNode: cmsNode, actNode: cmsNode });
|
|
2994
3003
|
}
|
|
2995
|
-
}
|
|
2996
|
-
|
|
3004
|
+
}
|
|
3005
|
+
if (seeded.length > 0) {
|
|
3006
|
+
seqEventBuffers.set(orchId, seeded);
|
|
2997
3007
|
}
|
|
2998
3008
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3009
|
+
seqCmsSeededSessions.add(orchId);
|
|
3010
|
+
}
|
|
3011
|
+
} catch (err) {
|
|
3012
|
+
loadFailed = true;
|
|
3013
|
+
appendLog(`{yellow-fg}CMS history load failed: ${err.message}{/yellow-fg}`);
|
|
3014
|
+
} finally {
|
|
3015
|
+
if (sessionHistoryLoadPromises.get(orchId) === loadPromise) {
|
|
3016
|
+
sessionHistoryLoadPromises.delete(orchId);
|
|
3002
3017
|
}
|
|
3003
|
-
seqCmsSeededSessions.add(orchId);
|
|
3004
3018
|
}
|
|
3005
|
-
}
|
|
3006
|
-
|
|
3007
|
-
|
|
3019
|
+
})();
|
|
3020
|
+
|
|
3021
|
+
sessionHistoryLoadPromises.set(orchId, loadPromise);
|
|
3022
|
+
try {
|
|
3023
|
+
return await loadPromise;
|
|
3024
|
+
} finally {
|
|
3025
|
+
perfEnd(_ph, {
|
|
3026
|
+
orchId: orchId.slice(0, 12),
|
|
3027
|
+
events: eventCount,
|
|
3028
|
+
err: loadFailed || undefined,
|
|
3029
|
+
force: force || undefined,
|
|
3030
|
+
});
|
|
3008
3031
|
}
|
|
3009
|
-
perfEnd(_ph, { orchId: orchId.slice(0, 12), events: eventCount, err: loadFailed || undefined });
|
|
3010
3032
|
}
|
|
3011
3033
|
|
|
3012
3034
|
// ─── Start the PilotSwarm client (embedded workers + client) ────────
|
|
@@ -3254,10 +3276,30 @@ if (!modelProviders) {
|
|
|
3254
3276
|
// Will be populated from mgmt.getModelsByProvider() after mgmt.start()
|
|
3255
3277
|
}
|
|
3256
3278
|
|
|
3257
|
-
// Capture session policy + agent list from the first worker
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3279
|
+
// Capture session policy + agent list from the first worker.
|
|
3280
|
+
// In remote mode (no local workers), load directly from the plugin directory
|
|
3281
|
+
// so the TUI enforces the same session creation restrictions as the backend.
|
|
3282
|
+
let _workerSessionPolicy = workers[0]?.sessionPolicy || null;
|
|
3283
|
+
let _workerAllowedAgentNames = workers[0]?.allowedAgentNames || [];
|
|
3284
|
+
let _workerLoadedAgents = workers[0]?.loadedAgents || [];
|
|
3285
|
+
|
|
3286
|
+
if (workers.length === 0 && process.env.PLUGIN_DIRS) {
|
|
3287
|
+
const pluginDirsArr = process.env.PLUGIN_DIRS.split(",").map(d => d.trim()).filter(Boolean);
|
|
3288
|
+
for (const dir of pluginDirsArr) {
|
|
3289
|
+
const policyFile = path.join(dir, "session-policy.json");
|
|
3290
|
+
if (fs.existsSync(policyFile)) {
|
|
3291
|
+
try { _workerSessionPolicy = JSON.parse(fs.readFileSync(policyFile, "utf-8")); } catch {}
|
|
3292
|
+
}
|
|
3293
|
+
const agentsDir = path.join(dir, "agents");
|
|
3294
|
+
if (fs.existsSync(agentsDir)) {
|
|
3295
|
+
try {
|
|
3296
|
+
const agents = loadAgentFiles(agentsDir).filter(a => !a.system && a.name !== "default");
|
|
3297
|
+
_workerLoadedAgents = agents;
|
|
3298
|
+
_workerAllowedAgentNames = agents.map(a => a.name).filter(Boolean);
|
|
3299
|
+
} catch {}
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3261
3303
|
if (_workerSessionPolicy) {
|
|
3262
3304
|
appendLog(`Session policy: mode=${_workerSessionPolicy.creation?.mode || "open"}, allowGeneric=${_workerSessionPolicy.creation?.allowGeneric ?? true}`);
|
|
3263
3305
|
}
|
|
@@ -3402,6 +3444,7 @@ const sessionAgentIds = new Map(); // orchId → agentId string (e.g. "pilotswar
|
|
|
3402
3444
|
const sessionChatBuffers = new Map(); // orchId → string[]
|
|
3403
3445
|
const sessionHistoryLoadedAt = new Map(); // orchId → epoch ms of last CMS history load
|
|
3404
3446
|
const sessionHistoryLoadGeneration = new Map(); // orchId → monotonically increasing async load token
|
|
3447
|
+
const sessionHistoryLoadPromises = new Map(); // orchId → in-flight CMS history load promise
|
|
3405
3448
|
const sessionRenderedCmsSeq = new Map(); // orchId → highest CMS seq already incorporated into buffers
|
|
3406
3449
|
const sessionExpandLevel = new Map(); // orchId → 0 (default) | 1 | 2 (how many times user expanded history)
|
|
3407
3450
|
const sessionSplashApplied = new Set(); // orchIds that have had splash prepended (idempotency guard)
|
|
@@ -6509,7 +6552,7 @@ screen.on("keypress", (ch, key) => {
|
|
|
6509
6552
|
const levelNames = ["", "expanded (500)", "full history"];
|
|
6510
6553
|
setStatus(`Loading ${levelNames[currentLevel + 1]}...`);
|
|
6511
6554
|
screen.render();
|
|
6512
|
-
loadCmsHistory(targetOrchId).then(() => {
|
|
6555
|
+
loadCmsHistory(targetOrchId, { force: true }).then(() => {
|
|
6513
6556
|
if (targetOrchId === activeOrchId) {
|
|
6514
6557
|
startCmsPoller(targetOrchId);
|
|
6515
6558
|
invalidateChat("top");
|