@yeaft/webchat-agent 1.0.196 → 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/server/handlers/agent-output.js +27 -11
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +77 -77
- 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/skills.js +36 -3
- package/yeaft/web-bridge.js +490 -146
package/yeaft/web-bridge.js
CHANGED
|
@@ -80,6 +80,7 @@ const LEGACY_SKILL_COMMAND_PREFIX = 'skill:';
|
|
|
80
80
|
const YEAFT_SKILL_COMMAND_PREFIX = 'yeaft-skills:';
|
|
81
81
|
const PROJECT_SKILL_TIERS = new Set(['project', 'project-claude', 'project-codex']);
|
|
82
82
|
const BASE_RUNTIME_KEY = '__base__';
|
|
83
|
+
const SKILL_RELOAD_INTERVAL_MS = 2_000;
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
86
|
* Live AskUser requests. They are Session-scoped runtime state rather than a
|
|
@@ -253,7 +254,7 @@ function scheduleYeaftLoadHistoryMetadataReplay(sessionId) {
|
|
|
253
254
|
const replayConversationId = yeaftConversationId;
|
|
254
255
|
setTimeout(async () => {
|
|
255
256
|
try {
|
|
256
|
-
if (!replaySession) return;
|
|
257
|
+
if (!replaySession || session !== replaySession) return;
|
|
257
258
|
try {
|
|
258
259
|
await refreshLiveSessionConfig();
|
|
259
260
|
} catch (err) {
|
|
@@ -264,10 +265,19 @@ function scheduleYeaftLoadHistoryMetadataReplay(sessionId) {
|
|
|
264
265
|
try {
|
|
265
266
|
const metaRoot = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
266
267
|
const meta = loadSessionMeta(join(sessionsRoot(metaRoot), sessionId));
|
|
267
|
-
|
|
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
|
+
}
|
|
268
277
|
} catch { /* best-effort project metadata */ }
|
|
269
278
|
}
|
|
270
|
-
|
|
279
|
+
if (session !== replaySession) return;
|
|
280
|
+
const status = mergedStatusForProjectRuntime(projectRuntime, replaySession);
|
|
271
281
|
hydrateYeaftStatusFromSession({ ...replaySession, status }, { reason: 'history_load', emitEvent: true });
|
|
272
282
|
sendSessionEvent({
|
|
273
283
|
type: 'session_ready',
|
|
@@ -590,14 +600,73 @@ const routePromisesByMsgId = new Map();
|
|
|
590
600
|
const projectRuntimes = new Map();
|
|
591
601
|
/** @type {Map<string, Promise<any>>} */
|
|
592
602
|
const baseRuntimeLoadPromises = new Map();
|
|
603
|
+
let baseRuntime = null;
|
|
593
604
|
let activeRuntimeKey = BASE_RUNTIME_KEY;
|
|
605
|
+
let skillReloadTimer = null;
|
|
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
|
+
}
|
|
654
|
+
|
|
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
|
+
}
|
|
594
661
|
|
|
595
|
-
function replaceSessionMcpTools(mcpManager) {
|
|
596
|
-
if (!
|
|
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') {
|
|
597
666
|
return { removed: 0, added: 0, skipped: true };
|
|
598
667
|
}
|
|
599
668
|
try {
|
|
600
|
-
const result =
|
|
669
|
+
const result = ownerSession.toolRegistry.replaceMcpTools(mcpManager, buildMcpFlattenedTools);
|
|
601
670
|
return { ...result, skipped: false };
|
|
602
671
|
} catch (err) {
|
|
603
672
|
console.warn('[Yeaft] hot-swap MCP tools failed:', err?.message || err);
|
|
@@ -605,9 +674,10 @@ function replaceSessionMcpTools(mcpManager) {
|
|
|
605
674
|
}
|
|
606
675
|
}
|
|
607
676
|
|
|
608
|
-
function retargetVpEngines({ skillManager, mcpManager }) {
|
|
677
|
+
function retargetVpEngines(owner, { skillManager, mcpManager }) {
|
|
678
|
+
if (!isCurrentRuntimeOwner(owner)) return;
|
|
609
679
|
try {
|
|
610
|
-
|
|
680
|
+
owner.ownerSession.engine?.setRuntimeManagers?.({ skillManager, mcpManager });
|
|
611
681
|
} catch { /* best-effort default-engine retarget */ }
|
|
612
682
|
for (const eng of vpEngines.values()) {
|
|
613
683
|
try {
|
|
@@ -616,48 +686,113 @@ function retargetVpEngines({ skillManager, mcpManager }) {
|
|
|
616
686
|
}
|
|
617
687
|
}
|
|
618
688
|
|
|
619
|
-
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 };
|
|
620
719
|
activeRuntimeKey = BASE_RUNTIME_KEY;
|
|
621
|
-
const swap = replaceSessionMcpTools(
|
|
622
|
-
retargetVpEngines({
|
|
623
|
-
skillManager: session?.skillManager || null,
|
|
624
|
-
mcpManager: session?.mcpManager || null,
|
|
625
|
-
});
|
|
626
|
-
const status = session?.status || null;
|
|
720
|
+
const swap = replaceSessionMcpTools(owner, mcpManager);
|
|
721
|
+
retargetVpEngines(owner, { skillManager: skillManager || null, mcpManager: mcpManager || null });
|
|
627
722
|
if (status) {
|
|
628
|
-
status.skills =
|
|
723
|
+
status.skills = skillManager?.size || 0;
|
|
629
724
|
status.mcpServers = Array.isArray(status.mcpServers) ? status.mcpServers : [];
|
|
630
725
|
status.mcpFailed = Array.isArray(status.mcpFailed) ? status.mcpFailed : [];
|
|
631
|
-
status.tools =
|
|
726
|
+
status.tools = ownerSession.toolRegistry?.size || status.tools || 0;
|
|
727
|
+
}
|
|
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 });
|
|
632
732
|
}
|
|
633
|
-
|
|
733
|
+
startSkillHotReload(owner);
|
|
634
734
|
return swap;
|
|
635
735
|
}
|
|
636
736
|
|
|
637
|
-
function activateProjectRuntime(runtime) {
|
|
638
|
-
if (!runtime) return activateBaseRuntime();
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
|
|
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, {
|
|
642
749
|
skillManager: runtime.skillManager,
|
|
643
750
|
mcpManager: runtime.mcpManager,
|
|
644
751
|
});
|
|
645
752
|
runtime.status = {
|
|
646
753
|
...runtime.status,
|
|
647
|
-
|
|
754
|
+
skills: runtime.skillManager?.size || 0,
|
|
755
|
+
tools: owner.ownerSession.toolRegistry?.size || runtime.status?.tools || 0,
|
|
648
756
|
};
|
|
649
|
-
|
|
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);
|
|
650
765
|
return swap;
|
|
651
766
|
}
|
|
652
767
|
|
|
653
768
|
async function shutdownProjectRuntimes() {
|
|
654
|
-
|
|
769
|
+
// Invalidate before the first await so every old continuation is cleanup-only.
|
|
770
|
+
invalidateRuntimeOwnership();
|
|
771
|
+
stopSkillHotReload();
|
|
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;
|
|
655
786
|
projectRuntimes.clear();
|
|
656
787
|
projectRuntimeLoadPromises.clear();
|
|
657
788
|
baseRuntimeLoadPromises.clear();
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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]);
|
|
661
796
|
}
|
|
662
797
|
|
|
663
798
|
function getVpThreadMap(sessionId, vpId) {
|
|
@@ -936,7 +1071,6 @@ let yeaftConversationId = null;
|
|
|
936
1071
|
* creates/replaces the virtual Yeaft conversation id so `/` autocomplete never
|
|
937
1072
|
* falls back to built-ins while full Session metadata is still loading. */
|
|
938
1073
|
let lastYeaftSlashCommandSnapshot = null;
|
|
939
|
-
let lastYeaftGeneratedSlashCommands = new Set();
|
|
940
1074
|
/** @type {Map<string, Promise<any>>} */
|
|
941
1075
|
const projectRuntimeLoadPromises = new Map();
|
|
942
1076
|
|
|
@@ -1401,9 +1535,12 @@ export function __testResolveVpEffectiveConfig(sessionId) {
|
|
|
1401
1535
|
* @param {{ conversationStore: object } | null} sessionLike
|
|
1402
1536
|
*/
|
|
1403
1537
|
export function __testSetSession(sessionLike) {
|
|
1538
|
+
invalidateRuntimeOwnership();
|
|
1539
|
+
stopSkillHotReload();
|
|
1404
1540
|
session = sessionLike;
|
|
1405
1541
|
sessionLoadPromise = null;
|
|
1406
|
-
if (
|
|
1542
|
+
if (sessionLike) claimRuntimeOwnership(sessionLike);
|
|
1543
|
+
else yeaftConversationId = null;
|
|
1407
1544
|
}
|
|
1408
1545
|
|
|
1409
1546
|
/**
|
|
@@ -2181,6 +2318,7 @@ export function buildMergedSkillSlashCommands(skillManagers = []) {
|
|
|
2181
2318
|
function sendSkillSlashCommandsUpdate({ conversationId, slashCommands, slashCommandDescriptions }) {
|
|
2182
2319
|
sendToServer({
|
|
2183
2320
|
type: 'slash_commands_update',
|
|
2321
|
+
commandSet: 'yeaft',
|
|
2184
2322
|
agentId: ctx.AGENT_ID || ctx.agentId || null,
|
|
2185
2323
|
conversationId,
|
|
2186
2324
|
slashCommands,
|
|
@@ -2221,21 +2359,10 @@ export function preloadYeaftSkillSlashCommands() {
|
|
|
2221
2359
|
|
|
2222
2360
|
function broadcastSkillSlashCommands(sessionLike, extraSkillManagers = []) {
|
|
2223
2361
|
const managers = [sessionLike?.skillManager, ...extraSkillManagers].filter(Boolean);
|
|
2224
|
-
const { commands, descriptions } = buildMergedSkillSlashCommands(managers);
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|| cmd.startsWith(YEAFT_SKILL_COMMAND_PREFIX));
|
|
2229
|
-
const nonSkillCommands = (ctx.slashCommands || []).filter(cmd => !isYeaftSkillCommand(cmd));
|
|
2230
|
-
const slashCommands = [...new Set([...nonSkillCommands, ...commands])];
|
|
2231
|
-
const slashCommandDescriptions = Object.fromEntries(
|
|
2232
|
-
Object.entries(ctx.slashCommandDescriptions || {})
|
|
2233
|
-
.filter(([cmd]) => !isYeaftSkillCommand(cmd))
|
|
2234
|
-
);
|
|
2235
|
-
Object.assign(slashCommandDescriptions, descriptions);
|
|
2236
|
-
ctx.slashCommands = slashCommands;
|
|
2237
|
-
ctx.slashCommandDescriptions = slashCommandDescriptions;
|
|
2238
|
-
lastYeaftGeneratedSlashCommands = new Set(commands);
|
|
2362
|
+
const { commands: slashCommands, descriptions: slashCommandDescriptions } = buildMergedSkillSlashCommands(managers);
|
|
2363
|
+
// Yeaft owns an isolated command catalogue. Reusing ctx's Claude Chat
|
|
2364
|
+
// commands made unsupported entries such as /compact and /mcp appear in a
|
|
2365
|
+
// Session even though the Yeaft engine only parses effort and Skill prefixes.
|
|
2239
2366
|
lastYeaftSlashCommandSnapshot = { slashCommands, slashCommandDescriptions };
|
|
2240
2367
|
sendSkillSlashCommandsUpdate({
|
|
2241
2368
|
conversationId: yeaftConversationId || '__preload__',
|
|
@@ -2244,177 +2371,332 @@ function broadcastSkillSlashCommands(sessionLike, extraSkillManagers = []) {
|
|
|
2244
2371
|
});
|
|
2245
2372
|
}
|
|
2246
2373
|
|
|
2247
|
-
|
|
2248
|
-
if (!
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
const runtime = projectRuntimes.get(activeRuntimeKey) || null;
|
|
2386
|
+
return runtimeBelongsToOwner(runtime, owner) ? runtime : null;
|
|
2387
|
+
}
|
|
2252
2388
|
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2389
|
+
function reloadActiveSkills(owner = skillReloadOwner || captureRuntimeOwner()) {
|
|
2390
|
+
if (!isCurrentRuntimeOwner(owner)) return { changed: false, loaded: 0, errors: [] };
|
|
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
|
+
|
|
2396
|
+
skillReloadRunning = true;
|
|
2397
|
+
try {
|
|
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;
|
|
2409
|
+
}
|
|
2410
|
+
if (changed) {
|
|
2411
|
+
broadcastSkillSlashCommands({ skillManager: manager });
|
|
2412
|
+
const activeStatus = activeRuntimeKey === BASE_RUNTIME_KEY
|
|
2413
|
+
? runtime.status
|
|
2414
|
+
: mergedStatusForProjectRuntime(runtime, owner.ownerSession);
|
|
2415
|
+
hydrateYeaftStatusFromSession(
|
|
2416
|
+
activeStatus ? { ...owner.ownerSession, status: activeStatus } : owner.ownerSession,
|
|
2417
|
+
{ reason: 'skills_hot_reload', emitEvent: true },
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
if (errors.length > 0) {
|
|
2421
|
+
console.warn(`[Yeaft] skill hot reload completed with ${errors.length} error(s):`, errors.join('; '));
|
|
2422
|
+
}
|
|
2423
|
+
return { changed, loaded, errors };
|
|
2424
|
+
} finally {
|
|
2425
|
+
skillReloadRunning = false;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
|
|
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); }
|
|
2444
|
+
catch (err) { console.warn('[Yeaft] skill hot reload failed:', err?.message || err); }
|
|
2445
|
+
}, SKILL_RELOAD_INTERVAL_MS);
|
|
2446
|
+
skillReloadTimer = timer;
|
|
2447
|
+
timer.unref?.();
|
|
2448
|
+
return true;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
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);
|
|
2458
|
+
skillReloadTimer = null;
|
|
2459
|
+
skillReloadOwner = null;
|
|
2460
|
+
skillReloadRunning = false;
|
|
2461
|
+
return true;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
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();
|
|
2256
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
|
+
};
|
|
2257
2493
|
|
|
2258
|
-
if (
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
session.status.mcpFailed = [];
|
|
2262
|
-
session.status.mcpSkipped = mcpConfig.skipped || [];
|
|
2263
|
-
session.status.tools = session.toolRegistry?.size || session.status.tools || 0;
|
|
2494
|
+
if (!isCurrentRuntimeOwner(owner)) {
|
|
2495
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2496
|
+
return null;
|
|
2264
2497
|
}
|
|
2498
|
+
baseRuntime = runtime;
|
|
2499
|
+
ownerSession.skillManager = skillManager;
|
|
2500
|
+
ownerSession.mcpManager = mcpManager;
|
|
2501
|
+
ownerSession.status = { ...ownerSession.status, ...runtime.status };
|
|
2265
2502
|
if (activeRuntimeKey === BASE_RUNTIME_KEY) {
|
|
2266
|
-
activateBaseRuntime();
|
|
2267
|
-
|
|
2268
|
-
broadcastSkillSlashCommands(session);
|
|
2503
|
+
activateBaseRuntime(owner, { reloadSkills: false });
|
|
2504
|
+
hydrateYeaftStatusFromSession(ownerSession, { reason: 'base_runtime_skills', emitEvent: true });
|
|
2269
2505
|
}
|
|
2270
|
-
hydrateYeaftStatusFromSession(session, { reason: 'base_runtime_skills', emitEvent: true });
|
|
2271
2506
|
|
|
2272
2507
|
if (mcpConfig.servers.length > 0) {
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
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;
|
|
2279
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 };
|
|
2280
2535
|
if (activeRuntimeKey === BASE_RUNTIME_KEY) {
|
|
2281
|
-
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
|
+
}
|
|
2282
2541
|
}
|
|
2283
|
-
hydrateYeaftStatusFromSession(session, { reason: 'base_runtime_mcp', emitEvent: true });
|
|
2284
|
-
try { broadcastMcpUpdated({ reason: 'base-runtime-load' }); } catch { /* best-effort */ }
|
|
2285
2542
|
}
|
|
2286
2543
|
|
|
2287
|
-
return
|
|
2544
|
+
return isCurrentRuntimeOwner(owner) && baseRuntime === runtime ? runtime : null;
|
|
2288
2545
|
}
|
|
2289
2546
|
|
|
2290
2547
|
function scheduleBaseRuntimeLoad() {
|
|
2291
|
-
|
|
2292
|
-
if (
|
|
2293
|
-
const
|
|
2294
|
-
|
|
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))
|
|
2295
2555
|
.catch((err) => {
|
|
2296
2556
|
console.warn('[Yeaft] async base runtime load failed:', err?.message || err);
|
|
2297
2557
|
return null;
|
|
2298
2558
|
})
|
|
2299
|
-
.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);
|
|
2300
2566
|
baseRuntimeLoadPromises.set(BASE_RUNTIME_KEY, promise);
|
|
2301
2567
|
return promise;
|
|
2302
2568
|
}
|
|
2303
2569
|
|
|
2304
|
-
async function loadProjectRuntime(workDir) {
|
|
2305
|
-
if (!
|
|
2570
|
+
async function loadProjectRuntime(workDir, owner = captureRuntimeOwner()) {
|
|
2571
|
+
if (!isCurrentRuntimeOwner(owner)) return null;
|
|
2572
|
+
const ownerSession = owner.ownerSession;
|
|
2306
2573
|
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
2307
2574
|
if (!normalizedWorkDir) {
|
|
2308
|
-
activateBaseRuntime();
|
|
2575
|
+
activateBaseRuntime(owner);
|
|
2309
2576
|
return null;
|
|
2310
2577
|
}
|
|
2311
2578
|
const key = projectRuntimeKey(normalizedWorkDir);
|
|
2312
2579
|
const cached = projectRuntimes.get(key);
|
|
2313
|
-
if (cached) {
|
|
2314
|
-
activateProjectRuntime(cached);
|
|
2580
|
+
if (runtimeBelongsToOwner(cached, owner)) {
|
|
2581
|
+
activateProjectRuntime(cached, owner);
|
|
2315
2582
|
return cached;
|
|
2316
2583
|
}
|
|
2317
2584
|
|
|
2318
|
-
const yeaftDir = ctx.CONFIG?.yeaftDir ||
|
|
2319
|
-
const skillRoots = normalizedWorkDir
|
|
2585
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir || ownerSession.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
2586
|
+
const skillRoots = normalizedWorkDir !== process.cwd()
|
|
2320
2587
|
? `${process.cwd()}${delimiter}${normalizedWorkDir}`
|
|
2321
2588
|
: normalizedWorkDir;
|
|
2322
|
-
const skillManager =
|
|
2323
|
-
const mcpConfig =
|
|
2324
|
-
const mcpManager =
|
|
2589
|
+
const skillManager = createRuntimeSkillManager(yeaftDir, skillRoots);
|
|
2590
|
+
const mcpConfig = loadRuntimeMcpConfig(yeaftDir, undefined, normalizedWorkDir);
|
|
2591
|
+
const mcpManager = createRuntimeMcpManager();
|
|
2325
2592
|
let mcpStatus = { connected: [], failed: [] };
|
|
2326
2593
|
const runtime = {
|
|
2594
|
+
generation: owner.generation,
|
|
2595
|
+
ownerSession,
|
|
2327
2596
|
workDir: normalizedWorkDir,
|
|
2328
2597
|
skillManager,
|
|
2329
2598
|
mcpManager,
|
|
2330
2599
|
mcpStatus,
|
|
2331
2600
|
mcpConfig,
|
|
2601
|
+
loading: mcpConfig.servers.length > 0,
|
|
2332
2602
|
status: {
|
|
2333
2603
|
skills: skillManager.size,
|
|
2334
|
-
mcpServers:
|
|
2335
|
-
mcpFailed:
|
|
2604
|
+
mcpServers: [],
|
|
2605
|
+
mcpFailed: [],
|
|
2336
2606
|
mcpSkipped: mcpConfig.skipped || [],
|
|
2337
|
-
tools:
|
|
2607
|
+
tools: ownerSession.toolRegistry?.size || 0,
|
|
2338
2608
|
},
|
|
2339
2609
|
};
|
|
2610
|
+
if (!isCurrentRuntimeOwner(owner)) {
|
|
2611
|
+
await disconnectRuntimeMcpManager(mcpManager);
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2340
2614
|
projectRuntimes.set(key, runtime);
|
|
2341
|
-
// Skill metadata is
|
|
2342
|
-
//
|
|
2343
|
-
activateProjectRuntime(runtime);
|
|
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 });
|
|
2344
2618
|
if (mcpConfig.servers.length > 0) {
|
|
2345
|
-
|
|
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
|
+
}
|
|
2346
2632
|
runtime.mcpStatus = mcpStatus;
|
|
2347
2633
|
runtime.status = {
|
|
2348
2634
|
...runtime.status,
|
|
2349
2635
|
mcpServers: mcpStatus.connected,
|
|
2350
2636
|
mcpFailed: mcpStatus.failed,
|
|
2351
2637
|
mcpSkipped: mcpConfig.skipped || [],
|
|
2352
|
-
tools:
|
|
2638
|
+
tools: ownerSession.toolRegistry?.size || 0,
|
|
2353
2639
|
};
|
|
2354
|
-
activateProjectRuntime(runtime);
|
|
2640
|
+
if (activeRuntimeKey === key) activateProjectRuntime(runtime, owner, { reloadSkills: false });
|
|
2355
2641
|
}
|
|
2356
|
-
return runtime;
|
|
2642
|
+
return runtimeBelongsToOwner(runtime, owner) && projectRuntimes.get(key) === runtime ? runtime : null;
|
|
2357
2643
|
}
|
|
2358
2644
|
|
|
2359
|
-
|
|
2360
2645
|
function scheduleProjectRuntimeLoad(workDir) {
|
|
2646
|
+
const owner = captureRuntimeOwner();
|
|
2361
2647
|
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
2362
|
-
if (!normalizedWorkDir || !
|
|
2648
|
+
if (!normalizedWorkDir || !owner) return null;
|
|
2363
2649
|
const key = projectRuntimeKey(normalizedWorkDir);
|
|
2364
|
-
|
|
2365
|
-
if (
|
|
2366
|
-
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)
|
|
2367
2656
|
.catch((err) => {
|
|
2368
2657
|
console.warn('[Yeaft] async project runtime load failed for %s: %s', normalizedWorkDir, err?.message || err);
|
|
2369
2658
|
return null;
|
|
2370
2659
|
})
|
|
2371
|
-
.finally(() => {
|
|
2660
|
+
.finally(() => {
|
|
2661
|
+
if (owner.generation === runtimeGeneration
|
|
2662
|
+
&& projectRuntimeLoadPromises.get(key) === promise) {
|
|
2663
|
+
projectRuntimeLoadPromises.delete(key);
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
runtimeLoaderOwners.set(promise, owner);
|
|
2372
2667
|
projectRuntimeLoadPromises.set(key, promise);
|
|
2373
2668
|
return promise;
|
|
2374
2669
|
}
|
|
2375
2670
|
|
|
2376
|
-
async function ensureProjectRuntimeForSessionMeta(sessionMeta) {
|
|
2377
|
-
const workDir = normalizeSessionWorkDir(sessionMeta?.workDir);
|
|
2378
|
-
if (!workDir) {
|
|
2379
|
-
activateBaseRuntime();
|
|
2380
|
-
return null;
|
|
2381
|
-
}
|
|
2382
|
-
try {
|
|
2383
|
-
return await loadProjectRuntime(workDir);
|
|
2384
|
-
} catch (err) {
|
|
2385
|
-
console.warn('[Yeaft] project runtime load failed for %s: %s', workDir, err?.message || err);
|
|
2386
|
-
activateBaseRuntime();
|
|
2387
|
-
return null;
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
2671
|
function getProjectRuntimeForTurn(sessionMeta) {
|
|
2672
|
+
const owner = captureRuntimeOwner();
|
|
2673
|
+
if (!owner) return null;
|
|
2392
2674
|
const workDir = normalizeSessionWorkDir(sessionMeta?.workDir);
|
|
2393
2675
|
if (!workDir) {
|
|
2394
|
-
activateBaseRuntime();
|
|
2676
|
+
activateBaseRuntime(owner);
|
|
2395
2677
|
return null;
|
|
2396
2678
|
}
|
|
2397
2679
|
const cached = projectRuntimes.get(projectRuntimeKey(workDir)) || null;
|
|
2398
|
-
if (cached) {
|
|
2399
|
-
activateProjectRuntime(cached);
|
|
2680
|
+
if (runtimeBelongsToOwner(cached, owner)) {
|
|
2681
|
+
activateProjectRuntime(cached, owner);
|
|
2400
2682
|
return cached;
|
|
2401
2683
|
}
|
|
2402
2684
|
scheduleProjectRuntimeLoad(workDir);
|
|
2403
2685
|
// Do not let a previous workDir's MCP tools leak into this turn while the
|
|
2404
2686
|
// requested project runtime is still loading in the background.
|
|
2405
|
-
activateBaseRuntime();
|
|
2687
|
+
activateBaseRuntime(owner);
|
|
2406
2688
|
return null;
|
|
2407
2689
|
}
|
|
2408
2690
|
|
|
2409
|
-
function mergedStatusForProjectRuntime(runtime) {
|
|
2410
|
-
if (!
|
|
2691
|
+
function mergedStatusForProjectRuntime(runtime, ownerSession = session) {
|
|
2692
|
+
if (!ownerSession?.status || !runtime?.status) return ownerSession?.status || { skills: 0, mcpServers: [], tools: 0 };
|
|
2411
2693
|
return {
|
|
2412
|
-
...
|
|
2413
|
-
skills: Math.max(Number(
|
|
2414
|
-
mcpServers: [...new Set([...(
|
|
2415
|
-
mcpFailed: [...(
|
|
2416
|
-
mcpSkipped: [...(
|
|
2417
|
-
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),
|
|
2418
2700
|
};
|
|
2419
2701
|
}
|
|
2420
2702
|
|
|
@@ -4261,13 +4543,14 @@ export async function ensureSessionLoaded(opts = {}) {
|
|
|
4261
4543
|
const bootConfigRevision = sessionConfigRefreshRevision;
|
|
4262
4544
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
4263
4545
|
const normalizedWorkDir = normalizeSessionWorkDir(opts?.workDir || opts?.sessionMeta?.workDir);
|
|
4264
|
-
session = await
|
|
4546
|
+
session = await loadRuntimeSession({
|
|
4265
4547
|
...(yeaftDir && { dir: yeaftDir }),
|
|
4266
4548
|
...(normalizedWorkDir && { workDir: normalizedWorkDir }),
|
|
4267
4549
|
skipMCP: true,
|
|
4268
4550
|
skipSkills: true,
|
|
4269
4551
|
serverMode: true,
|
|
4270
4552
|
});
|
|
4553
|
+
claimRuntimeOwnership(session);
|
|
4271
4554
|
|
|
4272
4555
|
installYeaftRuntimeBridge(session);
|
|
4273
4556
|
// A save may complete after loadSession read config.json but before this
|
|
@@ -4302,13 +4585,15 @@ export async function ensureSessionLoaded(opts = {}) {
|
|
|
4302
4585
|
|
|
4303
4586
|
ensureYeaftConversationId();
|
|
4304
4587
|
scheduleBaseRuntimeLoad();
|
|
4305
|
-
|
|
4588
|
+
let bootProjectRuntime = normalizedWorkDir ? projectRuntimes.get(projectRuntimeKey(normalizedWorkDir)) || null : null;
|
|
4306
4589
|
if (normalizedWorkDir && !bootProjectRuntime) {
|
|
4307
4590
|
scheduleProjectRuntimeLoad(normalizedWorkDir);
|
|
4591
|
+
bootProjectRuntime = projectRuntimes.get(projectRuntimeKey(normalizedWorkDir)) || null;
|
|
4308
4592
|
}
|
|
4309
4593
|
const bootStatus = mergedStatusForProjectRuntime(bootProjectRuntime);
|
|
4310
4594
|
hydrateYeaftStatusFromSession({ ...session, status: bootStatus }, { reason: 'session_ready', emitEvent: true });
|
|
4311
|
-
broadcastSkillSlashCommands(
|
|
4595
|
+
if (bootProjectRuntime) broadcastSkillSlashCommands({ skillManager: bootProjectRuntime.skillManager });
|
|
4596
|
+
else broadcastSkillSlashCommands(session);
|
|
4312
4597
|
|
|
4313
4598
|
// Per-group history is hydrated lazily on first `getOrCreateSessionHistory`
|
|
4314
4599
|
// — there's no global "all conversations" tape any more.
|
|
@@ -4345,6 +4630,7 @@ export async function ensureSessionLoaded(opts = {}) {
|
|
|
4345
4630
|
try {
|
|
4346
4631
|
return await sessionLoadPromise;
|
|
4347
4632
|
} catch (err) {
|
|
4633
|
+
await shutdownProjectRuntimes();
|
|
4348
4634
|
session = null;
|
|
4349
4635
|
throw err;
|
|
4350
4636
|
} finally {
|
|
@@ -6383,7 +6669,10 @@ export async function handleYeaftLoadMoreHistory(msg) {
|
|
|
6383
6669
|
* session, then re-initialises so the frontend gets fresh config.
|
|
6384
6670
|
*/
|
|
6385
6671
|
export async function resetYeaftSession() {
|
|
6386
|
-
await
|
|
6672
|
+
// Runtime ownership must be revoked before reset reaches its first await.
|
|
6673
|
+
const oldSession = session;
|
|
6674
|
+
const oldRuntimesShutdown = shutdownProjectRuntimes();
|
|
6675
|
+
await oldRuntimesShutdown;
|
|
6387
6676
|
if (currentAbortCtrl && !currentAbortCtrl.signal.aborted) {
|
|
6388
6677
|
try { currentAbortCtrl.abort(); } catch { /* ignore */ }
|
|
6389
6678
|
}
|
|
@@ -6392,9 +6681,9 @@ export async function resetYeaftSession() {
|
|
|
6392
6681
|
try { _vpUnsubscribe(); } catch { /* ignore */ }
|
|
6393
6682
|
_vpUnsubscribe = null;
|
|
6394
6683
|
}
|
|
6395
|
-
if (
|
|
6396
|
-
await
|
|
6397
|
-
session = null;
|
|
6684
|
+
if (oldSession) {
|
|
6685
|
+
await oldSession.shutdown();
|
|
6686
|
+
if (session === oldSession) session = null;
|
|
6398
6687
|
}
|
|
6399
6688
|
yeaftConversationId = null;
|
|
6400
6689
|
// Per-group histories live on sessionContexts entries — clearing the
|
|
@@ -6438,12 +6727,13 @@ export async function resetYeaftSession() {
|
|
|
6438
6727
|
|
|
6439
6728
|
try {
|
|
6440
6729
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
6441
|
-
session = await
|
|
6730
|
+
session = await loadRuntimeSession({
|
|
6442
6731
|
...(yeaftDir && { dir: yeaftDir }),
|
|
6443
6732
|
skipMCP: true,
|
|
6444
6733
|
skipSkills: true,
|
|
6445
6734
|
serverMode: true,
|
|
6446
6735
|
});
|
|
6736
|
+
claimRuntimeOwnership(session);
|
|
6447
6737
|
installYeaftRuntimeBridge(session);
|
|
6448
6738
|
|
|
6449
6739
|
yeaftConversationId = `yeaft-${Date.now()}`;
|
|
@@ -6530,7 +6820,7 @@ function mcpRuntimeSnapshot() {
|
|
|
6530
6820
|
* pick up the change.
|
|
6531
6821
|
*/
|
|
6532
6822
|
function hotSwapMcpTools() {
|
|
6533
|
-
return replaceSessionMcpTools(session?.mcpManager);
|
|
6823
|
+
return replaceSessionMcpTools(captureRuntimeOwner(), session?.mcpManager);
|
|
6534
6824
|
}
|
|
6535
6825
|
|
|
6536
6826
|
/**
|
|
@@ -6589,7 +6879,7 @@ export async function handleYeaftMcpAdd(msg = {}) {
|
|
|
6589
6879
|
}
|
|
6590
6880
|
}
|
|
6591
6881
|
|
|
6592
|
-
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
6882
|
+
const swap = replaceSessionMcpTools(captureRuntimeOwner(), session?.mcpManager);
|
|
6593
6883
|
|
|
6594
6884
|
sendToServer({
|
|
6595
6885
|
type: 'yeaft_mcp_add_result',
|
|
@@ -6626,7 +6916,7 @@ export async function handleYeaftMcpRemove(msg = {}) {
|
|
|
6626
6916
|
}
|
|
6627
6917
|
}
|
|
6628
6918
|
|
|
6629
|
-
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
6919
|
+
const swap = replaceSessionMcpTools(captureRuntimeOwner(), session?.mcpManager);
|
|
6630
6920
|
|
|
6631
6921
|
sendToServer({
|
|
6632
6922
|
type: 'yeaft_mcp_remove_result',
|
|
@@ -6684,7 +6974,7 @@ export async function handleYeaftMcpReload(msg = {}) {
|
|
|
6684
6974
|
console.warn('[Yeaft] MCP reload failed:', err?.message || err);
|
|
6685
6975
|
}
|
|
6686
6976
|
|
|
6687
|
-
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
6977
|
+
const swap = replaceSessionMcpTools(captureRuntimeOwner(), session?.mcpManager);
|
|
6688
6978
|
|
|
6689
6979
|
sendToServer({
|
|
6690
6980
|
type: 'yeaft_mcp_reload_result',
|
|
@@ -6707,8 +6997,56 @@ export const __testHooks = {
|
|
|
6707
6997
|
return runYeaftSessionSend(msg);
|
|
6708
6998
|
},
|
|
6709
6999
|
setSessionForTest(nextSession) {
|
|
6710
|
-
|
|
6711
|
-
|
|
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
|
+
};
|
|
6712
7050
|
},
|
|
6713
7051
|
ensureYeaftConversationIdForTest() {
|
|
6714
7052
|
return ensureYeaftConversationId();
|
|
@@ -6716,6 +7054,9 @@ export const __testHooks = {
|
|
|
6716
7054
|
preloadYeaftSkillSlashCommandsForTest() {
|
|
6717
7055
|
return broadcastSkillSlashCommands(session);
|
|
6718
7056
|
},
|
|
7057
|
+
reloadActiveSkillsForTest() {
|
|
7058
|
+
return reloadActiveSkills();
|
|
7059
|
+
},
|
|
6719
7060
|
loadAndBroadcastYeaftSkillSlashCommandsForTest() {
|
|
6720
7061
|
return loadAndBroadcastYeaftSkillSlashCommands();
|
|
6721
7062
|
},
|
|
@@ -6779,7 +7120,10 @@ export const __testHooks = {
|
|
|
6779
7120
|
},
|
|
6780
7121
|
seedProjectRuntime(workDir, runtime) {
|
|
6781
7122
|
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
7123
|
+
const owner = captureRuntimeOwner();
|
|
6782
7124
|
const seeded = {
|
|
7125
|
+
generation: owner?.generation ?? runtimeGeneration,
|
|
7126
|
+
ownerSession: owner?.ownerSession || session,
|
|
6783
7127
|
workDir: normalizedWorkDir,
|
|
6784
7128
|
skillManager: runtime?.skillManager || { list: () => [] },
|
|
6785
7129
|
mcpManager: runtime?.mcpManager || { listTools: () => [], disconnectAll: async () => {} },
|