@yeaft/webchat-agent 1.0.197 → 1.0.198
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/package.json +1 -1
- package/yeaft/web-bridge.js +450 -179
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.198"}
|
package/package.json
CHANGED
package/yeaft/web-bridge.js
CHANGED
|
@@ -254,7 +254,7 @@ function scheduleYeaftLoadHistoryMetadataReplay(sessionId) {
|
|
|
254
254
|
const replayConversationId = yeaftConversationId;
|
|
255
255
|
setTimeout(async () => {
|
|
256
256
|
try {
|
|
257
|
-
if (!replaySession) return;
|
|
257
|
+
if (!replaySession || session !== replaySession) return;
|
|
258
258
|
try {
|
|
259
259
|
await refreshLiveSessionConfig();
|
|
260
260
|
} catch (err) {
|
|
@@ -265,10 +265,19 @@ function scheduleYeaftLoadHistoryMetadataReplay(sessionId) {
|
|
|
265
265
|
try {
|
|
266
266
|
const metaRoot = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
267
267
|
const meta = loadSessionMeta(join(sessionsRoot(metaRoot), sessionId));
|
|
268
|
-
|
|
268
|
+
const workDir = normalizeSessionWorkDir(meta?.workDir);
|
|
269
|
+
if (workDir) {
|
|
270
|
+
const scheduled = scheduleProjectRuntimeLoad(workDir);
|
|
271
|
+
projectRuntime = scheduled && typeof scheduled.then === 'function'
|
|
272
|
+
? await scheduled
|
|
273
|
+
: scheduled;
|
|
274
|
+
} else {
|
|
275
|
+
activateBaseRuntime(captureRuntimeOwner(replaySession));
|
|
276
|
+
}
|
|
269
277
|
} catch { /* best-effort project metadata */ }
|
|
270
278
|
}
|
|
271
|
-
|
|
279
|
+
if (session !== replaySession) return;
|
|
280
|
+
const status = mergedStatusForProjectRuntime(projectRuntime, replaySession);
|
|
272
281
|
hydrateYeaftStatusFromSession({ ...replaySession, status }, { reason: 'history_load', emitEvent: true });
|
|
273
282
|
sendSessionEvent({
|
|
274
283
|
type: 'session_ready',
|
|
@@ -591,16 +600,73 @@ const routePromisesByMsgId = new Map();
|
|
|
591
600
|
const projectRuntimes = new Map();
|
|
592
601
|
/** @type {Map<string, Promise<any>>} */
|
|
593
602
|
const baseRuntimeLoadPromises = new Map();
|
|
603
|
+
let baseRuntime = null;
|
|
594
604
|
let activeRuntimeKey = BASE_RUNTIME_KEY;
|
|
595
605
|
let skillReloadTimer = null;
|
|
596
606
|
let skillReloadRunning = false;
|
|
607
|
+
let skillReloadOwner = null;
|
|
608
|
+
let runtimeGeneration = 0;
|
|
609
|
+
/** @type {import('./session.js').Session | null} */
|
|
610
|
+
let runtimeOwnerSession = null;
|
|
611
|
+
const disconnectedRuntimeMcpManagers = new WeakSet();
|
|
612
|
+
|
|
613
|
+
let createRuntimeSkillManager = createSkillManager;
|
|
614
|
+
let createRuntimeMcpManager = () => new MCPManager();
|
|
615
|
+
let loadRuntimeMcpConfig = loadMCPConfig;
|
|
616
|
+
let loadRuntimeSession = loadSession;
|
|
617
|
+
const runtimeLoaderOwners = new WeakMap();
|
|
618
|
+
|
|
619
|
+
function loaderBelongsToOwner(promise, owner) {
|
|
620
|
+
const tracked = promise ? runtimeLoaderOwners.get(promise) : null;
|
|
621
|
+
return !!tracked
|
|
622
|
+
&& tracked.generation === owner?.generation
|
|
623
|
+
&& tracked.ownerSession === owner?.ownerSession;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function claimRuntimeOwnership(ownerSession) {
|
|
627
|
+
if (!ownerSession) return null;
|
|
628
|
+
runtimeOwnerSession = ownerSession;
|
|
629
|
+
return { generation: runtimeGeneration, ownerSession };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function captureRuntimeOwner(ownerSession = session) {
|
|
633
|
+
if (!ownerSession || ownerSession !== session || ownerSession !== runtimeOwnerSession) return null;
|
|
634
|
+
return { generation: runtimeGeneration, ownerSession };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function isCurrentRuntimeOwner(owner) {
|
|
638
|
+
return !!owner
|
|
639
|
+
&& owner.generation === runtimeGeneration
|
|
640
|
+
&& owner.ownerSession === runtimeOwnerSession
|
|
641
|
+
&& owner.ownerSession === session;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function invalidateRuntimeOwnership() {
|
|
645
|
+
runtimeGeneration += 1;
|
|
646
|
+
runtimeOwnerSession = null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function runtimeBelongsToOwner(runtime, owner) {
|
|
650
|
+
return isCurrentRuntimeOwner(owner)
|
|
651
|
+
&& runtime?.generation === owner.generation
|
|
652
|
+
&& runtime?.ownerSession === owner.ownerSession;
|
|
653
|
+
}
|
|
597
654
|
|
|
598
|
-
function
|
|
599
|
-
if (!
|
|
655
|
+
async function disconnectRuntimeMcpManager(mcpManager) {
|
|
656
|
+
if (!mcpManager || typeof mcpManager.disconnectAll !== 'function') return;
|
|
657
|
+
if (disconnectedRuntimeMcpManagers.has(mcpManager)) return;
|
|
658
|
+
disconnectedRuntimeMcpManagers.add(mcpManager);
|
|
659
|
+
try { await mcpManager.disconnectAll(); } catch { /* best-effort shutdown */ }
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function replaceSessionMcpTools(owner, mcpManager) {
|
|
663
|
+
if (!isCurrentRuntimeOwner(owner)) return { removed: 0, added: 0, skipped: true };
|
|
664
|
+
const ownerSession = owner.ownerSession;
|
|
665
|
+
if (!ownerSession.toolRegistry || typeof ownerSession.toolRegistry.replaceMcpTools !== 'function') {
|
|
600
666
|
return { removed: 0, added: 0, skipped: true };
|
|
601
667
|
}
|
|
602
668
|
try {
|
|
603
|
-
const result =
|
|
669
|
+
const result = ownerSession.toolRegistry.replaceMcpTools(mcpManager, buildMcpFlattenedTools);
|
|
604
670
|
return { ...result, skipped: false };
|
|
605
671
|
} catch (err) {
|
|
606
672
|
console.warn('[Yeaft] hot-swap MCP tools failed:', err?.message || err);
|
|
@@ -608,9 +674,10 @@ function replaceSessionMcpTools(mcpManager) {
|
|
|
608
674
|
}
|
|
609
675
|
}
|
|
610
676
|
|
|
611
|
-
function retargetVpEngines({ skillManager, mcpManager }) {
|
|
677
|
+
function retargetVpEngines(owner, { skillManager, mcpManager }) {
|
|
678
|
+
if (!isCurrentRuntimeOwner(owner)) return;
|
|
612
679
|
try {
|
|
613
|
-
|
|
680
|
+
owner.ownerSession.engine?.setRuntimeManagers?.({ skillManager, mcpManager });
|
|
614
681
|
} catch { /* best-effort default-engine retarget */ }
|
|
615
682
|
for (const eng of vpEngines.values()) {
|
|
616
683
|
try {
|
|
@@ -619,49 +686,113 @@ function retargetVpEngines({ skillManager, mcpManager }) {
|
|
|
619
686
|
}
|
|
620
687
|
}
|
|
621
688
|
|
|
622
|
-
function
|
|
689
|
+
function reloadRuntimeSkillManager(owner, skillManager, status) {
|
|
690
|
+
if (!isCurrentRuntimeOwner(owner) || typeof skillManager?.load !== 'function') {
|
|
691
|
+
return { changed: false, loaded: 0, errors: [] };
|
|
692
|
+
}
|
|
693
|
+
let result;
|
|
694
|
+
try {
|
|
695
|
+
result = skillManager.load() || {};
|
|
696
|
+
} catch (err) {
|
|
697
|
+
result = { changed: false, loaded: 0, errors: [err?.message || String(err)] };
|
|
698
|
+
}
|
|
699
|
+
if (isCurrentRuntimeOwner(owner) && status) status.skills = skillManager.size || 0;
|
|
700
|
+
return {
|
|
701
|
+
changed: !!result.changed,
|
|
702
|
+
loaded: Number(result.loaded) || 0,
|
|
703
|
+
errors: result.errors || [],
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function activateBaseRuntime(owner = captureRuntimeOwner(), { reloadSkills = true } = {}) {
|
|
708
|
+
if (!isCurrentRuntimeOwner(owner)) return { removed: 0, added: 0, skipped: true };
|
|
709
|
+
const ownerSession = owner.ownerSession;
|
|
710
|
+
const runtime = baseRuntime && runtimeBelongsToOwner(baseRuntime, owner) ? baseRuntime : null;
|
|
711
|
+
const skillManager = runtime?.skillManager || ownerSession.skillManager;
|
|
712
|
+
const mcpManager = runtime?.mcpManager || ownerSession.mcpManager;
|
|
713
|
+
const status = ownerSession.status || runtime?.status;
|
|
714
|
+
const switchingRuntime = activeRuntimeKey !== BASE_RUNTIME_KEY;
|
|
715
|
+
const reload = reloadSkills && switchingRuntime
|
|
716
|
+
? reloadRuntimeSkillManager(owner, skillManager, status)
|
|
717
|
+
: { changed: false, loaded: 0, errors: [] };
|
|
718
|
+
if (!isCurrentRuntimeOwner(owner)) return { removed: 0, added: 0, skipped: true };
|
|
623
719
|
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;
|
|
720
|
+
const swap = replaceSessionMcpTools(owner, mcpManager);
|
|
721
|
+
retargetVpEngines(owner, { skillManager: skillManager || null, mcpManager: mcpManager || null });
|
|
630
722
|
if (status) {
|
|
631
|
-
status.skills =
|
|
723
|
+
status.skills = skillManager?.size || 0;
|
|
632
724
|
status.mcpServers = Array.isArray(status.mcpServers) ? status.mcpServers : [];
|
|
633
725
|
status.mcpFailed = Array.isArray(status.mcpFailed) ? status.mcpFailed : [];
|
|
634
|
-
status.tools =
|
|
726
|
+
status.tools = ownerSession.toolRegistry?.size || status.tools || 0;
|
|
635
727
|
}
|
|
636
|
-
|
|
728
|
+
if (!isCurrentRuntimeOwner(owner)) return { removed: 0, added: 0, skipped: true };
|
|
729
|
+
broadcastSkillSlashCommands({ skillManager });
|
|
730
|
+
if (switchingRuntime || reload.changed) {
|
|
731
|
+
hydrateYeaftStatusFromSession({ ...ownerSession, status }, { reason: 'skills_runtime_activate', emitEvent: true });
|
|
732
|
+
}
|
|
733
|
+
startSkillHotReload(owner);
|
|
637
734
|
return swap;
|
|
638
735
|
}
|
|
639
736
|
|
|
640
|
-
function activateProjectRuntime(runtime) {
|
|
641
|
-
if (!runtime) return activateBaseRuntime();
|
|
642
|
-
|
|
643
|
-
const
|
|
644
|
-
|
|
737
|
+
function activateProjectRuntime(runtime, owner = captureRuntimeOwner(), { reloadSkills = true } = {}) {
|
|
738
|
+
if (!runtime) return activateBaseRuntime(owner, { reloadSkills });
|
|
739
|
+
if (!runtimeBelongsToOwner(runtime, owner)) return { removed: 0, added: 0, skipped: true };
|
|
740
|
+
const runtimeKey = projectRuntimeKey(runtime.workDir);
|
|
741
|
+
const switchingRuntime = activeRuntimeKey !== runtimeKey;
|
|
742
|
+
const reload = reloadSkills && switchingRuntime
|
|
743
|
+
? reloadRuntimeSkillManager(owner, runtime.skillManager, runtime.status)
|
|
744
|
+
: { changed: false, loaded: 0, errors: [] };
|
|
745
|
+
if (!runtimeBelongsToOwner(runtime, owner)) return { removed: 0, added: 0, skipped: true };
|
|
746
|
+
activeRuntimeKey = runtimeKey;
|
|
747
|
+
const swap = replaceSessionMcpTools(owner, runtime.mcpManager);
|
|
748
|
+
retargetVpEngines(owner, {
|
|
645
749
|
skillManager: runtime.skillManager,
|
|
646
750
|
mcpManager: runtime.mcpManager,
|
|
647
751
|
});
|
|
648
752
|
runtime.status = {
|
|
649
753
|
...runtime.status,
|
|
650
|
-
|
|
754
|
+
skills: runtime.skillManager?.size || 0,
|
|
755
|
+
tools: owner.ownerSession.toolRegistry?.size || runtime.status?.tools || 0,
|
|
651
756
|
};
|
|
652
|
-
|
|
757
|
+
if (!runtimeBelongsToOwner(runtime, owner)) return { removed: 0, added: 0, skipped: true };
|
|
758
|
+
// A project manager already contains bundled, user, and project tiers.
|
|
759
|
+
broadcastSkillSlashCommands({ skillManager: runtime.skillManager });
|
|
760
|
+
if (switchingRuntime || reload.changed) {
|
|
761
|
+
const status = mergedStatusForProjectRuntime(runtime, owner.ownerSession);
|
|
762
|
+
hydrateYeaftStatusFromSession({ ...owner.ownerSession, status }, { reason: 'skills_runtime_activate', emitEvent: true });
|
|
763
|
+
}
|
|
764
|
+
startSkillHotReload(owner);
|
|
653
765
|
return swap;
|
|
654
766
|
}
|
|
655
767
|
|
|
656
768
|
async function shutdownProjectRuntimes() {
|
|
769
|
+
// Invalidate before the first await so every old continuation is cleanup-only.
|
|
770
|
+
invalidateRuntimeOwnership();
|
|
657
771
|
stopSkillHotReload();
|
|
658
|
-
const runtimes =
|
|
772
|
+
const runtimes = [baseRuntime, ...projectRuntimes.values()].filter(Boolean);
|
|
773
|
+
const loaderPromises = [
|
|
774
|
+
...baseRuntimeLoadPromises.values(),
|
|
775
|
+
...projectRuntimeLoadPromises.values(),
|
|
776
|
+
];
|
|
777
|
+
for (const runtime of runtimes) {
|
|
778
|
+
if (runtime?.previousSkillManager && runtime.ownerSession?.skillManager === runtime.skillManager) {
|
|
779
|
+
runtime.ownerSession.skillManager = runtime.previousSkillManager;
|
|
780
|
+
}
|
|
781
|
+
if (runtime?.previousMcpManager && runtime.ownerSession?.mcpManager === runtime.mcpManager) {
|
|
782
|
+
runtime.ownerSession.mcpManager = runtime.previousMcpManager;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
baseRuntime = null;
|
|
659
786
|
projectRuntimes.clear();
|
|
660
787
|
projectRuntimeLoadPromises.clear();
|
|
661
788
|
baseRuntimeLoadPromises.clear();
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
789
|
+
activeRuntimeKey = BASE_RUNTIME_KEY;
|
|
790
|
+
const disconnects = runtimes
|
|
791
|
+
// A loading manager may acquire its first connection after an early
|
|
792
|
+
// disconnect; the stale loader performs the reliable post-connect cleanup.
|
|
793
|
+
.filter(runtime => !runtime?.loading)
|
|
794
|
+
.map(runtime => disconnectRuntimeMcpManager(runtime?.mcpManager));
|
|
795
|
+
await Promise.allSettled([...disconnects, ...loaderPromises]);
|
|
665
796
|
}
|
|
666
797
|
|
|
667
798
|
function getVpThreadMap(sessionId, vpId) {
|
|
@@ -1404,9 +1535,12 @@ export function __testResolveVpEffectiveConfig(sessionId) {
|
|
|
1404
1535
|
* @param {{ conversationStore: object } | null} sessionLike
|
|
1405
1536
|
*/
|
|
1406
1537
|
export function __testSetSession(sessionLike) {
|
|
1538
|
+
invalidateRuntimeOwnership();
|
|
1539
|
+
stopSkillHotReload();
|
|
1407
1540
|
session = sessionLike;
|
|
1408
1541
|
sessionLoadPromise = null;
|
|
1409
|
-
if (
|
|
1542
|
+
if (sessionLike) claimRuntimeOwnership(sessionLike);
|
|
1543
|
+
else yeaftConversationId = null;
|
|
1410
1544
|
}
|
|
1411
1545
|
|
|
1412
1546
|
/**
|
|
@@ -2237,53 +2371,49 @@ function broadcastSkillSlashCommands(sessionLike, extraSkillManagers = []) {
|
|
|
2237
2371
|
});
|
|
2238
2372
|
}
|
|
2239
2373
|
|
|
2240
|
-
function
|
|
2241
|
-
|
|
2242
|
-
if (
|
|
2243
|
-
|
|
2244
|
-
|
|
2374
|
+
function activeSkillRuntime(owner) {
|
|
2375
|
+
if (!isCurrentRuntimeOwner(owner)) return null;
|
|
2376
|
+
if (activeRuntimeKey === BASE_RUNTIME_KEY) {
|
|
2377
|
+
if (baseRuntime && runtimeBelongsToOwner(baseRuntime, owner)) return baseRuntime;
|
|
2378
|
+
return {
|
|
2379
|
+
generation: owner.generation,
|
|
2380
|
+
ownerSession: owner.ownerSession,
|
|
2381
|
+
skillManager: owner.ownerSession.skillManager,
|
|
2382
|
+
status: owner.ownerSession.status,
|
|
2383
|
+
};
|
|
2245
2384
|
}
|
|
2246
|
-
|
|
2385
|
+
const runtime = projectRuntimes.get(activeRuntimeKey) || null;
|
|
2386
|
+
return runtimeBelongsToOwner(runtime, owner) ? runtime : null;
|
|
2247
2387
|
}
|
|
2248
2388
|
|
|
2249
|
-
function reloadActiveSkills() {
|
|
2389
|
+
function reloadActiveSkills(owner = skillReloadOwner || captureRuntimeOwner()) {
|
|
2390
|
+
if (!isCurrentRuntimeOwner(owner)) return { changed: false, loaded: 0, errors: [] };
|
|
2250
2391
|
if (skillReloadRunning) return { changed: false, loaded: 0, errors: [] };
|
|
2392
|
+
const runtime = activeSkillRuntime(owner);
|
|
2393
|
+
const manager = runtime?.skillManager;
|
|
2394
|
+
if (typeof manager?.load !== 'function') return { changed: false, loaded: 0, errors: [] };
|
|
2395
|
+
|
|
2251
2396
|
skillReloadRunning = true;
|
|
2252
2397
|
try {
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
}
|
|
2265
|
-
loaded += Number(result?.loaded) || 0;
|
|
2266
|
-
errors.push(...(result?.errors || []));
|
|
2398
|
+
const result = manager.load() || {};
|
|
2399
|
+
const currentRuntime = activeSkillRuntime(owner);
|
|
2400
|
+
if (!isCurrentRuntimeOwner(owner) || currentRuntime?.skillManager !== manager) {
|
|
2401
|
+
return { changed: false, loaded: 0, errors: [] };
|
|
2402
|
+
}
|
|
2403
|
+
const changed = !!result.changed;
|
|
2404
|
+
const loaded = Number(result.loaded) || 0;
|
|
2405
|
+
const errors = result.errors || [];
|
|
2406
|
+
if (runtime.status) runtime.status.skills = manager.size || 0;
|
|
2407
|
+
if (activeRuntimeKey === BASE_RUNTIME_KEY && owner.ownerSession.status) {
|
|
2408
|
+
owner.ownerSession.status.skills = manager.size || 0;
|
|
2267
2409
|
}
|
|
2268
2410
|
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;
|
|
2411
|
+
broadcastSkillSlashCommands({ skillManager: manager });
|
|
2412
|
+
const activeStatus = activeRuntimeKey === BASE_RUNTIME_KEY
|
|
2413
|
+
? runtime.status
|
|
2414
|
+
: mergedStatusForProjectRuntime(runtime, owner.ownerSession);
|
|
2285
2415
|
hydrateYeaftStatusFromSession(
|
|
2286
|
-
activeStatus
|
|
2416
|
+
activeStatus ? { ...owner.ownerSession, status: activeStatus } : owner.ownerSession,
|
|
2287
2417
|
{ reason: 'skills_hot_reload', emitEvent: true },
|
|
2288
2418
|
);
|
|
2289
2419
|
}
|
|
@@ -2296,195 +2426,277 @@ function reloadActiveSkills() {
|
|
|
2296
2426
|
}
|
|
2297
2427
|
}
|
|
2298
2428
|
|
|
2299
|
-
function startSkillHotReload() {
|
|
2300
|
-
if (
|
|
2301
|
-
|
|
2302
|
-
|
|
2429
|
+
function startSkillHotReload(owner = captureRuntimeOwner()) {
|
|
2430
|
+
if (!isCurrentRuntimeOwner(owner)) return false;
|
|
2431
|
+
if (skillReloadTimer && skillReloadOwner
|
|
2432
|
+
&& skillReloadOwner.generation === owner.generation
|
|
2433
|
+
&& skillReloadOwner.ownerSession === owner.ownerSession) {
|
|
2434
|
+
return false;
|
|
2435
|
+
}
|
|
2436
|
+
stopSkillHotReload();
|
|
2437
|
+
skillReloadOwner = owner;
|
|
2438
|
+
const timer = setInterval(() => {
|
|
2439
|
+
if (!isCurrentRuntimeOwner(owner)) {
|
|
2440
|
+
if (skillReloadTimer === timer) stopSkillHotReload(owner);
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
try { reloadActiveSkills(owner); }
|
|
2303
2444
|
catch (err) { console.warn('[Yeaft] skill hot reload failed:', err?.message || err); }
|
|
2304
2445
|
}, SKILL_RELOAD_INTERVAL_MS);
|
|
2305
|
-
skillReloadTimer
|
|
2446
|
+
skillReloadTimer = timer;
|
|
2447
|
+
timer.unref?.();
|
|
2448
|
+
return true;
|
|
2306
2449
|
}
|
|
2307
2450
|
|
|
2308
|
-
function stopSkillHotReload() {
|
|
2309
|
-
if (
|
|
2310
|
-
|
|
2451
|
+
function stopSkillHotReload(owner = null) {
|
|
2452
|
+
if (owner && skillReloadOwner
|
|
2453
|
+
&& (skillReloadOwner.generation !== owner.generation
|
|
2454
|
+
|| skillReloadOwner.ownerSession !== owner.ownerSession)) {
|
|
2455
|
+
return false;
|
|
2456
|
+
}
|
|
2457
|
+
if (skillReloadTimer) clearInterval(skillReloadTimer);
|
|
2311
2458
|
skillReloadTimer = null;
|
|
2459
|
+
skillReloadOwner = null;
|
|
2312
2460
|
skillReloadRunning = false;
|
|
2461
|
+
return true;
|
|
2313
2462
|
}
|
|
2314
2463
|
|
|
2315
|
-
async function loadBaseRuntime() {
|
|
2316
|
-
if (!
|
|
2317
|
-
const
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
const
|
|
2322
|
-
const
|
|
2323
|
-
|
|
2464
|
+
async function loadBaseRuntime(owner = captureRuntimeOwner()) {
|
|
2465
|
+
if (!isCurrentRuntimeOwner(owner)) return null;
|
|
2466
|
+
const ownerSession = owner.ownerSession;
|
|
2467
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir || ownerSession.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
2468
|
+
const previousSkillManager = ownerSession.skillManager;
|
|
2469
|
+
const previousMcpManager = ownerSession.mcpManager;
|
|
2470
|
+
const skillManager = createRuntimeSkillManager(yeaftDir, process.cwd());
|
|
2471
|
+
const mcpConfig = loadRuntimeMcpConfig(yeaftDir, undefined, process.cwd());
|
|
2472
|
+
const mcpManager = createRuntimeMcpManager();
|
|
2324
2473
|
let mcpStatus = { connected: [], failed: [] };
|
|
2474
|
+
const runtime = {
|
|
2475
|
+
generation: owner.generation,
|
|
2476
|
+
ownerSession,
|
|
2477
|
+
workDir: '',
|
|
2478
|
+
previousSkillManager,
|
|
2479
|
+
previousMcpManager,
|
|
2480
|
+
skillManager,
|
|
2481
|
+
mcpManager,
|
|
2482
|
+
mcpStatus,
|
|
2483
|
+
mcpConfig,
|
|
2484
|
+
loading: mcpConfig.servers.length > 0,
|
|
2485
|
+
status: {
|
|
2486
|
+
skills: skillManager.size,
|
|
2487
|
+
mcpServers: [],
|
|
2488
|
+
mcpFailed: [],
|
|
2489
|
+
mcpSkipped: mcpConfig.skipped || [],
|
|
2490
|
+
tools: ownerSession.toolRegistry?.size || 0,
|
|
2491
|
+
},
|
|
2492
|
+
};
|
|
2325
2493
|
|
|
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;
|
|
2494
|
+
if (!isCurrentRuntimeOwner(owner)) {
|
|
2495
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2496
|
+
return null;
|
|
2332
2497
|
}
|
|
2498
|
+
baseRuntime = runtime;
|
|
2499
|
+
ownerSession.skillManager = skillManager;
|
|
2500
|
+
ownerSession.mcpManager = mcpManager;
|
|
2501
|
+
ownerSession.status = { ...ownerSession.status, ...runtime.status };
|
|
2333
2502
|
if (activeRuntimeKey === BASE_RUNTIME_KEY) {
|
|
2334
|
-
activateBaseRuntime();
|
|
2335
|
-
|
|
2336
|
-
broadcastSkillSlashCommands(session);
|
|
2503
|
+
activateBaseRuntime(owner, { reloadSkills: false });
|
|
2504
|
+
hydrateYeaftStatusFromSession(ownerSession, { reason: 'base_runtime_skills', emitEvent: true });
|
|
2337
2505
|
}
|
|
2338
|
-
startSkillHotReload();
|
|
2339
|
-
hydrateYeaftStatusFromSession(session, { reason: 'base_runtime_skills', emitEvent: true });
|
|
2340
2506
|
|
|
2341
2507
|
if (mcpConfig.servers.length > 0) {
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2508
|
+
try {
|
|
2509
|
+
mcpStatus = await mcpManager.connectAll(mcpConfig.servers);
|
|
2510
|
+
} catch (err) {
|
|
2511
|
+
runtime.loading = false;
|
|
2512
|
+
if (ownerSession.skillManager === skillManager) ownerSession.skillManager = previousSkillManager;
|
|
2513
|
+
if (ownerSession.mcpManager === mcpManager) ownerSession.mcpManager = previousMcpManager;
|
|
2514
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2515
|
+
if (baseRuntime === runtime) baseRuntime = null;
|
|
2516
|
+
throw err;
|
|
2517
|
+
}
|
|
2518
|
+
runtime.loading = false;
|
|
2519
|
+
if (!isCurrentRuntimeOwner(owner) || baseRuntime !== runtime) {
|
|
2520
|
+
if (ownerSession.skillManager === skillManager) ownerSession.skillManager = previousSkillManager;
|
|
2521
|
+
if (ownerSession.mcpManager === mcpManager) ownerSession.mcpManager = previousMcpManager;
|
|
2522
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2523
|
+
return null;
|
|
2348
2524
|
}
|
|
2525
|
+
runtime.mcpStatus = mcpStatus;
|
|
2526
|
+
runtime.status = {
|
|
2527
|
+
...runtime.status,
|
|
2528
|
+
mcpServers: mcpStatus.connected,
|
|
2529
|
+
mcpFailed: mcpStatus.failed,
|
|
2530
|
+
mcpSkipped: mcpConfig.skipped || [],
|
|
2531
|
+
tools: ownerSession.toolRegistry?.size || 0,
|
|
2532
|
+
};
|
|
2533
|
+
ownerSession.mcpManager = mcpManager;
|
|
2534
|
+
ownerSession.status = { ...ownerSession.status, ...runtime.status };
|
|
2349
2535
|
if (activeRuntimeKey === BASE_RUNTIME_KEY) {
|
|
2350
|
-
activateBaseRuntime();
|
|
2536
|
+
activateBaseRuntime(owner, { reloadSkills: false });
|
|
2537
|
+
hydrateYeaftStatusFromSession(ownerSession, { reason: 'base_runtime_mcp', emitEvent: true });
|
|
2538
|
+
if (isCurrentRuntimeOwner(owner)) {
|
|
2539
|
+
try { broadcastMcpUpdated({ reason: 'base-runtime-load' }); } catch { /* best-effort */ }
|
|
2540
|
+
}
|
|
2351
2541
|
}
|
|
2352
|
-
hydrateYeaftStatusFromSession(session, { reason: 'base_runtime_mcp', emitEvent: true });
|
|
2353
|
-
try { broadcastMcpUpdated({ reason: 'base-runtime-load' }); } catch { /* best-effort */ }
|
|
2354
2542
|
}
|
|
2355
2543
|
|
|
2356
|
-
return
|
|
2544
|
+
return isCurrentRuntimeOwner(owner) && baseRuntime === runtime ? runtime : null;
|
|
2357
2545
|
}
|
|
2358
2546
|
|
|
2359
2547
|
function scheduleBaseRuntimeLoad() {
|
|
2360
|
-
|
|
2361
|
-
if (
|
|
2362
|
-
const
|
|
2363
|
-
|
|
2548
|
+
const owner = captureRuntimeOwner();
|
|
2549
|
+
if (!owner) return null;
|
|
2550
|
+
const current = baseRuntimeLoadPromises.get(BASE_RUNTIME_KEY);
|
|
2551
|
+
if (current && loaderBelongsToOwner(current, owner)) return current;
|
|
2552
|
+
let promise;
|
|
2553
|
+
promise = new Promise(resolve => setTimeout(resolve, 0))
|
|
2554
|
+
.then(() => loadBaseRuntime(owner))
|
|
2364
2555
|
.catch((err) => {
|
|
2365
2556
|
console.warn('[Yeaft] async base runtime load failed:', err?.message || err);
|
|
2366
2557
|
return null;
|
|
2367
2558
|
})
|
|
2368
|
-
.finally(() => {
|
|
2559
|
+
.finally(() => {
|
|
2560
|
+
if (owner.generation === runtimeGeneration
|
|
2561
|
+
&& baseRuntimeLoadPromises.get(BASE_RUNTIME_KEY) === promise) {
|
|
2562
|
+
baseRuntimeLoadPromises.delete(BASE_RUNTIME_KEY);
|
|
2563
|
+
}
|
|
2564
|
+
});
|
|
2565
|
+
runtimeLoaderOwners.set(promise, owner);
|
|
2369
2566
|
baseRuntimeLoadPromises.set(BASE_RUNTIME_KEY, promise);
|
|
2370
2567
|
return promise;
|
|
2371
2568
|
}
|
|
2372
2569
|
|
|
2373
|
-
async function loadProjectRuntime(workDir) {
|
|
2374
|
-
if (!
|
|
2570
|
+
async function loadProjectRuntime(workDir, owner = captureRuntimeOwner()) {
|
|
2571
|
+
if (!isCurrentRuntimeOwner(owner)) return null;
|
|
2572
|
+
const ownerSession = owner.ownerSession;
|
|
2375
2573
|
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
2376
2574
|
if (!normalizedWorkDir) {
|
|
2377
|
-
activateBaseRuntime();
|
|
2575
|
+
activateBaseRuntime(owner);
|
|
2378
2576
|
return null;
|
|
2379
2577
|
}
|
|
2380
2578
|
const key = projectRuntimeKey(normalizedWorkDir);
|
|
2381
2579
|
const cached = projectRuntimes.get(key);
|
|
2382
|
-
if (cached) {
|
|
2383
|
-
activateProjectRuntime(cached);
|
|
2580
|
+
if (runtimeBelongsToOwner(cached, owner)) {
|
|
2581
|
+
activateProjectRuntime(cached, owner);
|
|
2384
2582
|
return cached;
|
|
2385
2583
|
}
|
|
2386
2584
|
|
|
2387
|
-
const yeaftDir = ctx.CONFIG?.yeaftDir ||
|
|
2388
|
-
const skillRoots = normalizedWorkDir
|
|
2585
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir || ownerSession.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
2586
|
+
const skillRoots = normalizedWorkDir !== process.cwd()
|
|
2389
2587
|
? `${process.cwd()}${delimiter}${normalizedWorkDir}`
|
|
2390
2588
|
: normalizedWorkDir;
|
|
2391
|
-
const skillManager =
|
|
2392
|
-
const mcpConfig =
|
|
2393
|
-
const mcpManager =
|
|
2589
|
+
const skillManager = createRuntimeSkillManager(yeaftDir, skillRoots);
|
|
2590
|
+
const mcpConfig = loadRuntimeMcpConfig(yeaftDir, undefined, normalizedWorkDir);
|
|
2591
|
+
const mcpManager = createRuntimeMcpManager();
|
|
2394
2592
|
let mcpStatus = { connected: [], failed: [] };
|
|
2395
2593
|
const runtime = {
|
|
2594
|
+
generation: owner.generation,
|
|
2595
|
+
ownerSession,
|
|
2396
2596
|
workDir: normalizedWorkDir,
|
|
2397
2597
|
skillManager,
|
|
2398
2598
|
mcpManager,
|
|
2399
2599
|
mcpStatus,
|
|
2400
2600
|
mcpConfig,
|
|
2601
|
+
loading: mcpConfig.servers.length > 0,
|
|
2401
2602
|
status: {
|
|
2402
2603
|
skills: skillManager.size,
|
|
2403
|
-
mcpServers:
|
|
2404
|
-
mcpFailed:
|
|
2604
|
+
mcpServers: [],
|
|
2605
|
+
mcpFailed: [],
|
|
2405
2606
|
mcpSkipped: mcpConfig.skipped || [],
|
|
2406
|
-
tools:
|
|
2607
|
+
tools: ownerSession.toolRegistry?.size || 0,
|
|
2407
2608
|
},
|
|
2408
2609
|
};
|
|
2610
|
+
if (!isCurrentRuntimeOwner(owner)) {
|
|
2611
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2409
2614
|
projectRuntimes.set(key, runtime);
|
|
2410
|
-
// Skill metadata is
|
|
2411
|
-
//
|
|
2412
|
-
activateProjectRuntime(runtime);
|
|
2413
|
-
startSkillHotReload();
|
|
2615
|
+
// Skill metadata is ready before external MCP startup. Activation is still
|
|
2616
|
+
// owner-gated so reset cannot publish this runtime into a replacement session.
|
|
2617
|
+
activateProjectRuntime(runtime, owner, { reloadSkills: false });
|
|
2414
2618
|
if (mcpConfig.servers.length > 0) {
|
|
2415
|
-
|
|
2619
|
+
try {
|
|
2620
|
+
mcpStatus = await mcpManager.connectAll(mcpConfig.servers);
|
|
2621
|
+
} catch (err) {
|
|
2622
|
+
runtime.loading = false;
|
|
2623
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2624
|
+
if (projectRuntimes.get(key) === runtime) projectRuntimes.delete(key);
|
|
2625
|
+
throw err;
|
|
2626
|
+
}
|
|
2627
|
+
runtime.loading = false;
|
|
2628
|
+
if (!runtimeBelongsToOwner(runtime, owner) || projectRuntimes.get(key) !== runtime) {
|
|
2629
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2630
|
+
return null;
|
|
2631
|
+
}
|
|
2416
2632
|
runtime.mcpStatus = mcpStatus;
|
|
2417
2633
|
runtime.status = {
|
|
2418
2634
|
...runtime.status,
|
|
2419
2635
|
mcpServers: mcpStatus.connected,
|
|
2420
2636
|
mcpFailed: mcpStatus.failed,
|
|
2421
2637
|
mcpSkipped: mcpConfig.skipped || [],
|
|
2422
|
-
tools:
|
|
2638
|
+
tools: ownerSession.toolRegistry?.size || 0,
|
|
2423
2639
|
};
|
|
2424
|
-
activateProjectRuntime(runtime);
|
|
2640
|
+
if (activeRuntimeKey === key) activateProjectRuntime(runtime, owner, { reloadSkills: false });
|
|
2425
2641
|
}
|
|
2426
|
-
return runtime;
|
|
2642
|
+
return runtimeBelongsToOwner(runtime, owner) && projectRuntimes.get(key) === runtime ? runtime : null;
|
|
2427
2643
|
}
|
|
2428
2644
|
|
|
2429
|
-
|
|
2430
2645
|
function scheduleProjectRuntimeLoad(workDir) {
|
|
2646
|
+
const owner = captureRuntimeOwner();
|
|
2431
2647
|
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
2432
|
-
if (!normalizedWorkDir || !
|
|
2648
|
+
if (!normalizedWorkDir || !owner) return null;
|
|
2433
2649
|
const key = projectRuntimeKey(normalizedWorkDir);
|
|
2434
|
-
|
|
2435
|
-
if (
|
|
2436
|
-
const
|
|
2650
|
+
const cached = projectRuntimes.get(key);
|
|
2651
|
+
if (runtimeBelongsToOwner(cached, owner)) return cached;
|
|
2652
|
+
const current = projectRuntimeLoadPromises.get(key);
|
|
2653
|
+
if (current && loaderBelongsToOwner(current, owner)) return current;
|
|
2654
|
+
let promise;
|
|
2655
|
+
promise = loadProjectRuntime(normalizedWorkDir, owner)
|
|
2437
2656
|
.catch((err) => {
|
|
2438
2657
|
console.warn('[Yeaft] async project runtime load failed for %s: %s', normalizedWorkDir, err?.message || err);
|
|
2439
2658
|
return null;
|
|
2440
2659
|
})
|
|
2441
|
-
.finally(() => {
|
|
2660
|
+
.finally(() => {
|
|
2661
|
+
if (owner.generation === runtimeGeneration
|
|
2662
|
+
&& projectRuntimeLoadPromises.get(key) === promise) {
|
|
2663
|
+
projectRuntimeLoadPromises.delete(key);
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
runtimeLoaderOwners.set(promise, owner);
|
|
2442
2667
|
projectRuntimeLoadPromises.set(key, promise);
|
|
2443
2668
|
return promise;
|
|
2444
2669
|
}
|
|
2445
2670
|
|
|
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
2671
|
function getProjectRuntimeForTurn(sessionMeta) {
|
|
2672
|
+
const owner = captureRuntimeOwner();
|
|
2673
|
+
if (!owner) return null;
|
|
2462
2674
|
const workDir = normalizeSessionWorkDir(sessionMeta?.workDir);
|
|
2463
2675
|
if (!workDir) {
|
|
2464
|
-
activateBaseRuntime();
|
|
2676
|
+
activateBaseRuntime(owner);
|
|
2465
2677
|
return null;
|
|
2466
2678
|
}
|
|
2467
2679
|
const cached = projectRuntimes.get(projectRuntimeKey(workDir)) || null;
|
|
2468
|
-
if (cached) {
|
|
2469
|
-
activateProjectRuntime(cached);
|
|
2680
|
+
if (runtimeBelongsToOwner(cached, owner)) {
|
|
2681
|
+
activateProjectRuntime(cached, owner);
|
|
2470
2682
|
return cached;
|
|
2471
2683
|
}
|
|
2472
2684
|
scheduleProjectRuntimeLoad(workDir);
|
|
2473
2685
|
// Do not let a previous workDir's MCP tools leak into this turn while the
|
|
2474
2686
|
// requested project runtime is still loading in the background.
|
|
2475
|
-
activateBaseRuntime();
|
|
2687
|
+
activateBaseRuntime(owner);
|
|
2476
2688
|
return null;
|
|
2477
2689
|
}
|
|
2478
2690
|
|
|
2479
|
-
function mergedStatusForProjectRuntime(runtime) {
|
|
2480
|
-
if (!
|
|
2691
|
+
function mergedStatusForProjectRuntime(runtime, ownerSession = session) {
|
|
2692
|
+
if (!ownerSession?.status || !runtime?.status) return ownerSession?.status || { skills: 0, mcpServers: [], tools: 0 };
|
|
2481
2693
|
return {
|
|
2482
|
-
...
|
|
2483
|
-
skills: Math.max(Number(
|
|
2484
|
-
mcpServers: [...new Set([...(
|
|
2485
|
-
mcpFailed: [...(
|
|
2486
|
-
mcpSkipped: [...(
|
|
2487
|
-
tools: Math.max(Number(
|
|
2694
|
+
...ownerSession.status,
|
|
2695
|
+
skills: Math.max(Number(ownerSession.status.skills) || 0, Number(runtime.status.skills) || 0),
|
|
2696
|
+
mcpServers: [...new Set([...(ownerSession.status.mcpServers || []), ...(runtime.status.mcpServers || [])])],
|
|
2697
|
+
mcpFailed: [...(ownerSession.status.mcpFailed || []), ...(runtime.status.mcpFailed || [])],
|
|
2698
|
+
mcpSkipped: [...(ownerSession.status.mcpSkipped || []), ...(runtime.status.mcpSkipped || [])],
|
|
2699
|
+
tools: Math.max(Number(ownerSession.status.tools) || 0, Number(runtime.status.tools) || 0),
|
|
2488
2700
|
};
|
|
2489
2701
|
}
|
|
2490
2702
|
|
|
@@ -4331,13 +4543,14 @@ export async function ensureSessionLoaded(opts = {}) {
|
|
|
4331
4543
|
const bootConfigRevision = sessionConfigRefreshRevision;
|
|
4332
4544
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
4333
4545
|
const normalizedWorkDir = normalizeSessionWorkDir(opts?.workDir || opts?.sessionMeta?.workDir);
|
|
4334
|
-
session = await
|
|
4546
|
+
session = await loadRuntimeSession({
|
|
4335
4547
|
...(yeaftDir && { dir: yeaftDir }),
|
|
4336
4548
|
...(normalizedWorkDir && { workDir: normalizedWorkDir }),
|
|
4337
4549
|
skipMCP: true,
|
|
4338
4550
|
skipSkills: true,
|
|
4339
4551
|
serverMode: true,
|
|
4340
4552
|
});
|
|
4553
|
+
claimRuntimeOwnership(session);
|
|
4341
4554
|
|
|
4342
4555
|
installYeaftRuntimeBridge(session);
|
|
4343
4556
|
// A save may complete after loadSession read config.json but before this
|
|
@@ -4372,13 +4585,15 @@ export async function ensureSessionLoaded(opts = {}) {
|
|
|
4372
4585
|
|
|
4373
4586
|
ensureYeaftConversationId();
|
|
4374
4587
|
scheduleBaseRuntimeLoad();
|
|
4375
|
-
|
|
4588
|
+
let bootProjectRuntime = normalizedWorkDir ? projectRuntimes.get(projectRuntimeKey(normalizedWorkDir)) || null : null;
|
|
4376
4589
|
if (normalizedWorkDir && !bootProjectRuntime) {
|
|
4377
4590
|
scheduleProjectRuntimeLoad(normalizedWorkDir);
|
|
4591
|
+
bootProjectRuntime = projectRuntimes.get(projectRuntimeKey(normalizedWorkDir)) || null;
|
|
4378
4592
|
}
|
|
4379
4593
|
const bootStatus = mergedStatusForProjectRuntime(bootProjectRuntime);
|
|
4380
4594
|
hydrateYeaftStatusFromSession({ ...session, status: bootStatus }, { reason: 'session_ready', emitEvent: true });
|
|
4381
|
-
broadcastSkillSlashCommands(
|
|
4595
|
+
if (bootProjectRuntime) broadcastSkillSlashCommands({ skillManager: bootProjectRuntime.skillManager });
|
|
4596
|
+
else broadcastSkillSlashCommands(session);
|
|
4382
4597
|
|
|
4383
4598
|
// Per-group history is hydrated lazily on first `getOrCreateSessionHistory`
|
|
4384
4599
|
// — there's no global "all conversations" tape any more.
|
|
@@ -4415,6 +4630,7 @@ export async function ensureSessionLoaded(opts = {}) {
|
|
|
4415
4630
|
try {
|
|
4416
4631
|
return await sessionLoadPromise;
|
|
4417
4632
|
} catch (err) {
|
|
4633
|
+
await shutdownProjectRuntimes();
|
|
4418
4634
|
session = null;
|
|
4419
4635
|
throw err;
|
|
4420
4636
|
} finally {
|
|
@@ -6453,7 +6669,10 @@ export async function handleYeaftLoadMoreHistory(msg) {
|
|
|
6453
6669
|
* session, then re-initialises so the frontend gets fresh config.
|
|
6454
6670
|
*/
|
|
6455
6671
|
export async function resetYeaftSession() {
|
|
6456
|
-
await
|
|
6672
|
+
// Runtime ownership must be revoked before reset reaches its first await.
|
|
6673
|
+
const oldSession = session;
|
|
6674
|
+
const oldRuntimesShutdown = shutdownProjectRuntimes();
|
|
6675
|
+
await oldRuntimesShutdown;
|
|
6457
6676
|
if (currentAbortCtrl && !currentAbortCtrl.signal.aborted) {
|
|
6458
6677
|
try { currentAbortCtrl.abort(); } catch { /* ignore */ }
|
|
6459
6678
|
}
|
|
@@ -6462,9 +6681,9 @@ export async function resetYeaftSession() {
|
|
|
6462
6681
|
try { _vpUnsubscribe(); } catch { /* ignore */ }
|
|
6463
6682
|
_vpUnsubscribe = null;
|
|
6464
6683
|
}
|
|
6465
|
-
if (
|
|
6466
|
-
await
|
|
6467
|
-
session = null;
|
|
6684
|
+
if (oldSession) {
|
|
6685
|
+
await oldSession.shutdown();
|
|
6686
|
+
if (session === oldSession) session = null;
|
|
6468
6687
|
}
|
|
6469
6688
|
yeaftConversationId = null;
|
|
6470
6689
|
// Per-group histories live on sessionContexts entries — clearing the
|
|
@@ -6508,12 +6727,13 @@ export async function resetYeaftSession() {
|
|
|
6508
6727
|
|
|
6509
6728
|
try {
|
|
6510
6729
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
6511
|
-
session = await
|
|
6730
|
+
session = await loadRuntimeSession({
|
|
6512
6731
|
...(yeaftDir && { dir: yeaftDir }),
|
|
6513
6732
|
skipMCP: true,
|
|
6514
6733
|
skipSkills: true,
|
|
6515
6734
|
serverMode: true,
|
|
6516
6735
|
});
|
|
6736
|
+
claimRuntimeOwnership(session);
|
|
6517
6737
|
installYeaftRuntimeBridge(session);
|
|
6518
6738
|
|
|
6519
6739
|
yeaftConversationId = `yeaft-${Date.now()}`;
|
|
@@ -6600,7 +6820,7 @@ function mcpRuntimeSnapshot() {
|
|
|
6600
6820
|
* pick up the change.
|
|
6601
6821
|
*/
|
|
6602
6822
|
function hotSwapMcpTools() {
|
|
6603
|
-
return replaceSessionMcpTools(session?.mcpManager);
|
|
6823
|
+
return replaceSessionMcpTools(captureRuntimeOwner(), session?.mcpManager);
|
|
6604
6824
|
}
|
|
6605
6825
|
|
|
6606
6826
|
/**
|
|
@@ -6659,7 +6879,7 @@ export async function handleYeaftMcpAdd(msg = {}) {
|
|
|
6659
6879
|
}
|
|
6660
6880
|
}
|
|
6661
6881
|
|
|
6662
|
-
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
6882
|
+
const swap = replaceSessionMcpTools(captureRuntimeOwner(), session?.mcpManager);
|
|
6663
6883
|
|
|
6664
6884
|
sendToServer({
|
|
6665
6885
|
type: 'yeaft_mcp_add_result',
|
|
@@ -6696,7 +6916,7 @@ export async function handleYeaftMcpRemove(msg = {}) {
|
|
|
6696
6916
|
}
|
|
6697
6917
|
}
|
|
6698
6918
|
|
|
6699
|
-
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
6919
|
+
const swap = replaceSessionMcpTools(captureRuntimeOwner(), session?.mcpManager);
|
|
6700
6920
|
|
|
6701
6921
|
sendToServer({
|
|
6702
6922
|
type: 'yeaft_mcp_remove_result',
|
|
@@ -6754,7 +6974,7 @@ export async function handleYeaftMcpReload(msg = {}) {
|
|
|
6754
6974
|
console.warn('[Yeaft] MCP reload failed:', err?.message || err);
|
|
6755
6975
|
}
|
|
6756
6976
|
|
|
6757
|
-
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
6977
|
+
const swap = replaceSessionMcpTools(captureRuntimeOwner(), session?.mcpManager);
|
|
6758
6978
|
|
|
6759
6979
|
sendToServer({
|
|
6760
6980
|
type: 'yeaft_mcp_reload_result',
|
|
@@ -6777,8 +6997,56 @@ export const __testHooks = {
|
|
|
6777
6997
|
return runYeaftSessionSend(msg);
|
|
6778
6998
|
},
|
|
6779
6999
|
setSessionForTest(nextSession) {
|
|
6780
|
-
|
|
6781
|
-
|
|
7000
|
+
__testSetSession(nextSession || null);
|
|
7001
|
+
},
|
|
7002
|
+
setRuntimeFactoriesForTest({
|
|
7003
|
+
createSkillManager: nextCreateSkillManager,
|
|
7004
|
+
createMcpManager: nextCreateMcpManager,
|
|
7005
|
+
loadMcpConfig: nextLoadMcpConfig,
|
|
7006
|
+
loadSession: nextLoadSession,
|
|
7007
|
+
} = {}) {
|
|
7008
|
+
if (typeof nextCreateSkillManager === 'function') createRuntimeSkillManager = nextCreateSkillManager;
|
|
7009
|
+
if (typeof nextCreateMcpManager === 'function') createRuntimeMcpManager = nextCreateMcpManager;
|
|
7010
|
+
if (typeof nextLoadMcpConfig === 'function') loadRuntimeMcpConfig = nextLoadMcpConfig;
|
|
7011
|
+
if (typeof nextLoadSession === 'function') loadRuntimeSession = nextLoadSession;
|
|
7012
|
+
},
|
|
7013
|
+
resetRuntimeFactoriesForTest() {
|
|
7014
|
+
createRuntimeSkillManager = createSkillManager;
|
|
7015
|
+
createRuntimeMcpManager = () => new MCPManager();
|
|
7016
|
+
loadRuntimeMcpConfig = loadMCPConfig;
|
|
7017
|
+
loadRuntimeSession = loadSession;
|
|
7018
|
+
},
|
|
7019
|
+
scheduleBaseRuntimeLoadForTest() {
|
|
7020
|
+
return scheduleBaseRuntimeLoad();
|
|
7021
|
+
},
|
|
7022
|
+
scheduleProjectRuntimeLoadForTest(workDir) {
|
|
7023
|
+
return scheduleProjectRuntimeLoad(workDir);
|
|
7024
|
+
},
|
|
7025
|
+
activateBaseRuntimeForTest() {
|
|
7026
|
+
return activateBaseRuntime();
|
|
7027
|
+
},
|
|
7028
|
+
activateProjectRuntimeForTest(workDir) {
|
|
7029
|
+
const runtime = projectRuntimes.get(projectRuntimeKey(workDir)) || null;
|
|
7030
|
+
return activateProjectRuntime(runtime);
|
|
7031
|
+
},
|
|
7032
|
+
startSkillHotReloadForTest() {
|
|
7033
|
+
return startSkillHotReload();
|
|
7034
|
+
},
|
|
7035
|
+
runtimeLifecycleSnapshotForTest(workDir = '') {
|
|
7036
|
+
const key = workDir ? projectRuntimeKey(workDir) : null;
|
|
7037
|
+
const owner = captureRuntimeOwner();
|
|
7038
|
+
return {
|
|
7039
|
+
generation: runtimeGeneration,
|
|
7040
|
+
ownerSession: runtimeOwnerSession,
|
|
7041
|
+
activeRuntimeKey,
|
|
7042
|
+
timerActive: !!skillReloadTimer,
|
|
7043
|
+
timerOwnerGeneration: skillReloadOwner?.generation ?? null,
|
|
7044
|
+
timerOwnerSession: skillReloadOwner?.ownerSession || null,
|
|
7045
|
+
basePromise: baseRuntimeLoadPromises.get(BASE_RUNTIME_KEY) || null,
|
|
7046
|
+
projectPromise: key ? projectRuntimeLoadPromises.get(key) || null : null,
|
|
7047
|
+
projectRuntime: key ? projectRuntimes.get(key) || null : null,
|
|
7048
|
+
activeSkillManager: activeSkillRuntime(owner)?.skillManager || null,
|
|
7049
|
+
};
|
|
6782
7050
|
},
|
|
6783
7051
|
ensureYeaftConversationIdForTest() {
|
|
6784
7052
|
return ensureYeaftConversationId();
|
|
@@ -6852,7 +7120,10 @@ export const __testHooks = {
|
|
|
6852
7120
|
},
|
|
6853
7121
|
seedProjectRuntime(workDir, runtime) {
|
|
6854
7122
|
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
7123
|
+
const owner = captureRuntimeOwner();
|
|
6855
7124
|
const seeded = {
|
|
7125
|
+
generation: owner?.generation ?? runtimeGeneration,
|
|
7126
|
+
ownerSession: owner?.ownerSession || session,
|
|
6856
7127
|
workDir: normalizedWorkDir,
|
|
6857
7128
|
skillManager: runtime?.skillManager || { list: () => [] },
|
|
6858
7129
|
mcpManager: runtime?.mcpManager || { listTools: () => [], disconnectAll: async () => {} },
|