@yeaft/webchat-agent 1.0.57 → 1.0.58
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/package.json +1 -1
- package/yeaft/engine.js +16 -0
- package/yeaft/session.js +3 -6
- package/yeaft/skills.js +13 -9
- package/yeaft/web-bridge.js +249 -42
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -703,6 +703,22 @@ export class Engine {
|
|
|
703
703
|
this.#config.language = lang;
|
|
704
704
|
}
|
|
705
705
|
|
|
706
|
+
/**
|
|
707
|
+
* Hot-swap runtime managers for a project-bound Session. The ToolRegistry is
|
|
708
|
+
* shared at the bridge/session layer; these references only decide which
|
|
709
|
+
* skills are injected and which MCP manager flattened tools call through.
|
|
710
|
+
*
|
|
711
|
+
* @param {{ skillManager?: import('./skills.js').SkillManager, mcpManager?: import('./mcp.js').MCPManager }} managers
|
|
712
|
+
*/
|
|
713
|
+
setRuntimeManagers(managers = {}) {
|
|
714
|
+
if (Object.prototype.hasOwnProperty.call(managers, 'skillManager')) {
|
|
715
|
+
this.#skillManager = managers.skillManager || null;
|
|
716
|
+
}
|
|
717
|
+
if (Object.prototype.hasOwnProperty.call(managers, 'mcpManager')) {
|
|
718
|
+
this.#mcpManager = managers.mcpManager || null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
706
722
|
/**
|
|
707
723
|
* Unregister a tool.
|
|
708
724
|
*
|
package/yeaft/session.js
CHANGED
|
@@ -363,12 +363,9 @@ export async function loadSession(options = {}) {
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
// ─── 6. Load skills ────────────────────────────────────
|
|
366
|
-
// Project tier root for skills + MCP project assets. Per-session
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
// process cwd, the common case when an agent is launched inside a project
|
|
370
|
-
// the user wants project-tier assets for. Shared so skills (.claude/skills,
|
|
371
|
-
// .yeaft/skills) and MCP (.mcp.json) resolve from the same root.
|
|
366
|
+
// Project tier root for skills + MCP project assets. Per-session workDir
|
|
367
|
+
// overlays are loaded by web-bridge once it knows the selected Session meta;
|
|
368
|
+
// the base runtime still uses the agent process cwd for global/default status.
|
|
372
369
|
const projectTierRoot = process.cwd();
|
|
373
370
|
|
|
374
371
|
let skillManager;
|
package/yeaft/skills.js
CHANGED
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
55
|
import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync, mkdirSync, statSync } from 'fs';
|
|
56
|
-
import { join, basename, sep, dirname, resolve } from 'path';
|
|
56
|
+
import { join, basename, sep, dirname, resolve, delimiter } from 'path';
|
|
57
57
|
import { platform, homedir } from 'os';
|
|
58
58
|
import { fileURLToPath } from 'url';
|
|
59
59
|
|
|
@@ -758,19 +758,23 @@ export function createSkillManager(yeaftDir, workDir) {
|
|
|
758
758
|
const claudeUserDir = home ? join(home, '.claude', 'skills') : null;
|
|
759
759
|
const codexUserDir = home ? join(home, '.codex', 'skills') : null;
|
|
760
760
|
const userDir = join(yeaftDir, 'skills');
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const
|
|
761
|
+
const projectRoots = [...new Set(String(workDir || '')
|
|
762
|
+
.split(delimiter)
|
|
763
|
+
.map(p => p.trim())
|
|
764
|
+
.filter(Boolean))];
|
|
765
|
+
const claudeProjectDirs = projectRoots.map(root => join(root, '.claude', 'skills'));
|
|
766
|
+
const codexProjectDirs = projectRoots.map(root => join(root, '.agents', 'skills'));
|
|
767
|
+
const projectDirs = projectRoots.map(root => join(root, '.yeaft', 'skills'));
|
|
768
|
+
|
|
769
|
+
const dirs = [bundled, claudeUserDir, codexUserDir, userDir, ...claudeProjectDirs, ...codexProjectDirs, ...projectDirs].filter(Boolean);
|
|
766
770
|
const tierByDir = {};
|
|
767
771
|
if (bundled) tierByDir[bundled] = 'bundled';
|
|
768
772
|
if (claudeUserDir) tierByDir[claudeUserDir] = 'user-claude';
|
|
769
773
|
if (codexUserDir) tierByDir[codexUserDir] = 'user-codex';
|
|
770
774
|
tierByDir[userDir] = 'user';
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
775
|
+
for (const dir of claudeProjectDirs) tierByDir[dir] = 'project-claude';
|
|
776
|
+
for (const dir of codexProjectDirs) tierByDir[dir] = 'project-codex';
|
|
777
|
+
for (const dir of projectDirs) tierByDir[dir] = 'project';
|
|
774
778
|
|
|
775
779
|
const ignorePathsByDir = {};
|
|
776
780
|
if (claudeUserDir && bundled && pathIsInside(bundled, claudeUserDir)) {
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* ranges removed.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { join } from 'node:path';
|
|
21
|
+
import { delimiter, join } from 'node:path';
|
|
22
22
|
import { COLLAB_TOOL_POLICY } from './tools/registry.js';
|
|
23
23
|
import { existsSync } from 'node:fs';
|
|
24
24
|
import { randomUUID } from 'node:crypto';
|
|
@@ -26,7 +26,9 @@ import { DEFAULT_YEAFT_DIR } from './init.js';
|
|
|
26
26
|
import { buildDreamOutputSnapshot } from './dream/output-snapshot.js';
|
|
27
27
|
import { Engine } from './engine.js';
|
|
28
28
|
import { loadSession } from './session.js';
|
|
29
|
-
import { loadConfig } from './config.js';
|
|
29
|
+
import { loadConfig, loadMCPConfig } from './config.js';
|
|
30
|
+
import { createSkillManager } from './skills.js';
|
|
31
|
+
import { MCPManager } from './mcp.js';
|
|
30
32
|
import { sendToServer } from '../connection/buffer.js';
|
|
31
33
|
import ctx from '../context.js';
|
|
32
34
|
import { hydrateYeaftStatusFromSession } from './status-cache.js';
|
|
@@ -168,20 +170,29 @@ async function sendDreamSnapshotForSession(sessionId, extra = {}) {
|
|
|
168
170
|
function scheduleYeaftLoadHistoryMetadataReplay(sessionId) {
|
|
169
171
|
const replaySession = session;
|
|
170
172
|
const replayConversationId = yeaftConversationId;
|
|
171
|
-
setTimeout(() => {
|
|
173
|
+
setTimeout(async () => {
|
|
172
174
|
try {
|
|
173
175
|
if (!replaySession) return;
|
|
174
176
|
refreshLiveSessionConfig();
|
|
175
|
-
|
|
177
|
+
let projectRuntime = null;
|
|
178
|
+
if (sessionId) {
|
|
179
|
+
try {
|
|
180
|
+
const groupYeaftDir = resolveSessionYeaftDir(ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR, sessionId);
|
|
181
|
+
const meta = loadSessionMeta(join(sessionsRoot(groupYeaftDir), sessionId));
|
|
182
|
+
projectRuntime = await ensureProjectRuntimeForSessionMeta(meta);
|
|
183
|
+
} catch { /* best-effort project metadata */ }
|
|
184
|
+
}
|
|
185
|
+
const status = mergedStatusForProjectRuntime(projectRuntime);
|
|
186
|
+
hydrateYeaftStatusFromSession({ ...replaySession, status }, { reason: 'history_load', emitEvent: true });
|
|
176
187
|
sendSessionEvent({
|
|
177
188
|
type: 'session_ready',
|
|
178
189
|
conversationId: replayConversationId,
|
|
179
190
|
model: replaySession.config.primaryModel || replaySession.config.model,
|
|
180
191
|
modelEffort: replaySession.config.modelEffort || null,
|
|
181
192
|
availableModels: replaySession.config.availableModels || [],
|
|
182
|
-
skills:
|
|
183
|
-
mcpServers:
|
|
184
|
-
tools:
|
|
193
|
+
skills: status.skills,
|
|
194
|
+
mcpServers: status.mcpServers,
|
|
195
|
+
tools: status.tools,
|
|
185
196
|
yeaftDir: ctx.CONFIG?.yeaftDir || null,
|
|
186
197
|
tasks: replaySession.taskManager ? replaySession.taskManager.listActiveTasks() : [],
|
|
187
198
|
});
|
|
@@ -385,6 +396,14 @@ function threadKey(sessionId, vpId, threadId) {
|
|
|
385
396
|
return `${sessionId}::${vpId}::${threadId || 'main'}`;
|
|
386
397
|
}
|
|
387
398
|
|
|
399
|
+
function normalizeSessionWorkDir(workDir) {
|
|
400
|
+
return typeof workDir === 'string' && workDir.trim() ? workDir.trim() : '';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function projectRuntimeKey(workDir) {
|
|
404
|
+
return normalizeSessionWorkDir(workDir) || '__agent_cwd__';
|
|
405
|
+
}
|
|
406
|
+
|
|
388
407
|
function createThreadId() {
|
|
389
408
|
return `thr_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
390
409
|
}
|
|
@@ -394,6 +413,62 @@ const RUNNING_THREAD_STATES = new Set(['queued', 'typing', 'thinking', 'streamin
|
|
|
394
413
|
const vpThreads = new Map();
|
|
395
414
|
/** @type {Map<string, Set<Promise<string|null>>>} */
|
|
396
415
|
const routePromisesByMsgId = new Map();
|
|
416
|
+
/** @type {Map<string, { workDir: string, skillManager: import('./skills.js').SkillManager, mcpManager: import('./mcp.js').MCPManager, mcpStatus: object, mcpConfig: object, status: { skills: number, mcpServers: string[], mcpFailed: object[], mcpSkipped: object[], tools: number } }>} */
|
|
417
|
+
const projectRuntimes = new Map();
|
|
418
|
+
|
|
419
|
+
function replaceSessionMcpTools(mcpManager) {
|
|
420
|
+
if (!session?.toolRegistry || typeof session.toolRegistry.replaceMcpTools !== 'function') {
|
|
421
|
+
return { removed: 0, added: 0, skipped: true };
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
const result = session.toolRegistry.replaceMcpTools(mcpManager, buildMcpFlattenedTools);
|
|
425
|
+
return { ...result, skipped: false };
|
|
426
|
+
} catch (err) {
|
|
427
|
+
console.warn('[Yeaft] hot-swap MCP tools failed:', err?.message || err);
|
|
428
|
+
return { removed: 0, added: 0, skipped: true, error: err?.message || String(err) };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function retargetVpEngines({ skillManager, mcpManager }) {
|
|
433
|
+
for (const eng of vpEngines.values()) {
|
|
434
|
+
try {
|
|
435
|
+
eng.setRuntimeManagers?.({ skillManager, mcpManager });
|
|
436
|
+
} catch { /* best-effort runtime retarget */ }
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function activateBaseRuntime() {
|
|
441
|
+
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
442
|
+
retargetVpEngines({
|
|
443
|
+
skillManager: session?.skillManager || null,
|
|
444
|
+
mcpManager: session?.mcpManager || null,
|
|
445
|
+
});
|
|
446
|
+
broadcastSkillSlashCommands(session);
|
|
447
|
+
return swap;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function activateProjectRuntime(runtime) {
|
|
451
|
+
if (!runtime) return activateBaseRuntime();
|
|
452
|
+
const swap = replaceSessionMcpTools(runtime.mcpManager);
|
|
453
|
+
retargetVpEngines({
|
|
454
|
+
skillManager: runtime.skillManager,
|
|
455
|
+
mcpManager: runtime.mcpManager,
|
|
456
|
+
});
|
|
457
|
+
runtime.status = {
|
|
458
|
+
...runtime.status,
|
|
459
|
+
tools: session?.toolRegistry?.size || runtime.status?.tools || 0,
|
|
460
|
+
};
|
|
461
|
+
broadcastSkillSlashCommands(session, [runtime.skillManager]);
|
|
462
|
+
return swap;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function shutdownProjectRuntimes() {
|
|
466
|
+
const runtimes = Array.from(projectRuntimes.values());
|
|
467
|
+
projectRuntimes.clear();
|
|
468
|
+
await Promise.all(runtimes.map(async (runtime) => {
|
|
469
|
+
try { await runtime?.mcpManager?.disconnectAll?.(); } catch { /* best-effort shutdown */ }
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
397
472
|
|
|
398
473
|
function getVpThreadMap(sessionId, vpId) {
|
|
399
474
|
const key = vpKey(sessionId, vpId);
|
|
@@ -575,7 +650,8 @@ function invalidateGroupContext(sessionId) {
|
|
|
575
650
|
// Engines are NOT torn down here on purpose. They hold subordinate
|
|
576
651
|
// state (AMS adjustments) that should survive a meta change and a
|
|
577
652
|
// closed sessionHandle — they don't reach the on-disk group meta
|
|
578
|
-
// directly.
|
|
653
|
+
// directly. Project runtime managers are hot-swapped before each turn.
|
|
654
|
+
// They *are* dropped on `resetYeaftSession`.
|
|
579
655
|
}
|
|
580
656
|
|
|
581
657
|
/**
|
|
@@ -1703,6 +1779,7 @@ export async function __testDrainVpDrivers() {
|
|
|
1703
1779
|
* a half-aborted controller writing to a now-cleared map.
|
|
1704
1780
|
*/
|
|
1705
1781
|
export async function __testResetVpState() {
|
|
1782
|
+
await shutdownProjectRuntimes();
|
|
1706
1783
|
for (const ctrl of vpAborts.values()) {
|
|
1707
1784
|
try { if (!ctrl.signal.aborted) ctrl.abort(); } catch { /* */ }
|
|
1708
1785
|
}
|
|
@@ -1777,23 +1854,110 @@ export function buildSkillSlashCommands(skillManager) {
|
|
|
1777
1854
|
return { commands, descriptions };
|
|
1778
1855
|
}
|
|
1779
1856
|
|
|
1780
|
-
function
|
|
1781
|
-
const
|
|
1782
|
-
const
|
|
1783
|
-
const
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1857
|
+
export function buildMergedSkillSlashCommands(skillManagers = []) {
|
|
1858
|
+
const commands = [];
|
|
1859
|
+
const descriptions = {};
|
|
1860
|
+
for (const manager of skillManagers) {
|
|
1861
|
+
const built = buildSkillSlashCommands(manager);
|
|
1862
|
+
for (const cmd of built.commands) commands.push(cmd);
|
|
1863
|
+
Object.assign(descriptions, built.descriptions);
|
|
1864
|
+
}
|
|
1865
|
+
return { commands: [...new Set(commands)].sort((a, b) => a.localeCompare(b)), descriptions };
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function broadcastSkillSlashCommands(sessionLike, extraSkillManagers = []) {
|
|
1869
|
+
const managers = [sessionLike?.skillManager, ...extraSkillManagers].filter(Boolean);
|
|
1870
|
+
const { commands, descriptions } = buildMergedSkillSlashCommands(managers);
|
|
1871
|
+
const nonSkillCommands = (ctx.slashCommands || [])
|
|
1872
|
+
.filter(cmd => !(typeof cmd === 'string' && cmd.startsWith(SKILL_COMMAND_PREFIX)));
|
|
1873
|
+
const slashCommands = [...new Set([...nonSkillCommands, ...commands])];
|
|
1874
|
+
const slashCommandDescriptions = Object.fromEntries(
|
|
1875
|
+
Object.entries(ctx.slashCommandDescriptions || {})
|
|
1876
|
+
.filter(([cmd]) => !(typeof cmd === 'string' && cmd.startsWith(SKILL_COMMAND_PREFIX)))
|
|
1877
|
+
);
|
|
1878
|
+
Object.assign(slashCommandDescriptions, descriptions);
|
|
1787
1879
|
ctx.slashCommands = slashCommands;
|
|
1788
1880
|
ctx.slashCommandDescriptions = slashCommandDescriptions;
|
|
1789
1881
|
sendToServer({
|
|
1790
1882
|
type: 'slash_commands_update',
|
|
1791
|
-
|
|
1883
|
+
agentId: ctx.AGENT_ID || ctx.agentId || null,
|
|
1884
|
+
conversationId: yeaftConversationId || '__preload__',
|
|
1792
1885
|
slashCommands,
|
|
1793
1886
|
slashCommandDescriptions,
|
|
1794
1887
|
});
|
|
1795
1888
|
}
|
|
1796
1889
|
|
|
1890
|
+
async function loadProjectRuntime(workDir) {
|
|
1891
|
+
if (!session) return null;
|
|
1892
|
+
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
1893
|
+
if (!normalizedWorkDir) {
|
|
1894
|
+
activateBaseRuntime();
|
|
1895
|
+
return null;
|
|
1896
|
+
}
|
|
1897
|
+
const key = projectRuntimeKey(normalizedWorkDir);
|
|
1898
|
+
const cached = projectRuntimes.get(key);
|
|
1899
|
+
if (cached) {
|
|
1900
|
+
activateProjectRuntime(cached);
|
|
1901
|
+
return cached;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir || session.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
1905
|
+
const skillRoots = normalizedWorkDir && normalizedWorkDir !== process.cwd()
|
|
1906
|
+
? `${process.cwd()}${delimiter}${normalizedWorkDir}`
|
|
1907
|
+
: normalizedWorkDir;
|
|
1908
|
+
const skillManager = createSkillManager(yeaftDir, skillRoots);
|
|
1909
|
+
const mcpConfig = loadMCPConfig(yeaftDir, undefined, normalizedWorkDir);
|
|
1910
|
+
const mcpManager = new MCPManager();
|
|
1911
|
+
let mcpStatus = { connected: [], failed: [] };
|
|
1912
|
+
if (mcpConfig.servers.length > 0) {
|
|
1913
|
+
mcpStatus = await mcpManager.connectAll(mcpConfig.servers);
|
|
1914
|
+
}
|
|
1915
|
+
const runtime = {
|
|
1916
|
+
workDir: normalizedWorkDir,
|
|
1917
|
+
skillManager,
|
|
1918
|
+
mcpManager,
|
|
1919
|
+
mcpStatus,
|
|
1920
|
+
mcpConfig,
|
|
1921
|
+
status: {
|
|
1922
|
+
skills: skillManager.size,
|
|
1923
|
+
mcpServers: mcpStatus.connected,
|
|
1924
|
+
mcpFailed: mcpStatus.failed,
|
|
1925
|
+
mcpSkipped: mcpConfig.skipped || [],
|
|
1926
|
+
tools: session.toolRegistry?.size || 0,
|
|
1927
|
+
},
|
|
1928
|
+
};
|
|
1929
|
+
projectRuntimes.set(key, runtime);
|
|
1930
|
+
activateProjectRuntime(runtime);
|
|
1931
|
+
return runtime;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
async function ensureProjectRuntimeForSessionMeta(sessionMeta) {
|
|
1935
|
+
const workDir = normalizeSessionWorkDir(sessionMeta?.workDir);
|
|
1936
|
+
if (!workDir) {
|
|
1937
|
+
activateBaseRuntime();
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
try {
|
|
1941
|
+
return await loadProjectRuntime(workDir);
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
console.warn('[Yeaft] project runtime load failed for %s: %s', workDir, err?.message || err);
|
|
1944
|
+
activateBaseRuntime();
|
|
1945
|
+
return null;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
function mergedStatusForProjectRuntime(runtime) {
|
|
1950
|
+
if (!session?.status || !runtime?.status) return session?.status || { skills: 0, mcpServers: [], tools: 0 };
|
|
1951
|
+
return {
|
|
1952
|
+
...session.status,
|
|
1953
|
+
skills: Math.max(Number(session.status.skills) || 0, Number(runtime.status.skills) || 0),
|
|
1954
|
+
mcpServers: [...new Set([...(session.status.mcpServers || []), ...(runtime.status.mcpServers || [])])],
|
|
1955
|
+
mcpFailed: [...(session.status.mcpFailed || []), ...(runtime.status.mcpFailed || [])],
|
|
1956
|
+
mcpSkipped: [...(session.status.mcpSkipped || []), ...(runtime.status.mcpSkipped || [])],
|
|
1957
|
+
tools: Math.max(Number(session.status.tools) || 0, Number(runtime.status.tools) || 0),
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1797
1961
|
/** Send a Yeaft Session metadata event over the legacy-compatible envelope. */
|
|
1798
1962
|
function sendSessionEvent(event, { sessionId, chatId, vpId, turnId, threadId, perfTraceId } = {}) {
|
|
1799
1963
|
sendToServer({
|
|
@@ -3121,15 +3285,13 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
3121
3285
|
return;
|
|
3122
3286
|
}
|
|
3123
3287
|
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
// Open the session. The default `grp_default` no longer self-seeds
|
|
3129
|
-
// here — session creation happens up-front via `handleYeaftCreateSession`,
|
|
3130
|
-
// so a missing dir surfaces a clear error rather than masking it.
|
|
3288
|
+
// Open the session metadata first so a workDir-backed Session can load its
|
|
3289
|
+
// project-tier skills and MCP before the first engine turn. The runtime boot
|
|
3290
|
+
// below uses the same workDir; if metadata is missing we still boot the agent
|
|
3291
|
+
// runtime for a useful error response, then fail on the session open check.
|
|
3131
3292
|
let sessionHandle = null;
|
|
3132
3293
|
let sessionRoot = null;
|
|
3294
|
+
let sessionMetaForRuntime = null;
|
|
3133
3295
|
const openSessionStart = perfNowMs();
|
|
3134
3296
|
try {
|
|
3135
3297
|
const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
|
|
@@ -3137,6 +3299,7 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
3137
3299
|
const dir = join(sessionRoot, sessionId);
|
|
3138
3300
|
if (existsSync(dir) && loadSessionMeta(dir)) {
|
|
3139
3301
|
sessionHandle = openSession(sessionRoot, sessionId);
|
|
3302
|
+
sessionMetaForRuntime = sessionHandle.getMeta();
|
|
3140
3303
|
} else {
|
|
3141
3304
|
// fix-yeaft-session-server-persistence: the `grp_default` on-the-
|
|
3142
3305
|
// fly seed used to manufacture a missing session here. That
|
|
@@ -3149,9 +3312,14 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
3149
3312
|
} catch (err) {
|
|
3150
3313
|
console.warn('[Yeaft] yeaft_session_chat: session open failed', err?.message || err);
|
|
3151
3314
|
}
|
|
3152
|
-
|
|
3153
3315
|
traceDuration('session_send.open_session', openSessionStart, { ok: !!sessionHandle });
|
|
3154
3316
|
|
|
3317
|
+
const ensureSessionStart = perfNowMs();
|
|
3318
|
+
await ensureSessionLoaded();
|
|
3319
|
+
await ensureProjectRuntimeForSessionMeta(sessionMetaForRuntime);
|
|
3320
|
+
traceDuration('session_send.ensure_session_loaded', ensureSessionStart);
|
|
3321
|
+
|
|
3322
|
+
|
|
3155
3323
|
if (!sessionHandle) {
|
|
3156
3324
|
sendSessionOutputFrame({
|
|
3157
3325
|
type: 'assistant',
|
|
@@ -3540,8 +3708,10 @@ async function ensureSessionLoaded() {
|
|
|
3540
3708
|
}
|
|
3541
3709
|
|
|
3542
3710
|
yeaftConversationId = `yeaft-${Date.now()}`;
|
|
3543
|
-
|
|
3544
|
-
|
|
3711
|
+
const bootProjectRuntime = workDir ? await ensureProjectRuntimeForSessionMeta({ workDir }) : null;
|
|
3712
|
+
const bootStatus = mergedStatusForProjectRuntime(bootProjectRuntime);
|
|
3713
|
+
hydrateYeaftStatusFromSession({ ...session, status: bootStatus }, { reason: 'session_ready', emitEvent: true });
|
|
3714
|
+
broadcastSkillSlashCommands(session, bootProjectRuntime ? [bootProjectRuntime.skillManager] : []);
|
|
3545
3715
|
|
|
3546
3716
|
// Per-group history is hydrated lazily on first `getOrCreateSessionHistory`
|
|
3547
3717
|
// — there's no global "all conversations" tape any more.
|
|
@@ -3552,9 +3722,9 @@ async function ensureSessionLoaded() {
|
|
|
3552
3722
|
model: session.config.primaryModel || session.config.model,
|
|
3553
3723
|
modelEffort: session.config.modelEffort || null,
|
|
3554
3724
|
availableModels: session.config.availableModels || [],
|
|
3555
|
-
skills:
|
|
3556
|
-
mcpServers:
|
|
3557
|
-
tools:
|
|
3725
|
+
skills: bootStatus.skills,
|
|
3726
|
+
mcpServers: bootStatus.mcpServers,
|
|
3727
|
+
tools: bootStatus.tools,
|
|
3558
3728
|
yeaftDir: ctx.CONFIG?.yeaftDir || null,
|
|
3559
3729
|
tasks: session.taskManager ? session.taskManager.listActiveTasks() : [],
|
|
3560
3730
|
});
|
|
@@ -3795,8 +3965,20 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
3795
3965
|
envelope: inboundEnvelope,
|
|
3796
3966
|
threadId,
|
|
3797
3967
|
});
|
|
3968
|
+
const projectRuntime = await ensureProjectRuntimeForSessionMeta(sessionCoordinator?.group?.getMeta?.());
|
|
3798
3969
|
|
|
3799
3970
|
vpEngine = getOrCreateVpEngine(sessionId, vpId, threadId);
|
|
3971
|
+
if (projectRuntime) {
|
|
3972
|
+
vpEngine.setRuntimeManagers?.({
|
|
3973
|
+
skillManager: projectRuntime.skillManager,
|
|
3974
|
+
mcpManager: projectRuntime.mcpManager,
|
|
3975
|
+
});
|
|
3976
|
+
} else {
|
|
3977
|
+
vpEngine.setRuntimeManagers?.({
|
|
3978
|
+
skillManager: session?.skillManager || null,
|
|
3979
|
+
mcpManager: session?.mcpManager || null,
|
|
3980
|
+
});
|
|
3981
|
+
}
|
|
3800
3982
|
if (thread) thread.engine = vpEngine;
|
|
3801
3983
|
|
|
3802
3984
|
const inboundInjectedBy = inboundEnvelope?.msg?.meta?.injectedBy;
|
|
@@ -5233,15 +5415,25 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
5233
5415
|
}
|
|
5234
5416
|
historyAlreadyReplayed = true;
|
|
5235
5417
|
|
|
5418
|
+
let sessionMetaForRuntime = null;
|
|
5419
|
+
if (sessionId) {
|
|
5420
|
+
try {
|
|
5421
|
+
const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
|
|
5422
|
+
const metaDir = join(sessionsRoot(groupYeaftDir), sessionId);
|
|
5423
|
+
sessionMetaForRuntime = loadSessionMeta(metaDir);
|
|
5424
|
+
} catch { /* best-effort metadata hint */ }
|
|
5425
|
+
}
|
|
5236
5426
|
const bootStart = perfNowMs();
|
|
5237
5427
|
session = await loadSession({
|
|
5238
5428
|
dir: yeaftDir,
|
|
5429
|
+
...(normalizeSessionWorkDir(sessionMetaForRuntime?.workDir) && { workDir: normalizeSessionWorkDir(sessionMetaForRuntime.workDir) }),
|
|
5239
5430
|
skipMCP: false,
|
|
5240
5431
|
skipSkills: false,
|
|
5241
5432
|
serverMode: true,
|
|
5242
5433
|
});
|
|
5243
5434
|
traceDuration('history.load_session_runtime', bootStart);
|
|
5244
5435
|
installYeaftRuntimeBridge(session);
|
|
5436
|
+
await ensureProjectRuntimeForSessionMeta(sessionMetaForRuntime);
|
|
5245
5437
|
|
|
5246
5438
|
// Per-group history hydrates lazily via getOrCreateSessionHistory.
|
|
5247
5439
|
// When the load-history call carries a sessionId, force-refresh THAT
|
|
@@ -5352,6 +5544,7 @@ export async function handleYeaftLoadMoreHistory(msg) {
|
|
|
5352
5544
|
* session, then re-initialises so the frontend gets fresh config.
|
|
5353
5545
|
*/
|
|
5354
5546
|
export async function resetYeaftSession() {
|
|
5547
|
+
await shutdownProjectRuntimes();
|
|
5355
5548
|
if (currentAbortCtrl && !currentAbortCtrl.signal.aborted) {
|
|
5356
5549
|
try { currentAbortCtrl.abort(); } catch { /* ignore */ }
|
|
5357
5550
|
}
|
|
@@ -5415,6 +5608,7 @@ export async function resetYeaftSession() {
|
|
|
5415
5608
|
|
|
5416
5609
|
yeaftConversationId = `yeaft-${Date.now()}`;
|
|
5417
5610
|
hydrateYeaftStatusFromSession(session, { reason: 'reset', emitEvent: true });
|
|
5611
|
+
broadcastSkillSlashCommands(session);
|
|
5418
5612
|
|
|
5419
5613
|
// Per-group history hydrates lazily via getOrCreateSessionHistory on
|
|
5420
5614
|
// first read. Nothing to seed here.
|
|
@@ -5495,16 +5689,7 @@ function mcpRuntimeSnapshot() {
|
|
|
5495
5689
|
* pick up the change.
|
|
5496
5690
|
*/
|
|
5497
5691
|
function hotSwapMcpTools() {
|
|
5498
|
-
|
|
5499
|
-
return { removed: 0, added: 0, skipped: true };
|
|
5500
|
-
}
|
|
5501
|
-
try {
|
|
5502
|
-
const result = session.toolRegistry.replaceMcpTools(session.mcpManager, buildMcpFlattenedTools);
|
|
5503
|
-
return { ...result, skipped: false };
|
|
5504
|
-
} catch (err) {
|
|
5505
|
-
console.warn('[Yeaft] hot-swap MCP tools failed:', err?.message || err);
|
|
5506
|
-
return { removed: 0, added: 0, skipped: true, error: err?.message || String(err) };
|
|
5507
|
-
}
|
|
5692
|
+
return replaceSessionMcpTools(session?.mcpManager);
|
|
5508
5693
|
}
|
|
5509
5694
|
|
|
5510
5695
|
/**
|
|
@@ -5563,7 +5748,7 @@ export async function handleYeaftMcpAdd(msg = {}) {
|
|
|
5563
5748
|
}
|
|
5564
5749
|
}
|
|
5565
5750
|
|
|
5566
|
-
const swap =
|
|
5751
|
+
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
5567
5752
|
|
|
5568
5753
|
sendToServer({
|
|
5569
5754
|
type: 'yeaft_mcp_add_result',
|
|
@@ -5600,7 +5785,7 @@ export async function handleYeaftMcpRemove(msg = {}) {
|
|
|
5600
5785
|
}
|
|
5601
5786
|
}
|
|
5602
5787
|
|
|
5603
|
-
const swap =
|
|
5788
|
+
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
5604
5789
|
|
|
5605
5790
|
sendToServer({
|
|
5606
5791
|
type: 'yeaft_mcp_remove_result',
|
|
@@ -5658,7 +5843,7 @@ export async function handleYeaftMcpReload(msg = {}) {
|
|
|
5658
5843
|
console.warn('[Yeaft] MCP reload failed:', err?.message || err);
|
|
5659
5844
|
}
|
|
5660
5845
|
|
|
5661
|
-
const swap =
|
|
5846
|
+
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
5662
5847
|
|
|
5663
5848
|
sendToServer({
|
|
5664
5849
|
type: 'yeaft_mcp_reload_result',
|
|
@@ -5692,6 +5877,28 @@ export const __testHooks = {
|
|
|
5692
5877
|
return getVpStatusBroker().transition(status);
|
|
5693
5878
|
},
|
|
5694
5879
|
decorateSessionsWithRuntimeState,
|
|
5880
|
+
async loadProjectRuntime(workDir) {
|
|
5881
|
+
return loadProjectRuntime(workDir);
|
|
5882
|
+
},
|
|
5883
|
+
seedProjectRuntime(workDir, runtime) {
|
|
5884
|
+
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
5885
|
+
const seeded = {
|
|
5886
|
+
workDir: normalizedWorkDir,
|
|
5887
|
+
skillManager: runtime?.skillManager || { list: () => [] },
|
|
5888
|
+
mcpManager: runtime?.mcpManager || { listTools: () => [], disconnectAll: async () => {} },
|
|
5889
|
+
mcpStatus: runtime?.mcpStatus || { connected: [], failed: [] },
|
|
5890
|
+
mcpConfig: runtime?.mcpConfig || { servers: [], skipped: [] },
|
|
5891
|
+
status: runtime?.status || { skills: 0, mcpServers: [], mcpFailed: [], mcpSkipped: [], tools: 0 },
|
|
5892
|
+
};
|
|
5893
|
+
projectRuntimes.set(projectRuntimeKey(normalizedWorkDir), seeded);
|
|
5894
|
+
return seeded;
|
|
5895
|
+
},
|
|
5896
|
+
async shutdownProjectRuntimes() {
|
|
5897
|
+
return shutdownProjectRuntimes();
|
|
5898
|
+
},
|
|
5899
|
+
projectRuntimeCount() {
|
|
5900
|
+
return projectRuntimes.size;
|
|
5901
|
+
},
|
|
5695
5902
|
seedQueuedVpTurn({ sessionId = 'session-test', vpId = 'vp-test', threadId = 'main', turnId = 'turn-test' } = {}) {
|
|
5696
5903
|
const key = threadKey(sessionId, vpId, threadId);
|
|
5697
5904
|
const inbox = vpInboxes.get(key) || [];
|