@yeaft/webchat-agent 1.0.197 → 1.0.199
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +83 -83
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/conversation/persist.js +180 -36
- package/yeaft/engine.js +3 -1
- package/yeaft/tools/types.js +1 -1
- package/yeaft/web-bridge.js +469 -189
package/yeaft/web-bridge.js
CHANGED
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
trimSnapshotForBudget,
|
|
62
62
|
} from './history-compact.js';
|
|
63
63
|
import { persistYeaftAttachments, attachmentsForPersistence, persistedAttachmentPreviewPayload } from './attachments.js';
|
|
64
|
-
import { ConversationStore, parseSeqFromId } from './conversation/persist.js';
|
|
64
|
+
import { ConversationStore, parseSeqFromId, projectVisibleSessionMessages } from './conversation/persist.js';
|
|
65
65
|
import { isHiddenConversationRow } from './conversation/internal-control.js';
|
|
66
66
|
import { imageMetadataForPersistence } from './image-assets.js';
|
|
67
67
|
import { sliceLastNTurns } from './turn-utils.js';
|
|
@@ -94,6 +94,7 @@ const SKILL_RELOAD_INTERVAL_MS = 2_000;
|
|
|
94
94
|
* vpId:string,
|
|
95
95
|
* threadId:string,
|
|
96
96
|
* turnId:string,
|
|
97
|
+
* toolCallId:string,
|
|
97
98
|
* question:string,
|
|
98
99
|
* options:Array<string>,
|
|
99
100
|
* createdAt:number,
|
|
@@ -254,7 +255,7 @@ function scheduleYeaftLoadHistoryMetadataReplay(sessionId) {
|
|
|
254
255
|
const replayConversationId = yeaftConversationId;
|
|
255
256
|
setTimeout(async () => {
|
|
256
257
|
try {
|
|
257
|
-
if (!replaySession) return;
|
|
258
|
+
if (!replaySession || session !== replaySession) return;
|
|
258
259
|
try {
|
|
259
260
|
await refreshLiveSessionConfig();
|
|
260
261
|
} catch (err) {
|
|
@@ -265,10 +266,19 @@ function scheduleYeaftLoadHistoryMetadataReplay(sessionId) {
|
|
|
265
266
|
try {
|
|
266
267
|
const metaRoot = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
267
268
|
const meta = loadSessionMeta(join(sessionsRoot(metaRoot), sessionId));
|
|
268
|
-
|
|
269
|
+
const workDir = normalizeSessionWorkDir(meta?.workDir);
|
|
270
|
+
if (workDir) {
|
|
271
|
+
const scheduled = scheduleProjectRuntimeLoad(workDir);
|
|
272
|
+
projectRuntime = scheduled && typeof scheduled.then === 'function'
|
|
273
|
+
? await scheduled
|
|
274
|
+
: scheduled;
|
|
275
|
+
} else {
|
|
276
|
+
activateBaseRuntime(captureRuntimeOwner(replaySession));
|
|
277
|
+
}
|
|
269
278
|
} catch { /* best-effort project metadata */ }
|
|
270
279
|
}
|
|
271
|
-
|
|
280
|
+
if (session !== replaySession) return;
|
|
281
|
+
const status = mergedStatusForProjectRuntime(projectRuntime, replaySession);
|
|
272
282
|
hydrateYeaftStatusFromSession({ ...replaySession, status }, { reason: 'history_load', emitEvent: true });
|
|
273
283
|
sendSessionEvent({
|
|
274
284
|
type: 'session_ready',
|
|
@@ -591,16 +601,73 @@ const routePromisesByMsgId = new Map();
|
|
|
591
601
|
const projectRuntimes = new Map();
|
|
592
602
|
/** @type {Map<string, Promise<any>>} */
|
|
593
603
|
const baseRuntimeLoadPromises = new Map();
|
|
604
|
+
let baseRuntime = null;
|
|
594
605
|
let activeRuntimeKey = BASE_RUNTIME_KEY;
|
|
595
606
|
let skillReloadTimer = null;
|
|
596
607
|
let skillReloadRunning = false;
|
|
608
|
+
let skillReloadOwner = null;
|
|
609
|
+
let runtimeGeneration = 0;
|
|
610
|
+
/** @type {import('./session.js').Session | null} */
|
|
611
|
+
let runtimeOwnerSession = null;
|
|
612
|
+
const disconnectedRuntimeMcpManagers = new WeakSet();
|
|
613
|
+
|
|
614
|
+
let createRuntimeSkillManager = createSkillManager;
|
|
615
|
+
let createRuntimeMcpManager = () => new MCPManager();
|
|
616
|
+
let loadRuntimeMcpConfig = loadMCPConfig;
|
|
617
|
+
let loadRuntimeSession = loadSession;
|
|
618
|
+
const runtimeLoaderOwners = new WeakMap();
|
|
619
|
+
|
|
620
|
+
function loaderBelongsToOwner(promise, owner) {
|
|
621
|
+
const tracked = promise ? runtimeLoaderOwners.get(promise) : null;
|
|
622
|
+
return !!tracked
|
|
623
|
+
&& tracked.generation === owner?.generation
|
|
624
|
+
&& tracked.ownerSession === owner?.ownerSession;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function claimRuntimeOwnership(ownerSession) {
|
|
628
|
+
if (!ownerSession) return null;
|
|
629
|
+
runtimeOwnerSession = ownerSession;
|
|
630
|
+
return { generation: runtimeGeneration, ownerSession };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function captureRuntimeOwner(ownerSession = session) {
|
|
634
|
+
if (!ownerSession || ownerSession !== session || ownerSession !== runtimeOwnerSession) return null;
|
|
635
|
+
return { generation: runtimeGeneration, ownerSession };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function isCurrentRuntimeOwner(owner) {
|
|
639
|
+
return !!owner
|
|
640
|
+
&& owner.generation === runtimeGeneration
|
|
641
|
+
&& owner.ownerSession === runtimeOwnerSession
|
|
642
|
+
&& owner.ownerSession === session;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function invalidateRuntimeOwnership() {
|
|
646
|
+
runtimeGeneration += 1;
|
|
647
|
+
runtimeOwnerSession = null;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function runtimeBelongsToOwner(runtime, owner) {
|
|
651
|
+
return isCurrentRuntimeOwner(owner)
|
|
652
|
+
&& runtime?.generation === owner.generation
|
|
653
|
+
&& runtime?.ownerSession === owner.ownerSession;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function disconnectRuntimeMcpManager(mcpManager) {
|
|
657
|
+
if (!mcpManager || typeof mcpManager.disconnectAll !== 'function') return;
|
|
658
|
+
if (disconnectedRuntimeMcpManagers.has(mcpManager)) return;
|
|
659
|
+
disconnectedRuntimeMcpManagers.add(mcpManager);
|
|
660
|
+
try { await mcpManager.disconnectAll(); } catch { /* best-effort shutdown */ }
|
|
661
|
+
}
|
|
597
662
|
|
|
598
|
-
function replaceSessionMcpTools(mcpManager) {
|
|
599
|
-
if (!
|
|
663
|
+
function replaceSessionMcpTools(owner, mcpManager) {
|
|
664
|
+
if (!isCurrentRuntimeOwner(owner)) return { removed: 0, added: 0, skipped: true };
|
|
665
|
+
const ownerSession = owner.ownerSession;
|
|
666
|
+
if (!ownerSession.toolRegistry || typeof ownerSession.toolRegistry.replaceMcpTools !== 'function') {
|
|
600
667
|
return { removed: 0, added: 0, skipped: true };
|
|
601
668
|
}
|
|
602
669
|
try {
|
|
603
|
-
const result =
|
|
670
|
+
const result = ownerSession.toolRegistry.replaceMcpTools(mcpManager, buildMcpFlattenedTools);
|
|
604
671
|
return { ...result, skipped: false };
|
|
605
672
|
} catch (err) {
|
|
606
673
|
console.warn('[Yeaft] hot-swap MCP tools failed:', err?.message || err);
|
|
@@ -608,9 +675,10 @@ function replaceSessionMcpTools(mcpManager) {
|
|
|
608
675
|
}
|
|
609
676
|
}
|
|
610
677
|
|
|
611
|
-
function retargetVpEngines({ skillManager, mcpManager }) {
|
|
678
|
+
function retargetVpEngines(owner, { skillManager, mcpManager }) {
|
|
679
|
+
if (!isCurrentRuntimeOwner(owner)) return;
|
|
612
680
|
try {
|
|
613
|
-
|
|
681
|
+
owner.ownerSession.engine?.setRuntimeManagers?.({ skillManager, mcpManager });
|
|
614
682
|
} catch { /* best-effort default-engine retarget */ }
|
|
615
683
|
for (const eng of vpEngines.values()) {
|
|
616
684
|
try {
|
|
@@ -619,49 +687,113 @@ function retargetVpEngines({ skillManager, mcpManager }) {
|
|
|
619
687
|
}
|
|
620
688
|
}
|
|
621
689
|
|
|
622
|
-
function
|
|
690
|
+
function reloadRuntimeSkillManager(owner, skillManager, status) {
|
|
691
|
+
if (!isCurrentRuntimeOwner(owner) || typeof skillManager?.load !== 'function') {
|
|
692
|
+
return { changed: false, loaded: 0, errors: [] };
|
|
693
|
+
}
|
|
694
|
+
let result;
|
|
695
|
+
try {
|
|
696
|
+
result = skillManager.load() || {};
|
|
697
|
+
} catch (err) {
|
|
698
|
+
result = { changed: false, loaded: 0, errors: [err?.message || String(err)] };
|
|
699
|
+
}
|
|
700
|
+
if (isCurrentRuntimeOwner(owner) && status) status.skills = skillManager.size || 0;
|
|
701
|
+
return {
|
|
702
|
+
changed: !!result.changed,
|
|
703
|
+
loaded: Number(result.loaded) || 0,
|
|
704
|
+
errors: result.errors || [],
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function activateBaseRuntime(owner = captureRuntimeOwner(), { reloadSkills = true } = {}) {
|
|
709
|
+
if (!isCurrentRuntimeOwner(owner)) return { removed: 0, added: 0, skipped: true };
|
|
710
|
+
const ownerSession = owner.ownerSession;
|
|
711
|
+
const runtime = baseRuntime && runtimeBelongsToOwner(baseRuntime, owner) ? baseRuntime : null;
|
|
712
|
+
const skillManager = runtime?.skillManager || ownerSession.skillManager;
|
|
713
|
+
const mcpManager = runtime?.mcpManager || ownerSession.mcpManager;
|
|
714
|
+
const status = ownerSession.status || runtime?.status;
|
|
715
|
+
const switchingRuntime = activeRuntimeKey !== BASE_RUNTIME_KEY;
|
|
716
|
+
const reload = reloadSkills && switchingRuntime
|
|
717
|
+
? reloadRuntimeSkillManager(owner, skillManager, status)
|
|
718
|
+
: { changed: false, loaded: 0, errors: [] };
|
|
719
|
+
if (!isCurrentRuntimeOwner(owner)) return { removed: 0, added: 0, skipped: true };
|
|
623
720
|
activeRuntimeKey = BASE_RUNTIME_KEY;
|
|
624
|
-
const swap = replaceSessionMcpTools(
|
|
625
|
-
retargetVpEngines({
|
|
626
|
-
skillManager: session?.skillManager || null,
|
|
627
|
-
mcpManager: session?.mcpManager || null,
|
|
628
|
-
});
|
|
629
|
-
const status = session?.status || null;
|
|
721
|
+
const swap = replaceSessionMcpTools(owner, mcpManager);
|
|
722
|
+
retargetVpEngines(owner, { skillManager: skillManager || null, mcpManager: mcpManager || null });
|
|
630
723
|
if (status) {
|
|
631
|
-
status.skills =
|
|
724
|
+
status.skills = skillManager?.size || 0;
|
|
632
725
|
status.mcpServers = Array.isArray(status.mcpServers) ? status.mcpServers : [];
|
|
633
726
|
status.mcpFailed = Array.isArray(status.mcpFailed) ? status.mcpFailed : [];
|
|
634
|
-
status.tools =
|
|
727
|
+
status.tools = ownerSession.toolRegistry?.size || status.tools || 0;
|
|
728
|
+
}
|
|
729
|
+
if (!isCurrentRuntimeOwner(owner)) return { removed: 0, added: 0, skipped: true };
|
|
730
|
+
broadcastSkillSlashCommands({ skillManager });
|
|
731
|
+
if (switchingRuntime || reload.changed) {
|
|
732
|
+
hydrateYeaftStatusFromSession({ ...ownerSession, status }, { reason: 'skills_runtime_activate', emitEvent: true });
|
|
635
733
|
}
|
|
636
|
-
|
|
734
|
+
startSkillHotReload(owner);
|
|
637
735
|
return swap;
|
|
638
736
|
}
|
|
639
737
|
|
|
640
|
-
function activateProjectRuntime(runtime) {
|
|
641
|
-
if (!runtime) return activateBaseRuntime();
|
|
642
|
-
|
|
643
|
-
const
|
|
644
|
-
|
|
738
|
+
function activateProjectRuntime(runtime, owner = captureRuntimeOwner(), { reloadSkills = true } = {}) {
|
|
739
|
+
if (!runtime) return activateBaseRuntime(owner, { reloadSkills });
|
|
740
|
+
if (!runtimeBelongsToOwner(runtime, owner)) return { removed: 0, added: 0, skipped: true };
|
|
741
|
+
const runtimeKey = projectRuntimeKey(runtime.workDir);
|
|
742
|
+
const switchingRuntime = activeRuntimeKey !== runtimeKey;
|
|
743
|
+
const reload = reloadSkills && switchingRuntime
|
|
744
|
+
? reloadRuntimeSkillManager(owner, runtime.skillManager, runtime.status)
|
|
745
|
+
: { changed: false, loaded: 0, errors: [] };
|
|
746
|
+
if (!runtimeBelongsToOwner(runtime, owner)) return { removed: 0, added: 0, skipped: true };
|
|
747
|
+
activeRuntimeKey = runtimeKey;
|
|
748
|
+
const swap = replaceSessionMcpTools(owner, runtime.mcpManager);
|
|
749
|
+
retargetVpEngines(owner, {
|
|
645
750
|
skillManager: runtime.skillManager,
|
|
646
751
|
mcpManager: runtime.mcpManager,
|
|
647
752
|
});
|
|
648
753
|
runtime.status = {
|
|
649
754
|
...runtime.status,
|
|
650
|
-
|
|
755
|
+
skills: runtime.skillManager?.size || 0,
|
|
756
|
+
tools: owner.ownerSession.toolRegistry?.size || runtime.status?.tools || 0,
|
|
651
757
|
};
|
|
652
|
-
|
|
758
|
+
if (!runtimeBelongsToOwner(runtime, owner)) return { removed: 0, added: 0, skipped: true };
|
|
759
|
+
// A project manager already contains bundled, user, and project tiers.
|
|
760
|
+
broadcastSkillSlashCommands({ skillManager: runtime.skillManager });
|
|
761
|
+
if (switchingRuntime || reload.changed) {
|
|
762
|
+
const status = mergedStatusForProjectRuntime(runtime, owner.ownerSession);
|
|
763
|
+
hydrateYeaftStatusFromSession({ ...owner.ownerSession, status }, { reason: 'skills_runtime_activate', emitEvent: true });
|
|
764
|
+
}
|
|
765
|
+
startSkillHotReload(owner);
|
|
653
766
|
return swap;
|
|
654
767
|
}
|
|
655
768
|
|
|
656
769
|
async function shutdownProjectRuntimes() {
|
|
770
|
+
// Invalidate before the first await so every old continuation is cleanup-only.
|
|
771
|
+
invalidateRuntimeOwnership();
|
|
657
772
|
stopSkillHotReload();
|
|
658
|
-
const runtimes =
|
|
773
|
+
const runtimes = [baseRuntime, ...projectRuntimes.values()].filter(Boolean);
|
|
774
|
+
const loaderPromises = [
|
|
775
|
+
...baseRuntimeLoadPromises.values(),
|
|
776
|
+
...projectRuntimeLoadPromises.values(),
|
|
777
|
+
];
|
|
778
|
+
for (const runtime of runtimes) {
|
|
779
|
+
if (runtime?.previousSkillManager && runtime.ownerSession?.skillManager === runtime.skillManager) {
|
|
780
|
+
runtime.ownerSession.skillManager = runtime.previousSkillManager;
|
|
781
|
+
}
|
|
782
|
+
if (runtime?.previousMcpManager && runtime.ownerSession?.mcpManager === runtime.mcpManager) {
|
|
783
|
+
runtime.ownerSession.mcpManager = runtime.previousMcpManager;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
baseRuntime = null;
|
|
659
787
|
projectRuntimes.clear();
|
|
660
788
|
projectRuntimeLoadPromises.clear();
|
|
661
789
|
baseRuntimeLoadPromises.clear();
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
790
|
+
activeRuntimeKey = BASE_RUNTIME_KEY;
|
|
791
|
+
const disconnects = runtimes
|
|
792
|
+
// A loading manager may acquire its first connection after an early
|
|
793
|
+
// disconnect; the stale loader performs the reliable post-connect cleanup.
|
|
794
|
+
.filter(runtime => !runtime?.loading)
|
|
795
|
+
.map(runtime => disconnectRuntimeMcpManager(runtime?.mcpManager));
|
|
796
|
+
await Promise.allSettled([...disconnects, ...loaderPromises]);
|
|
665
797
|
}
|
|
666
798
|
|
|
667
799
|
function getVpThreadMap(sessionId, vpId) {
|
|
@@ -1057,6 +1189,9 @@ function projectPersistedToHistoryEntry(m) {
|
|
|
1057
1189
|
if (m.clientMessageId) entry.clientMessageId = m.clientMessageId;
|
|
1058
1190
|
if (m.speakerVpId) entry.speakerVpId = m.speakerVpId;
|
|
1059
1191
|
if (m.toolCallId) entry.toolCallId = m.toolCallId;
|
|
1192
|
+
if (Array.isArray(m.askUserResults) && m.askUserResults.length > 0) {
|
|
1193
|
+
entry.askUserResults = m.askUserResults;
|
|
1194
|
+
}
|
|
1060
1195
|
if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
|
|
1061
1196
|
entry.toolCalls = m.toolCalls.map(tc => ({
|
|
1062
1197
|
id: tc.id,
|
|
@@ -1072,7 +1207,7 @@ function projectPersistedToHistoryEntry(m) {
|
|
|
1072
1207
|
else if (m.time) entry.ts = m.time;
|
|
1073
1208
|
if (Array.isArray(m.images) && m.images.length > 0) entry.images = m.images;
|
|
1074
1209
|
if (Array.isArray(m.attachments) && m.attachments.length > 0) entry.attachments = m.attachments;
|
|
1075
|
-
if ((entry.role === 'user' || entry.role === 'assistant') && !entry.content && !entry.attachments && !entry.images && !entry.toolCalls && !entry.toolSummaryCount) return null;
|
|
1210
|
+
if ((entry.role === 'user' || entry.role === 'assistant') && !entry.content && !entry.attachments && !entry.images && !entry.toolCalls && !entry.toolSummaryCount && !entry.askUserResults) return null;
|
|
1076
1211
|
return entry;
|
|
1077
1212
|
}
|
|
1078
1213
|
|
|
@@ -1122,7 +1257,7 @@ function loadVisibleGroupHistoryPage(store, sessionId, limit, beforeSeq = null)
|
|
|
1122
1257
|
return { messages: [], oldestSeq: null, hasMore: false };
|
|
1123
1258
|
}
|
|
1124
1259
|
|
|
1125
|
-
const visible = rows
|
|
1260
|
+
const visible = projectVisibleSessionMessages(rows)
|
|
1126
1261
|
.map(projectPersistedToVisibleHistoryEntry)
|
|
1127
1262
|
.filter(Boolean);
|
|
1128
1263
|
const messages = sliceLastNTurns(visible, limit);
|
|
@@ -1149,7 +1284,7 @@ function ensureYeaftConversationId() {
|
|
|
1149
1284
|
}
|
|
1150
1285
|
|
|
1151
1286
|
function projectVisibleHistoryChunkMessages(messages = []) {
|
|
1152
|
-
return (messages
|
|
1287
|
+
return projectVisibleSessionMessages(messages)
|
|
1153
1288
|
.map(projectPersistedToVisibleHistoryEntry)
|
|
1154
1289
|
.filter(Boolean)
|
|
1155
1290
|
.map(m => ({
|
|
@@ -1165,6 +1300,7 @@ function projectVisibleHistoryChunkMessages(messages = []) {
|
|
|
1165
1300
|
...(Array.isArray(m.attachments) && m.attachments.length > 0 ? { attachments: hydrateHistoryAttachmentPreviews(m.attachments) } : {}),
|
|
1166
1301
|
...(Array.isArray(m.images) && m.images.length > 0 ? { images: m.images } : {}),
|
|
1167
1302
|
...(m.speakerVpId ? { speakerVpId: m.speakerVpId } : {}),
|
|
1303
|
+
...(Array.isArray(m.askUserResults) && m.askUserResults.length > 0 ? { askUserResults: m.askUserResults } : {}),
|
|
1168
1304
|
...(Number.isFinite(m.toolSummaryCount) && m.toolSummaryCount > 0
|
|
1169
1305
|
? { toolSummaryCount: m.toolSummaryCount }
|
|
1170
1306
|
: (Array.isArray(m.toolCalls) && m.toolCalls.length > 0 ? { toolSummaryCount: m.toolCalls.length } : {})),
|
|
@@ -1173,9 +1309,9 @@ function projectVisibleHistoryChunkMessages(messages = []) {
|
|
|
1173
1309
|
|
|
1174
1310
|
function emitHistoryChunk({ sessionId, messages, mode = 'older', oldestSeq = null, hasMore = false, latestSeq = null, afterSeq = null, turns = null, perfTraceId = null }) {
|
|
1175
1311
|
const projectedMessages = projectVisibleHistoryChunkMessages(messages);
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1312
|
+
// Empty deltas still carry the authoritative safe cursor and clear the
|
|
1313
|
+
// browser's syncingAfterSeq fence. Dropping this envelope leaves Session
|
|
1314
|
+
// switching stuck after hidden-only or pair-unsafe rows.
|
|
1179
1315
|
sendToServer({
|
|
1180
1316
|
type: 'yeaft_history_chunk',
|
|
1181
1317
|
conversationId: yeaftConversationId,
|
|
@@ -1404,9 +1540,12 @@ export function __testResolveVpEffectiveConfig(sessionId) {
|
|
|
1404
1540
|
* @param {{ conversationStore: object } | null} sessionLike
|
|
1405
1541
|
*/
|
|
1406
1542
|
export function __testSetSession(sessionLike) {
|
|
1543
|
+
invalidateRuntimeOwnership();
|
|
1544
|
+
stopSkillHotReload();
|
|
1407
1545
|
session = sessionLike;
|
|
1408
1546
|
sessionLoadPromise = null;
|
|
1409
|
-
if (
|
|
1547
|
+
if (sessionLike) claimRuntimeOwnership(sessionLike);
|
|
1548
|
+
else yeaftConversationId = null;
|
|
1410
1549
|
}
|
|
1411
1550
|
|
|
1412
1551
|
/**
|
|
@@ -2237,53 +2376,49 @@ function broadcastSkillSlashCommands(sessionLike, extraSkillManagers = []) {
|
|
|
2237
2376
|
});
|
|
2238
2377
|
}
|
|
2239
2378
|
|
|
2240
|
-
function
|
|
2241
|
-
|
|
2242
|
-
if (
|
|
2243
|
-
|
|
2244
|
-
|
|
2379
|
+
function activeSkillRuntime(owner) {
|
|
2380
|
+
if (!isCurrentRuntimeOwner(owner)) return null;
|
|
2381
|
+
if (activeRuntimeKey === BASE_RUNTIME_KEY) {
|
|
2382
|
+
if (baseRuntime && runtimeBelongsToOwner(baseRuntime, owner)) return baseRuntime;
|
|
2383
|
+
return {
|
|
2384
|
+
generation: owner.generation,
|
|
2385
|
+
ownerSession: owner.ownerSession,
|
|
2386
|
+
skillManager: owner.ownerSession.skillManager,
|
|
2387
|
+
status: owner.ownerSession.status,
|
|
2388
|
+
};
|
|
2245
2389
|
}
|
|
2246
|
-
|
|
2390
|
+
const runtime = projectRuntimes.get(activeRuntimeKey) || null;
|
|
2391
|
+
return runtimeBelongsToOwner(runtime, owner) ? runtime : null;
|
|
2247
2392
|
}
|
|
2248
2393
|
|
|
2249
|
-
function reloadActiveSkills() {
|
|
2394
|
+
function reloadActiveSkills(owner = skillReloadOwner || captureRuntimeOwner()) {
|
|
2395
|
+
if (!isCurrentRuntimeOwner(owner)) return { changed: false, loaded: 0, errors: [] };
|
|
2250
2396
|
if (skillReloadRunning) return { changed: false, loaded: 0, errors: [] };
|
|
2397
|
+
const runtime = activeSkillRuntime(owner);
|
|
2398
|
+
const manager = runtime?.skillManager;
|
|
2399
|
+
if (typeof manager?.load !== 'function') return { changed: false, loaded: 0, errors: [] };
|
|
2400
|
+
|
|
2251
2401
|
skillReloadRunning = true;
|
|
2252
2402
|
try {
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
}
|
|
2265
|
-
loaded += Number(result?.loaded) || 0;
|
|
2266
|
-
errors.push(...(result?.errors || []));
|
|
2403
|
+
const result = manager.load() || {};
|
|
2404
|
+
const currentRuntime = activeSkillRuntime(owner);
|
|
2405
|
+
if (!isCurrentRuntimeOwner(owner) || currentRuntime?.skillManager !== manager) {
|
|
2406
|
+
return { changed: false, loaded: 0, errors: [] };
|
|
2407
|
+
}
|
|
2408
|
+
const changed = !!result.changed;
|
|
2409
|
+
const loaded = Number(result.loaded) || 0;
|
|
2410
|
+
const errors = result.errors || [];
|
|
2411
|
+
if (runtime.status) runtime.status.skills = manager.size || 0;
|
|
2412
|
+
if (activeRuntimeKey === BASE_RUNTIME_KEY && owner.ownerSession.status) {
|
|
2413
|
+
owner.ownerSession.status.skills = manager.size || 0;
|
|
2267
2414
|
}
|
|
2268
2415
|
if (changed) {
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
if (runtime?.status && changedManagers.has(runtime.skillManager)) {
|
|
2274
|
-
runtime.status.skills = runtime.skillManager?.size || 0;
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
if (activeRuntimeKey === BASE_RUNTIME_KEY) {
|
|
2278
|
-
broadcastSkillSlashCommands(session);
|
|
2279
|
-
} else {
|
|
2280
|
-
const runtime = projectRuntimes.get(activeRuntimeKey);
|
|
2281
|
-
broadcastSkillSlashCommands(session, runtime?.skillManager ? [runtime.skillManager] : []);
|
|
2282
|
-
}
|
|
2283
|
-
const activeRuntime = activeRuntimeKey === BASE_RUNTIME_KEY ? null : projectRuntimes.get(activeRuntimeKey);
|
|
2284
|
-
const activeStatus = activeRuntime ? mergedStatusForProjectRuntime(activeRuntime) : session?.status;
|
|
2416
|
+
broadcastSkillSlashCommands({ skillManager: manager });
|
|
2417
|
+
const activeStatus = activeRuntimeKey === BASE_RUNTIME_KEY
|
|
2418
|
+
? runtime.status
|
|
2419
|
+
: mergedStatusForProjectRuntime(runtime, owner.ownerSession);
|
|
2285
2420
|
hydrateYeaftStatusFromSession(
|
|
2286
|
-
activeStatus
|
|
2421
|
+
activeStatus ? { ...owner.ownerSession, status: activeStatus } : owner.ownerSession,
|
|
2287
2422
|
{ reason: 'skills_hot_reload', emitEvent: true },
|
|
2288
2423
|
);
|
|
2289
2424
|
}
|
|
@@ -2296,195 +2431,277 @@ function reloadActiveSkills() {
|
|
|
2296
2431
|
}
|
|
2297
2432
|
}
|
|
2298
2433
|
|
|
2299
|
-
function startSkillHotReload() {
|
|
2300
|
-
if (
|
|
2301
|
-
|
|
2302
|
-
|
|
2434
|
+
function startSkillHotReload(owner = captureRuntimeOwner()) {
|
|
2435
|
+
if (!isCurrentRuntimeOwner(owner)) return false;
|
|
2436
|
+
if (skillReloadTimer && skillReloadOwner
|
|
2437
|
+
&& skillReloadOwner.generation === owner.generation
|
|
2438
|
+
&& skillReloadOwner.ownerSession === owner.ownerSession) {
|
|
2439
|
+
return false;
|
|
2440
|
+
}
|
|
2441
|
+
stopSkillHotReload();
|
|
2442
|
+
skillReloadOwner = owner;
|
|
2443
|
+
const timer = setInterval(() => {
|
|
2444
|
+
if (!isCurrentRuntimeOwner(owner)) {
|
|
2445
|
+
if (skillReloadTimer === timer) stopSkillHotReload(owner);
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
try { reloadActiveSkills(owner); }
|
|
2303
2449
|
catch (err) { console.warn('[Yeaft] skill hot reload failed:', err?.message || err); }
|
|
2304
2450
|
}, SKILL_RELOAD_INTERVAL_MS);
|
|
2305
|
-
skillReloadTimer
|
|
2451
|
+
skillReloadTimer = timer;
|
|
2452
|
+
timer.unref?.();
|
|
2453
|
+
return true;
|
|
2306
2454
|
}
|
|
2307
2455
|
|
|
2308
|
-
function stopSkillHotReload() {
|
|
2309
|
-
if (
|
|
2310
|
-
|
|
2456
|
+
function stopSkillHotReload(owner = null) {
|
|
2457
|
+
if (owner && skillReloadOwner
|
|
2458
|
+
&& (skillReloadOwner.generation !== owner.generation
|
|
2459
|
+
|| skillReloadOwner.ownerSession !== owner.ownerSession)) {
|
|
2460
|
+
return false;
|
|
2461
|
+
}
|
|
2462
|
+
if (skillReloadTimer) clearInterval(skillReloadTimer);
|
|
2311
2463
|
skillReloadTimer = null;
|
|
2464
|
+
skillReloadOwner = null;
|
|
2312
2465
|
skillReloadRunning = false;
|
|
2466
|
+
return true;
|
|
2313
2467
|
}
|
|
2314
2468
|
|
|
2315
|
-
async function loadBaseRuntime() {
|
|
2316
|
-
if (!
|
|
2317
|
-
const
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
const
|
|
2322
|
-
const
|
|
2323
|
-
|
|
2469
|
+
async function loadBaseRuntime(owner = captureRuntimeOwner()) {
|
|
2470
|
+
if (!isCurrentRuntimeOwner(owner)) return null;
|
|
2471
|
+
const ownerSession = owner.ownerSession;
|
|
2472
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir || ownerSession.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
2473
|
+
const previousSkillManager = ownerSession.skillManager;
|
|
2474
|
+
const previousMcpManager = ownerSession.mcpManager;
|
|
2475
|
+
const skillManager = createRuntimeSkillManager(yeaftDir, process.cwd());
|
|
2476
|
+
const mcpConfig = loadRuntimeMcpConfig(yeaftDir, undefined, process.cwd());
|
|
2477
|
+
const mcpManager = createRuntimeMcpManager();
|
|
2324
2478
|
let mcpStatus = { connected: [], failed: [] };
|
|
2479
|
+
const runtime = {
|
|
2480
|
+
generation: owner.generation,
|
|
2481
|
+
ownerSession,
|
|
2482
|
+
workDir: '',
|
|
2483
|
+
previousSkillManager,
|
|
2484
|
+
previousMcpManager,
|
|
2485
|
+
skillManager,
|
|
2486
|
+
mcpManager,
|
|
2487
|
+
mcpStatus,
|
|
2488
|
+
mcpConfig,
|
|
2489
|
+
loading: mcpConfig.servers.length > 0,
|
|
2490
|
+
status: {
|
|
2491
|
+
skills: skillManager.size,
|
|
2492
|
+
mcpServers: [],
|
|
2493
|
+
mcpFailed: [],
|
|
2494
|
+
mcpSkipped: mcpConfig.skipped || [],
|
|
2495
|
+
tools: ownerSession.toolRegistry?.size || 0,
|
|
2496
|
+
},
|
|
2497
|
+
};
|
|
2325
2498
|
|
|
2326
|
-
if (
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
session.status.mcpFailed = [];
|
|
2330
|
-
session.status.mcpSkipped = mcpConfig.skipped || [];
|
|
2331
|
-
session.status.tools = session.toolRegistry?.size || session.status.tools || 0;
|
|
2499
|
+
if (!isCurrentRuntimeOwner(owner)) {
|
|
2500
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2501
|
+
return null;
|
|
2332
2502
|
}
|
|
2503
|
+
baseRuntime = runtime;
|
|
2504
|
+
ownerSession.skillManager = skillManager;
|
|
2505
|
+
ownerSession.mcpManager = mcpManager;
|
|
2506
|
+
ownerSession.status = { ...ownerSession.status, ...runtime.status };
|
|
2333
2507
|
if (activeRuntimeKey === BASE_RUNTIME_KEY) {
|
|
2334
|
-
activateBaseRuntime();
|
|
2335
|
-
|
|
2336
|
-
broadcastSkillSlashCommands(session);
|
|
2508
|
+
activateBaseRuntime(owner, { reloadSkills: false });
|
|
2509
|
+
hydrateYeaftStatusFromSession(ownerSession, { reason: 'base_runtime_skills', emitEvent: true });
|
|
2337
2510
|
}
|
|
2338
|
-
startSkillHotReload();
|
|
2339
|
-
hydrateYeaftStatusFromSession(session, { reason: 'base_runtime_skills', emitEvent: true });
|
|
2340
2511
|
|
|
2341
2512
|
if (mcpConfig.servers.length > 0) {
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2513
|
+
try {
|
|
2514
|
+
mcpStatus = await mcpManager.connectAll(mcpConfig.servers);
|
|
2515
|
+
} catch (err) {
|
|
2516
|
+
runtime.loading = false;
|
|
2517
|
+
if (ownerSession.skillManager === skillManager) ownerSession.skillManager = previousSkillManager;
|
|
2518
|
+
if (ownerSession.mcpManager === mcpManager) ownerSession.mcpManager = previousMcpManager;
|
|
2519
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2520
|
+
if (baseRuntime === runtime) baseRuntime = null;
|
|
2521
|
+
throw err;
|
|
2348
2522
|
}
|
|
2523
|
+
runtime.loading = false;
|
|
2524
|
+
if (!isCurrentRuntimeOwner(owner) || baseRuntime !== runtime) {
|
|
2525
|
+
if (ownerSession.skillManager === skillManager) ownerSession.skillManager = previousSkillManager;
|
|
2526
|
+
if (ownerSession.mcpManager === mcpManager) ownerSession.mcpManager = previousMcpManager;
|
|
2527
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2528
|
+
return null;
|
|
2529
|
+
}
|
|
2530
|
+
runtime.mcpStatus = mcpStatus;
|
|
2531
|
+
runtime.status = {
|
|
2532
|
+
...runtime.status,
|
|
2533
|
+
mcpServers: mcpStatus.connected,
|
|
2534
|
+
mcpFailed: mcpStatus.failed,
|
|
2535
|
+
mcpSkipped: mcpConfig.skipped || [],
|
|
2536
|
+
tools: ownerSession.toolRegistry?.size || 0,
|
|
2537
|
+
};
|
|
2538
|
+
ownerSession.mcpManager = mcpManager;
|
|
2539
|
+
ownerSession.status = { ...ownerSession.status, ...runtime.status };
|
|
2349
2540
|
if (activeRuntimeKey === BASE_RUNTIME_KEY) {
|
|
2350
|
-
activateBaseRuntime();
|
|
2541
|
+
activateBaseRuntime(owner, { reloadSkills: false });
|
|
2542
|
+
hydrateYeaftStatusFromSession(ownerSession, { reason: 'base_runtime_mcp', emitEvent: true });
|
|
2543
|
+
if (isCurrentRuntimeOwner(owner)) {
|
|
2544
|
+
try { broadcastMcpUpdated({ reason: 'base-runtime-load' }); } catch { /* best-effort */ }
|
|
2545
|
+
}
|
|
2351
2546
|
}
|
|
2352
|
-
hydrateYeaftStatusFromSession(session, { reason: 'base_runtime_mcp', emitEvent: true });
|
|
2353
|
-
try { broadcastMcpUpdated({ reason: 'base-runtime-load' }); } catch { /* best-effort */ }
|
|
2354
2547
|
}
|
|
2355
2548
|
|
|
2356
|
-
return
|
|
2549
|
+
return isCurrentRuntimeOwner(owner) && baseRuntime === runtime ? runtime : null;
|
|
2357
2550
|
}
|
|
2358
2551
|
|
|
2359
2552
|
function scheduleBaseRuntimeLoad() {
|
|
2360
|
-
|
|
2361
|
-
if (
|
|
2362
|
-
const
|
|
2363
|
-
|
|
2553
|
+
const owner = captureRuntimeOwner();
|
|
2554
|
+
if (!owner) return null;
|
|
2555
|
+
const current = baseRuntimeLoadPromises.get(BASE_RUNTIME_KEY);
|
|
2556
|
+
if (current && loaderBelongsToOwner(current, owner)) return current;
|
|
2557
|
+
let promise;
|
|
2558
|
+
promise = new Promise(resolve => setTimeout(resolve, 0))
|
|
2559
|
+
.then(() => loadBaseRuntime(owner))
|
|
2364
2560
|
.catch((err) => {
|
|
2365
2561
|
console.warn('[Yeaft] async base runtime load failed:', err?.message || err);
|
|
2366
2562
|
return null;
|
|
2367
2563
|
})
|
|
2368
|
-
.finally(() => {
|
|
2564
|
+
.finally(() => {
|
|
2565
|
+
if (owner.generation === runtimeGeneration
|
|
2566
|
+
&& baseRuntimeLoadPromises.get(BASE_RUNTIME_KEY) === promise) {
|
|
2567
|
+
baseRuntimeLoadPromises.delete(BASE_RUNTIME_KEY);
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
runtimeLoaderOwners.set(promise, owner);
|
|
2369
2571
|
baseRuntimeLoadPromises.set(BASE_RUNTIME_KEY, promise);
|
|
2370
2572
|
return promise;
|
|
2371
2573
|
}
|
|
2372
2574
|
|
|
2373
|
-
async function loadProjectRuntime(workDir) {
|
|
2374
|
-
if (!
|
|
2575
|
+
async function loadProjectRuntime(workDir, owner = captureRuntimeOwner()) {
|
|
2576
|
+
if (!isCurrentRuntimeOwner(owner)) return null;
|
|
2577
|
+
const ownerSession = owner.ownerSession;
|
|
2375
2578
|
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
2376
2579
|
if (!normalizedWorkDir) {
|
|
2377
|
-
activateBaseRuntime();
|
|
2580
|
+
activateBaseRuntime(owner);
|
|
2378
2581
|
return null;
|
|
2379
2582
|
}
|
|
2380
2583
|
const key = projectRuntimeKey(normalizedWorkDir);
|
|
2381
2584
|
const cached = projectRuntimes.get(key);
|
|
2382
|
-
if (cached) {
|
|
2383
|
-
activateProjectRuntime(cached);
|
|
2585
|
+
if (runtimeBelongsToOwner(cached, owner)) {
|
|
2586
|
+
activateProjectRuntime(cached, owner);
|
|
2384
2587
|
return cached;
|
|
2385
2588
|
}
|
|
2386
2589
|
|
|
2387
|
-
const yeaftDir = ctx.CONFIG?.yeaftDir ||
|
|
2388
|
-
const skillRoots = normalizedWorkDir
|
|
2590
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir || ownerSession.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
2591
|
+
const skillRoots = normalizedWorkDir !== process.cwd()
|
|
2389
2592
|
? `${process.cwd()}${delimiter}${normalizedWorkDir}`
|
|
2390
2593
|
: normalizedWorkDir;
|
|
2391
|
-
const skillManager =
|
|
2392
|
-
const mcpConfig =
|
|
2393
|
-
const mcpManager =
|
|
2594
|
+
const skillManager = createRuntimeSkillManager(yeaftDir, skillRoots);
|
|
2595
|
+
const mcpConfig = loadRuntimeMcpConfig(yeaftDir, undefined, normalizedWorkDir);
|
|
2596
|
+
const mcpManager = createRuntimeMcpManager();
|
|
2394
2597
|
let mcpStatus = { connected: [], failed: [] };
|
|
2395
2598
|
const runtime = {
|
|
2599
|
+
generation: owner.generation,
|
|
2600
|
+
ownerSession,
|
|
2396
2601
|
workDir: normalizedWorkDir,
|
|
2397
2602
|
skillManager,
|
|
2398
2603
|
mcpManager,
|
|
2399
2604
|
mcpStatus,
|
|
2400
2605
|
mcpConfig,
|
|
2606
|
+
loading: mcpConfig.servers.length > 0,
|
|
2401
2607
|
status: {
|
|
2402
2608
|
skills: skillManager.size,
|
|
2403
|
-
mcpServers:
|
|
2404
|
-
mcpFailed:
|
|
2609
|
+
mcpServers: [],
|
|
2610
|
+
mcpFailed: [],
|
|
2405
2611
|
mcpSkipped: mcpConfig.skipped || [],
|
|
2406
|
-
tools:
|
|
2612
|
+
tools: ownerSession.toolRegistry?.size || 0,
|
|
2407
2613
|
},
|
|
2408
2614
|
};
|
|
2615
|
+
if (!isCurrentRuntimeOwner(owner)) {
|
|
2616
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2617
|
+
return null;
|
|
2618
|
+
}
|
|
2409
2619
|
projectRuntimes.set(key, runtime);
|
|
2410
|
-
// Skill metadata is
|
|
2411
|
-
//
|
|
2412
|
-
activateProjectRuntime(runtime);
|
|
2413
|
-
startSkillHotReload();
|
|
2620
|
+
// Skill metadata is ready before external MCP startup. Activation is still
|
|
2621
|
+
// owner-gated so reset cannot publish this runtime into a replacement session.
|
|
2622
|
+
activateProjectRuntime(runtime, owner, { reloadSkills: false });
|
|
2414
2623
|
if (mcpConfig.servers.length > 0) {
|
|
2415
|
-
|
|
2624
|
+
try {
|
|
2625
|
+
mcpStatus = await mcpManager.connectAll(mcpConfig.servers);
|
|
2626
|
+
} catch (err) {
|
|
2627
|
+
runtime.loading = false;
|
|
2628
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2629
|
+
if (projectRuntimes.get(key) === runtime) projectRuntimes.delete(key);
|
|
2630
|
+
throw err;
|
|
2631
|
+
}
|
|
2632
|
+
runtime.loading = false;
|
|
2633
|
+
if (!runtimeBelongsToOwner(runtime, owner) || projectRuntimes.get(key) !== runtime) {
|
|
2634
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2635
|
+
return null;
|
|
2636
|
+
}
|
|
2416
2637
|
runtime.mcpStatus = mcpStatus;
|
|
2417
2638
|
runtime.status = {
|
|
2418
2639
|
...runtime.status,
|
|
2419
2640
|
mcpServers: mcpStatus.connected,
|
|
2420
2641
|
mcpFailed: mcpStatus.failed,
|
|
2421
2642
|
mcpSkipped: mcpConfig.skipped || [],
|
|
2422
|
-
tools:
|
|
2643
|
+
tools: ownerSession.toolRegistry?.size || 0,
|
|
2423
2644
|
};
|
|
2424
|
-
activateProjectRuntime(runtime);
|
|
2645
|
+
if (activeRuntimeKey === key) activateProjectRuntime(runtime, owner, { reloadSkills: false });
|
|
2425
2646
|
}
|
|
2426
|
-
return runtime;
|
|
2647
|
+
return runtimeBelongsToOwner(runtime, owner) && projectRuntimes.get(key) === runtime ? runtime : null;
|
|
2427
2648
|
}
|
|
2428
2649
|
|
|
2429
|
-
|
|
2430
2650
|
function scheduleProjectRuntimeLoad(workDir) {
|
|
2651
|
+
const owner = captureRuntimeOwner();
|
|
2431
2652
|
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
2432
|
-
if (!normalizedWorkDir || !
|
|
2653
|
+
if (!normalizedWorkDir || !owner) return null;
|
|
2433
2654
|
const key = projectRuntimeKey(normalizedWorkDir);
|
|
2434
|
-
|
|
2435
|
-
if (
|
|
2436
|
-
const
|
|
2655
|
+
const cached = projectRuntimes.get(key);
|
|
2656
|
+
if (runtimeBelongsToOwner(cached, owner)) return cached;
|
|
2657
|
+
const current = projectRuntimeLoadPromises.get(key);
|
|
2658
|
+
if (current && loaderBelongsToOwner(current, owner)) return current;
|
|
2659
|
+
let promise;
|
|
2660
|
+
promise = loadProjectRuntime(normalizedWorkDir, owner)
|
|
2437
2661
|
.catch((err) => {
|
|
2438
2662
|
console.warn('[Yeaft] async project runtime load failed for %s: %s', normalizedWorkDir, err?.message || err);
|
|
2439
2663
|
return null;
|
|
2440
2664
|
})
|
|
2441
|
-
.finally(() => {
|
|
2665
|
+
.finally(() => {
|
|
2666
|
+
if (owner.generation === runtimeGeneration
|
|
2667
|
+
&& projectRuntimeLoadPromises.get(key) === promise) {
|
|
2668
|
+
projectRuntimeLoadPromises.delete(key);
|
|
2669
|
+
}
|
|
2670
|
+
});
|
|
2671
|
+
runtimeLoaderOwners.set(promise, owner);
|
|
2442
2672
|
projectRuntimeLoadPromises.set(key, promise);
|
|
2443
2673
|
return promise;
|
|
2444
2674
|
}
|
|
2445
2675
|
|
|
2446
|
-
async function ensureProjectRuntimeForSessionMeta(sessionMeta) {
|
|
2447
|
-
const workDir = normalizeSessionWorkDir(sessionMeta?.workDir);
|
|
2448
|
-
if (!workDir) {
|
|
2449
|
-
activateBaseRuntime();
|
|
2450
|
-
return null;
|
|
2451
|
-
}
|
|
2452
|
-
try {
|
|
2453
|
-
return await loadProjectRuntime(workDir);
|
|
2454
|
-
} catch (err) {
|
|
2455
|
-
console.warn('[Yeaft] project runtime load failed for %s: %s', workDir, err?.message || err);
|
|
2456
|
-
activateBaseRuntime();
|
|
2457
|
-
return null;
|
|
2458
|
-
}
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
2676
|
function getProjectRuntimeForTurn(sessionMeta) {
|
|
2677
|
+
const owner = captureRuntimeOwner();
|
|
2678
|
+
if (!owner) return null;
|
|
2462
2679
|
const workDir = normalizeSessionWorkDir(sessionMeta?.workDir);
|
|
2463
2680
|
if (!workDir) {
|
|
2464
|
-
activateBaseRuntime();
|
|
2681
|
+
activateBaseRuntime(owner);
|
|
2465
2682
|
return null;
|
|
2466
2683
|
}
|
|
2467
2684
|
const cached = projectRuntimes.get(projectRuntimeKey(workDir)) || null;
|
|
2468
|
-
if (cached) {
|
|
2469
|
-
activateProjectRuntime(cached);
|
|
2685
|
+
if (runtimeBelongsToOwner(cached, owner)) {
|
|
2686
|
+
activateProjectRuntime(cached, owner);
|
|
2470
2687
|
return cached;
|
|
2471
2688
|
}
|
|
2472
2689
|
scheduleProjectRuntimeLoad(workDir);
|
|
2473
2690
|
// Do not let a previous workDir's MCP tools leak into this turn while the
|
|
2474
2691
|
// requested project runtime is still loading in the background.
|
|
2475
|
-
activateBaseRuntime();
|
|
2692
|
+
activateBaseRuntime(owner);
|
|
2476
2693
|
return null;
|
|
2477
2694
|
}
|
|
2478
2695
|
|
|
2479
|
-
function mergedStatusForProjectRuntime(runtime) {
|
|
2480
|
-
if (!
|
|
2696
|
+
function mergedStatusForProjectRuntime(runtime, ownerSession = session) {
|
|
2697
|
+
if (!ownerSession?.status || !runtime?.status) return ownerSession?.status || { skills: 0, mcpServers: [], tools: 0 };
|
|
2481
2698
|
return {
|
|
2482
|
-
...
|
|
2483
|
-
skills: Math.max(Number(
|
|
2484
|
-
mcpServers: [...new Set([...(
|
|
2485
|
-
mcpFailed: [...(
|
|
2486
|
-
mcpSkipped: [...(
|
|
2487
|
-
tools: Math.max(Number(
|
|
2699
|
+
...ownerSession.status,
|
|
2700
|
+
skills: Math.max(Number(ownerSession.status.skills) || 0, Number(runtime.status.skills) || 0),
|
|
2701
|
+
mcpServers: [...new Set([...(ownerSession.status.mcpServers || []), ...(runtime.status.mcpServers || [])])],
|
|
2702
|
+
mcpFailed: [...(ownerSession.status.mcpFailed || []), ...(runtime.status.mcpFailed || [])],
|
|
2703
|
+
mcpSkipped: [...(ownerSession.status.mcpSkipped || []), ...(runtime.status.mcpSkipped || [])],
|
|
2704
|
+
tools: Math.max(Number(ownerSession.status.tools) || 0, Number(runtime.status.tools) || 0),
|
|
2488
2705
|
};
|
|
2489
2706
|
}
|
|
2490
2707
|
|
|
@@ -2507,6 +2724,7 @@ function pendingUserPromptEvent(requestId, pending, extra = {}) {
|
|
|
2507
2724
|
return {
|
|
2508
2725
|
type: 'ask_user_question',
|
|
2509
2726
|
requestId,
|
|
2727
|
+
toolCallId: pending.toolCallId || null,
|
|
2510
2728
|
questions: [{
|
|
2511
2729
|
question: pending.question,
|
|
2512
2730
|
options: pending.options.map(label => ({ label, description: '' })),
|
|
@@ -2546,6 +2764,7 @@ function settlePendingUserPrompt(requestId, pending, { answers = null, timedOut
|
|
|
2546
2764
|
sendSessionEvent({
|
|
2547
2765
|
type: timedOut ? 'ask_user_expired' : 'ask_user_answered',
|
|
2548
2766
|
requestId,
|
|
2767
|
+
toolCallId: pending.toolCallId || null,
|
|
2549
2768
|
...(timedOut ? { expiredAt: Date.now() } : { answers: answers || {} }),
|
|
2550
2769
|
}, {
|
|
2551
2770
|
sessionId: pending.sessionId,
|
|
@@ -4331,13 +4550,14 @@ export async function ensureSessionLoaded(opts = {}) {
|
|
|
4331
4550
|
const bootConfigRevision = sessionConfigRefreshRevision;
|
|
4332
4551
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
4333
4552
|
const normalizedWorkDir = normalizeSessionWorkDir(opts?.workDir || opts?.sessionMeta?.workDir);
|
|
4334
|
-
session = await
|
|
4553
|
+
session = await loadRuntimeSession({
|
|
4335
4554
|
...(yeaftDir && { dir: yeaftDir }),
|
|
4336
4555
|
...(normalizedWorkDir && { workDir: normalizedWorkDir }),
|
|
4337
4556
|
skipMCP: true,
|
|
4338
4557
|
skipSkills: true,
|
|
4339
4558
|
serverMode: true,
|
|
4340
4559
|
});
|
|
4560
|
+
claimRuntimeOwnership(session);
|
|
4341
4561
|
|
|
4342
4562
|
installYeaftRuntimeBridge(session);
|
|
4343
4563
|
// A save may complete after loadSession read config.json but before this
|
|
@@ -4372,13 +4592,15 @@ export async function ensureSessionLoaded(opts = {}) {
|
|
|
4372
4592
|
|
|
4373
4593
|
ensureYeaftConversationId();
|
|
4374
4594
|
scheduleBaseRuntimeLoad();
|
|
4375
|
-
|
|
4595
|
+
let bootProjectRuntime = normalizedWorkDir ? projectRuntimes.get(projectRuntimeKey(normalizedWorkDir)) || null : null;
|
|
4376
4596
|
if (normalizedWorkDir && !bootProjectRuntime) {
|
|
4377
4597
|
scheduleProjectRuntimeLoad(normalizedWorkDir);
|
|
4598
|
+
bootProjectRuntime = projectRuntimes.get(projectRuntimeKey(normalizedWorkDir)) || null;
|
|
4378
4599
|
}
|
|
4379
4600
|
const bootStatus = mergedStatusForProjectRuntime(bootProjectRuntime);
|
|
4380
4601
|
hydrateYeaftStatusFromSession({ ...session, status: bootStatus }, { reason: 'session_ready', emitEvent: true });
|
|
4381
|
-
broadcastSkillSlashCommands(
|
|
4602
|
+
if (bootProjectRuntime) broadcastSkillSlashCommands({ skillManager: bootProjectRuntime.skillManager });
|
|
4603
|
+
else broadcastSkillSlashCommands(session);
|
|
4382
4604
|
|
|
4383
4605
|
// Per-group history is hydrated lazily on first `getOrCreateSessionHistory`
|
|
4384
4606
|
// — there's no global "all conversations" tape any more.
|
|
@@ -4415,6 +4637,7 @@ export async function ensureSessionLoaded(opts = {}) {
|
|
|
4415
4637
|
try {
|
|
4416
4638
|
return await sessionLoadPromise;
|
|
4417
4639
|
} catch (err) {
|
|
4640
|
+
await shutdownProjectRuntimes();
|
|
4418
4641
|
session = null;
|
|
4419
4642
|
throw err;
|
|
4420
4643
|
} finally {
|
|
@@ -4809,7 +5032,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
4809
5032
|
// each write a copy of the user message, and history replay
|
|
4810
5033
|
// would render the user's prompt N times.
|
|
4811
5034
|
userAlreadyPersisted: true,
|
|
4812
|
-
askUser: ({ question, options }) => new Promise((resolve, reject) => {
|
|
5035
|
+
askUser: ({ question, options }, toolCall = null) => new Promise((resolve, reject) => {
|
|
4813
5036
|
const requestId = `ask_${randomUUID()}`;
|
|
4814
5037
|
const signal = vpAbort.signal;
|
|
4815
5038
|
const createdAt = Date.now();
|
|
@@ -4827,6 +5050,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
4827
5050
|
vpId,
|
|
4828
5051
|
threadId,
|
|
4829
5052
|
turnId,
|
|
5053
|
+
toolCallId: typeof toolCall?.id === 'string' ? toolCall.id : '',
|
|
4830
5054
|
question,
|
|
4831
5055
|
options: Array.isArray(options) ? options.filter(label => typeof label === 'string') : [],
|
|
4832
5056
|
createdAt,
|
|
@@ -5894,6 +6118,7 @@ export function handleYeaftAskUserAnswer(msg) {
|
|
|
5894
6118
|
if (msg.vpId && msg.vpId !== pending.vpId) return false;
|
|
5895
6119
|
if (msg.turnId && msg.turnId !== pending.turnId) return false;
|
|
5896
6120
|
if (msg.threadId && msg.threadId !== pending.threadId) return false;
|
|
6121
|
+
if (msg.toolCallId && msg.toolCallId !== pending.toolCallId) return false;
|
|
5897
6122
|
|
|
5898
6123
|
return settlePendingUserPrompt(requestId, pending, { answers: msg.answers || {} });
|
|
5899
6124
|
}
|
|
@@ -6453,7 +6678,10 @@ export async function handleYeaftLoadMoreHistory(msg) {
|
|
|
6453
6678
|
* session, then re-initialises so the frontend gets fresh config.
|
|
6454
6679
|
*/
|
|
6455
6680
|
export async function resetYeaftSession() {
|
|
6456
|
-
await
|
|
6681
|
+
// Runtime ownership must be revoked before reset reaches its first await.
|
|
6682
|
+
const oldSession = session;
|
|
6683
|
+
const oldRuntimesShutdown = shutdownProjectRuntimes();
|
|
6684
|
+
await oldRuntimesShutdown;
|
|
6457
6685
|
if (currentAbortCtrl && !currentAbortCtrl.signal.aborted) {
|
|
6458
6686
|
try { currentAbortCtrl.abort(); } catch { /* ignore */ }
|
|
6459
6687
|
}
|
|
@@ -6462,9 +6690,9 @@ export async function resetYeaftSession() {
|
|
|
6462
6690
|
try { _vpUnsubscribe(); } catch { /* ignore */ }
|
|
6463
6691
|
_vpUnsubscribe = null;
|
|
6464
6692
|
}
|
|
6465
|
-
if (
|
|
6466
|
-
await
|
|
6467
|
-
session = null;
|
|
6693
|
+
if (oldSession) {
|
|
6694
|
+
await oldSession.shutdown();
|
|
6695
|
+
if (session === oldSession) session = null;
|
|
6468
6696
|
}
|
|
6469
6697
|
yeaftConversationId = null;
|
|
6470
6698
|
// Per-group histories live on sessionContexts entries — clearing the
|
|
@@ -6508,12 +6736,13 @@ export async function resetYeaftSession() {
|
|
|
6508
6736
|
|
|
6509
6737
|
try {
|
|
6510
6738
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
6511
|
-
session = await
|
|
6739
|
+
session = await loadRuntimeSession({
|
|
6512
6740
|
...(yeaftDir && { dir: yeaftDir }),
|
|
6513
6741
|
skipMCP: true,
|
|
6514
6742
|
skipSkills: true,
|
|
6515
6743
|
serverMode: true,
|
|
6516
6744
|
});
|
|
6745
|
+
claimRuntimeOwnership(session);
|
|
6517
6746
|
installYeaftRuntimeBridge(session);
|
|
6518
6747
|
|
|
6519
6748
|
yeaftConversationId = `yeaft-${Date.now()}`;
|
|
@@ -6600,7 +6829,7 @@ function mcpRuntimeSnapshot() {
|
|
|
6600
6829
|
* pick up the change.
|
|
6601
6830
|
*/
|
|
6602
6831
|
function hotSwapMcpTools() {
|
|
6603
|
-
return replaceSessionMcpTools(session?.mcpManager);
|
|
6832
|
+
return replaceSessionMcpTools(captureRuntimeOwner(), session?.mcpManager);
|
|
6604
6833
|
}
|
|
6605
6834
|
|
|
6606
6835
|
/**
|
|
@@ -6659,7 +6888,7 @@ export async function handleYeaftMcpAdd(msg = {}) {
|
|
|
6659
6888
|
}
|
|
6660
6889
|
}
|
|
6661
6890
|
|
|
6662
|
-
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
6891
|
+
const swap = replaceSessionMcpTools(captureRuntimeOwner(), session?.mcpManager);
|
|
6663
6892
|
|
|
6664
6893
|
sendToServer({
|
|
6665
6894
|
type: 'yeaft_mcp_add_result',
|
|
@@ -6696,7 +6925,7 @@ export async function handleYeaftMcpRemove(msg = {}) {
|
|
|
6696
6925
|
}
|
|
6697
6926
|
}
|
|
6698
6927
|
|
|
6699
|
-
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
6928
|
+
const swap = replaceSessionMcpTools(captureRuntimeOwner(), session?.mcpManager);
|
|
6700
6929
|
|
|
6701
6930
|
sendToServer({
|
|
6702
6931
|
type: 'yeaft_mcp_remove_result',
|
|
@@ -6754,7 +6983,7 @@ export async function handleYeaftMcpReload(msg = {}) {
|
|
|
6754
6983
|
console.warn('[Yeaft] MCP reload failed:', err?.message || err);
|
|
6755
6984
|
}
|
|
6756
6985
|
|
|
6757
|
-
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
6986
|
+
const swap = replaceSessionMcpTools(captureRuntimeOwner(), session?.mcpManager);
|
|
6758
6987
|
|
|
6759
6988
|
sendToServer({
|
|
6760
6989
|
type: 'yeaft_mcp_reload_result',
|
|
@@ -6777,8 +7006,56 @@ export const __testHooks = {
|
|
|
6777
7006
|
return runYeaftSessionSend(msg);
|
|
6778
7007
|
},
|
|
6779
7008
|
setSessionForTest(nextSession) {
|
|
6780
|
-
|
|
6781
|
-
|
|
7009
|
+
__testSetSession(nextSession || null);
|
|
7010
|
+
},
|
|
7011
|
+
setRuntimeFactoriesForTest({
|
|
7012
|
+
createSkillManager: nextCreateSkillManager,
|
|
7013
|
+
createMcpManager: nextCreateMcpManager,
|
|
7014
|
+
loadMcpConfig: nextLoadMcpConfig,
|
|
7015
|
+
loadSession: nextLoadSession,
|
|
7016
|
+
} = {}) {
|
|
7017
|
+
if (typeof nextCreateSkillManager === 'function') createRuntimeSkillManager = nextCreateSkillManager;
|
|
7018
|
+
if (typeof nextCreateMcpManager === 'function') createRuntimeMcpManager = nextCreateMcpManager;
|
|
7019
|
+
if (typeof nextLoadMcpConfig === 'function') loadRuntimeMcpConfig = nextLoadMcpConfig;
|
|
7020
|
+
if (typeof nextLoadSession === 'function') loadRuntimeSession = nextLoadSession;
|
|
7021
|
+
},
|
|
7022
|
+
resetRuntimeFactoriesForTest() {
|
|
7023
|
+
createRuntimeSkillManager = createSkillManager;
|
|
7024
|
+
createRuntimeMcpManager = () => new MCPManager();
|
|
7025
|
+
loadRuntimeMcpConfig = loadMCPConfig;
|
|
7026
|
+
loadRuntimeSession = loadSession;
|
|
7027
|
+
},
|
|
7028
|
+
scheduleBaseRuntimeLoadForTest() {
|
|
7029
|
+
return scheduleBaseRuntimeLoad();
|
|
7030
|
+
},
|
|
7031
|
+
scheduleProjectRuntimeLoadForTest(workDir) {
|
|
7032
|
+
return scheduleProjectRuntimeLoad(workDir);
|
|
7033
|
+
},
|
|
7034
|
+
activateBaseRuntimeForTest() {
|
|
7035
|
+
return activateBaseRuntime();
|
|
7036
|
+
},
|
|
7037
|
+
activateProjectRuntimeForTest(workDir) {
|
|
7038
|
+
const runtime = projectRuntimes.get(projectRuntimeKey(workDir)) || null;
|
|
7039
|
+
return activateProjectRuntime(runtime);
|
|
7040
|
+
},
|
|
7041
|
+
startSkillHotReloadForTest() {
|
|
7042
|
+
return startSkillHotReload();
|
|
7043
|
+
},
|
|
7044
|
+
runtimeLifecycleSnapshotForTest(workDir = '') {
|
|
7045
|
+
const key = workDir ? projectRuntimeKey(workDir) : null;
|
|
7046
|
+
const owner = captureRuntimeOwner();
|
|
7047
|
+
return {
|
|
7048
|
+
generation: runtimeGeneration,
|
|
7049
|
+
ownerSession: runtimeOwnerSession,
|
|
7050
|
+
activeRuntimeKey,
|
|
7051
|
+
timerActive: !!skillReloadTimer,
|
|
7052
|
+
timerOwnerGeneration: skillReloadOwner?.generation ?? null,
|
|
7053
|
+
timerOwnerSession: skillReloadOwner?.ownerSession || null,
|
|
7054
|
+
basePromise: baseRuntimeLoadPromises.get(BASE_RUNTIME_KEY) || null,
|
|
7055
|
+
projectPromise: key ? projectRuntimeLoadPromises.get(key) || null : null,
|
|
7056
|
+
projectRuntime: key ? projectRuntimes.get(key) || null : null,
|
|
7057
|
+
activeSkillManager: activeSkillRuntime(owner)?.skillManager || null,
|
|
7058
|
+
};
|
|
6782
7059
|
},
|
|
6783
7060
|
ensureYeaftConversationIdForTest() {
|
|
6784
7061
|
return ensureYeaftConversationId();
|
|
@@ -6798,10 +7075,10 @@ export const __testHooks = {
|
|
|
6798
7075
|
vpAborts.clear();
|
|
6799
7076
|
vpInboxes.clear();
|
|
6800
7077
|
},
|
|
6801
|
-
seedPendingUserPrompt({ requestId = 'ask-test', sessionId = 'session-test', vpId = 'vp-test', threadId = 'main', turnId = 'turn-test', question = 'Continue?', options = [], createdAt = Date.now(), expiresAt = Date.now() + ASK_USER_TIMEOUT_MS } = {}) {
|
|
7078
|
+
seedPendingUserPrompt({ requestId = 'ask-test', sessionId = 'session-test', vpId = 'vp-test', threadId = 'main', turnId = 'turn-test', toolCallId = 'call-test', question = 'Continue?', options = [], createdAt = Date.now(), expiresAt = Date.now() + ASK_USER_TIMEOUT_MS } = {}) {
|
|
6802
7079
|
let resolved;
|
|
6803
7080
|
const promise = new Promise(resolve => { resolved = resolve; });
|
|
6804
|
-
pendingUserPrompts.set(requestId, { resolve: resolved, sessionId, vpId, threadId, turnId, question, options, createdAt, expiresAt, timer: null });
|
|
7081
|
+
pendingUserPrompts.set(requestId, { resolve: resolved, sessionId, vpId, threadId, turnId, toolCallId, question, options, createdAt, expiresAt, timer: null });
|
|
6805
7082
|
return promise;
|
|
6806
7083
|
},
|
|
6807
7084
|
replayPendingUserPrompts,
|
|
@@ -6852,7 +7129,10 @@ export const __testHooks = {
|
|
|
6852
7129
|
},
|
|
6853
7130
|
seedProjectRuntime(workDir, runtime) {
|
|
6854
7131
|
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
7132
|
+
const owner = captureRuntimeOwner();
|
|
6855
7133
|
const seeded = {
|
|
7134
|
+
generation: owner?.generation ?? runtimeGeneration,
|
|
7135
|
+
ownerSession: owner?.ownerSession || session,
|
|
6856
7136
|
workDir: normalizedWorkDir,
|
|
6857
7137
|
skillManager: runtime?.skillManager || { list: () => [] },
|
|
6858
7138
|
mcpManager: runtime?.mcpManager || { listTools: () => [], disconnectAll: async () => {} },
|