@yeaft/webchat-agent 1.0.70 → 1.0.71
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 -8
- package/yeaft/web-bridge.js +214 -30
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -100,19 +100,27 @@ const RETRY_DEFAULTS = Object.freeze({
|
|
|
100
100
|
// Autocomplete prefers /yeaft-skills:<name>; /skill:<name> remains supported
|
|
101
101
|
// for older clients and typed history.
|
|
102
102
|
const EXPLICIT_SKILL_COMMAND_RE = /^\/(?:skill|yeaft-skills):([^\s]+)(\s+|$)/;
|
|
103
|
+
const BARE_SKILL_COMMAND_RE = /^\/([A-Za-z0-9][A-Za-z0-9_.-]*)(\s+|$)/;
|
|
103
104
|
|
|
104
|
-
function parseExplicitSkillCommand(prompt) {
|
|
105
|
+
function parseExplicitSkillCommand(prompt, skillManager) {
|
|
105
106
|
if (typeof prompt !== 'string') {
|
|
106
107
|
return { skillName: null, cleanedPrompt: prompt };
|
|
107
108
|
}
|
|
108
109
|
const match = prompt.match(EXPLICIT_SKILL_COMMAND_RE);
|
|
109
|
-
if (
|
|
110
|
-
return {
|
|
110
|
+
if (match) {
|
|
111
|
+
return {
|
|
112
|
+
skillName: match[1],
|
|
113
|
+
cleanedPrompt: prompt.slice(match[0].length),
|
|
114
|
+
};
|
|
111
115
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
const bareMatch = prompt.match(BARE_SKILL_COMMAND_RE);
|
|
117
|
+
if (bareMatch && typeof skillManager?.has === 'function' && skillManager.has(bareMatch[1])) {
|
|
118
|
+
return {
|
|
119
|
+
skillName: bareMatch[1],
|
|
120
|
+
cleanedPrompt: prompt.slice(bareMatch[0].length),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return { skillName: null, cleanedPrompt: prompt };
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
function stripLeadingSkillCommandFromPromptParts(promptParts) {
|
|
@@ -1641,7 +1649,7 @@ export class Engine {
|
|
|
1641
1649
|
// the merge, so an invalid caller value (e.g. 'ULTRA') does not shadow a
|
|
1642
1650
|
// valid prompt prefix.
|
|
1643
1651
|
const parsed = parseEffortPrefix(prompt);
|
|
1644
|
-
const parsedSkill = parseExplicitSkillCommand(parsed.cleanedPrompt);
|
|
1652
|
+
const parsedSkill = parseExplicitSkillCommand(parsed.cleanedPrompt, this.#skillManager);
|
|
1645
1653
|
const effectivePrompt = parsedSkill.cleanedPrompt;
|
|
1646
1654
|
const effectivePromptParts = parsedSkill.skillName
|
|
1647
1655
|
? stripLeadingSkillCommandFromPromptParts(promptParts)
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -78,6 +78,8 @@ import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
|
|
|
78
78
|
|
|
79
79
|
const LEGACY_SKILL_COMMAND_PREFIX = 'skill:';
|
|
80
80
|
const YEAFT_SKILL_COMMAND_PREFIX = 'yeaft-skills:';
|
|
81
|
+
const PROJECT_SKILL_TIERS = new Set(['project', 'project-claude', 'project-codex']);
|
|
82
|
+
const BASE_RUNTIME_KEY = '__base__';
|
|
81
83
|
|
|
82
84
|
/** @type {import('./session.js').Session | null} */
|
|
83
85
|
let session = null;
|
|
@@ -423,6 +425,9 @@ const vpThreads = new Map();
|
|
|
423
425
|
const routePromisesByMsgId = new Map();
|
|
424
426
|
/** @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 } }>} */
|
|
425
427
|
const projectRuntimes = new Map();
|
|
428
|
+
/** @type {Map<string, Promise<any>>} */
|
|
429
|
+
const baseRuntimeLoadPromises = new Map();
|
|
430
|
+
let activeRuntimeKey = BASE_RUNTIME_KEY;
|
|
426
431
|
|
|
427
432
|
function replaceSessionMcpTools(mcpManager) {
|
|
428
433
|
if (!session?.toolRegistry || typeof session.toolRegistry.replaceMcpTools !== 'function') {
|
|
@@ -438,6 +443,9 @@ function replaceSessionMcpTools(mcpManager) {
|
|
|
438
443
|
}
|
|
439
444
|
|
|
440
445
|
function retargetVpEngines({ skillManager, mcpManager }) {
|
|
446
|
+
try {
|
|
447
|
+
session?.engine?.setRuntimeManagers?.({ skillManager, mcpManager });
|
|
448
|
+
} catch { /* best-effort default-engine retarget */ }
|
|
441
449
|
for (const eng of vpEngines.values()) {
|
|
442
450
|
try {
|
|
443
451
|
eng.setRuntimeManagers?.({ skillManager, mcpManager });
|
|
@@ -446,17 +454,26 @@ function retargetVpEngines({ skillManager, mcpManager }) {
|
|
|
446
454
|
}
|
|
447
455
|
|
|
448
456
|
function activateBaseRuntime() {
|
|
457
|
+
activeRuntimeKey = BASE_RUNTIME_KEY;
|
|
449
458
|
const swap = replaceSessionMcpTools(session?.mcpManager);
|
|
450
459
|
retargetVpEngines({
|
|
451
460
|
skillManager: session?.skillManager || null,
|
|
452
461
|
mcpManager: session?.mcpManager || null,
|
|
453
462
|
});
|
|
463
|
+
const status = session?.status || null;
|
|
464
|
+
if (status) {
|
|
465
|
+
status.skills = session?.skillManager?.size || 0;
|
|
466
|
+
status.mcpServers = Array.isArray(status.mcpServers) ? status.mcpServers : [];
|
|
467
|
+
status.mcpFailed = Array.isArray(status.mcpFailed) ? status.mcpFailed : [];
|
|
468
|
+
status.tools = session?.toolRegistry?.size || status.tools || 0;
|
|
469
|
+
}
|
|
454
470
|
broadcastSkillSlashCommands(session);
|
|
455
471
|
return swap;
|
|
456
472
|
}
|
|
457
473
|
|
|
458
474
|
function activateProjectRuntime(runtime) {
|
|
459
475
|
if (!runtime) return activateBaseRuntime();
|
|
476
|
+
activeRuntimeKey = projectRuntimeKey(runtime.workDir);
|
|
460
477
|
const swap = replaceSessionMcpTools(runtime.mcpManager);
|
|
461
478
|
retargetVpEngines({
|
|
462
479
|
skillManager: runtime.skillManager,
|
|
@@ -473,6 +490,8 @@ function activateProjectRuntime(runtime) {
|
|
|
473
490
|
async function shutdownProjectRuntimes() {
|
|
474
491
|
const runtimes = Array.from(projectRuntimes.values());
|
|
475
492
|
projectRuntimes.clear();
|
|
493
|
+
projectRuntimeLoadPromises.clear();
|
|
494
|
+
baseRuntimeLoadPromises.clear();
|
|
476
495
|
await Promise.all(runtimes.map(async (runtime) => {
|
|
477
496
|
try { await runtime?.mcpManager?.disconnectAll?.(); } catch { /* best-effort shutdown */ }
|
|
478
497
|
}));
|
|
@@ -728,6 +747,9 @@ let yeaftConversationId = null;
|
|
|
728
747
|
* creates/replaces the virtual Yeaft conversation id so `/` autocomplete never
|
|
729
748
|
* falls back to built-ins while full Session metadata is still loading. */
|
|
730
749
|
let lastYeaftSlashCommandSnapshot = null;
|
|
750
|
+
let lastYeaftGeneratedSlashCommands = new Set();
|
|
751
|
+
/** @type {Map<string, Promise<any>>} */
|
|
752
|
+
const projectRuntimeLoadPromises = new Map();
|
|
731
753
|
|
|
732
754
|
/** task-334-followup-batch-b: stored unsubscribe fn from VP subscribe,
|
|
733
755
|
* called on session reset to prevent stale subscriber leaks. */
|
|
@@ -1859,35 +1881,42 @@ function sendSessionOutputFrame(data, { sessionId, chatId, vpId, turnId, threadI
|
|
|
1859
1881
|
});
|
|
1860
1882
|
}
|
|
1861
1883
|
|
|
1884
|
+
function isProjectSkill(skill) {
|
|
1885
|
+
return PROJECT_SKILL_TIERS.has(String(skill?.tier || '').trim());
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1862
1888
|
export function buildSkillSlashCommands(skillManager) {
|
|
1863
1889
|
if (!skillManager || typeof skillManager.list !== 'function') return { commands: [], descriptions: {} };
|
|
1864
1890
|
const commands = [];
|
|
1865
1891
|
const descriptions = {};
|
|
1866
1892
|
for (const skill of skillManager.list()) {
|
|
1867
1893
|
if (!skill?.name || typeof skill.name !== 'string') continue;
|
|
1868
|
-
const
|
|
1894
|
+
const description = skill.description || skill.trigger || 'Load Yeaft skill';
|
|
1895
|
+
const commandName = isProjectSkill(skill)
|
|
1896
|
+
? skill.name
|
|
1897
|
+
: `${YEAFT_SKILL_COMMAND_PREFIX}${skill.name}`;
|
|
1869
1898
|
commands.push(commandName);
|
|
1870
|
-
descriptions[commandName] =
|
|
1899
|
+
descriptions[commandName] = description;
|
|
1871
1900
|
|
|
1872
|
-
//
|
|
1873
|
-
// to the visible
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
descriptions[legacyCommandName] = descriptions[commandName];
|
|
1901
|
+
// Accepted aliases for typed history and compatibility. They are not added
|
|
1902
|
+
// to the visible list unless they are the tier-appropriate display command.
|
|
1903
|
+
descriptions[`${LEGACY_SKILL_COMMAND_PREFIX}${skill.name}`] = description;
|
|
1904
|
+
descriptions[`${YEAFT_SKILL_COMMAND_PREFIX}${skill.name}`] = description;
|
|
1877
1905
|
}
|
|
1878
1906
|
commands.sort((a, b) => a.localeCompare(b));
|
|
1879
1907
|
return { commands, descriptions };
|
|
1880
1908
|
}
|
|
1881
1909
|
|
|
1882
1910
|
export function buildMergedSkillSlashCommands(skillManagers = []) {
|
|
1883
|
-
const
|
|
1884
|
-
const descriptions = {};
|
|
1911
|
+
const byName = new Map();
|
|
1885
1912
|
for (const manager of skillManagers) {
|
|
1886
|
-
|
|
1887
|
-
for (const
|
|
1888
|
-
|
|
1913
|
+
if (!manager || typeof manager.list !== 'function') continue;
|
|
1914
|
+
for (const skill of manager.list()) {
|
|
1915
|
+
if (!skill?.name || typeof skill.name !== 'string') continue;
|
|
1916
|
+
byName.set(skill.name, skill);
|
|
1917
|
+
}
|
|
1889
1918
|
}
|
|
1890
|
-
return {
|
|
1919
|
+
return buildSkillSlashCommands({ list: () => [...byName.values()] });
|
|
1891
1920
|
}
|
|
1892
1921
|
|
|
1893
1922
|
function sendSkillSlashCommandsUpdate({ conversationId, slashCommands, slashCommandDescriptions }) {
|
|
@@ -1909,7 +1938,7 @@ function replayCachedSkillSlashCommandsToYeaftConversation() {
|
|
|
1909
1938
|
});
|
|
1910
1939
|
}
|
|
1911
1940
|
|
|
1912
|
-
|
|
1941
|
+
function loadAndBroadcastYeaftSkillSlashCommands() {
|
|
1913
1942
|
const yeaftDir = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
1914
1943
|
const roots = [process.cwd()];
|
|
1915
1944
|
const configuredWorkDir = typeof ctx.CONFIG?.workDir === 'string' ? ctx.CONFIG.workDir.trim() : '';
|
|
@@ -1923,11 +1952,21 @@ export function preloadYeaftSkillSlashCommands() {
|
|
|
1923
1952
|
};
|
|
1924
1953
|
}
|
|
1925
1954
|
|
|
1955
|
+
export function preloadYeaftSkillSlashCommands() {
|
|
1956
|
+
setTimeout(() => {
|
|
1957
|
+
try { loadAndBroadcastYeaftSkillSlashCommands(); }
|
|
1958
|
+
catch (err) { console.warn('[Yeaft] async skill slash preload failed:', err?.message || err); }
|
|
1959
|
+
}, 0);
|
|
1960
|
+
return { scheduled: true };
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1926
1963
|
function broadcastSkillSlashCommands(sessionLike, extraSkillManagers = []) {
|
|
1927
1964
|
const managers = [sessionLike?.skillManager, ...extraSkillManagers].filter(Boolean);
|
|
1928
1965
|
const { commands, descriptions } = buildMergedSkillSlashCommands(managers);
|
|
1929
1966
|
const isYeaftSkillCommand = (cmd) => typeof cmd === 'string'
|
|
1930
|
-
&& (
|
|
1967
|
+
&& (lastYeaftGeneratedSlashCommands.has(cmd)
|
|
1968
|
+
|| cmd.startsWith(LEGACY_SKILL_COMMAND_PREFIX)
|
|
1969
|
+
|| cmd.startsWith(YEAFT_SKILL_COMMAND_PREFIX));
|
|
1931
1970
|
const nonSkillCommands = (ctx.slashCommands || []).filter(cmd => !isYeaftSkillCommand(cmd));
|
|
1932
1971
|
const slashCommands = [...new Set([...nonSkillCommands, ...commands])];
|
|
1933
1972
|
const slashCommandDescriptions = Object.fromEntries(
|
|
@@ -1937,6 +1976,7 @@ function broadcastSkillSlashCommands(sessionLike, extraSkillManagers = []) {
|
|
|
1937
1976
|
Object.assign(slashCommandDescriptions, descriptions);
|
|
1938
1977
|
ctx.slashCommands = slashCommands;
|
|
1939
1978
|
ctx.slashCommandDescriptions = slashCommandDescriptions;
|
|
1979
|
+
lastYeaftGeneratedSlashCommands = new Set(commands);
|
|
1940
1980
|
lastYeaftSlashCommandSnapshot = { slashCommands, slashCommandDescriptions };
|
|
1941
1981
|
sendSkillSlashCommandsUpdate({
|
|
1942
1982
|
conversationId: yeaftConversationId || '__preload__',
|
|
@@ -1945,6 +1985,63 @@ function broadcastSkillSlashCommands(sessionLike, extraSkillManagers = []) {
|
|
|
1945
1985
|
});
|
|
1946
1986
|
}
|
|
1947
1987
|
|
|
1988
|
+
async function loadBaseRuntime() {
|
|
1989
|
+
if (!session) return null;
|
|
1990
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir || session.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
1991
|
+
const skillManager = createSkillManager(yeaftDir, process.cwd());
|
|
1992
|
+
session.skillManager = skillManager;
|
|
1993
|
+
|
|
1994
|
+
const mcpConfig = loadMCPConfig(yeaftDir, undefined, process.cwd());
|
|
1995
|
+
const mcpManager = new MCPManager();
|
|
1996
|
+
session.mcpManager = mcpManager;
|
|
1997
|
+
let mcpStatus = { connected: [], failed: [] };
|
|
1998
|
+
|
|
1999
|
+
if (session.status) {
|
|
2000
|
+
session.status.skills = skillManager.size;
|
|
2001
|
+
session.status.mcpServers = [];
|
|
2002
|
+
session.status.mcpFailed = [];
|
|
2003
|
+
session.status.mcpSkipped = mcpConfig.skipped || [];
|
|
2004
|
+
session.status.tools = session.toolRegistry?.size || session.status.tools || 0;
|
|
2005
|
+
}
|
|
2006
|
+
if (activeRuntimeKey === BASE_RUNTIME_KEY) {
|
|
2007
|
+
activateBaseRuntime();
|
|
2008
|
+
} else {
|
|
2009
|
+
broadcastSkillSlashCommands(session);
|
|
2010
|
+
}
|
|
2011
|
+
hydrateYeaftStatusFromSession(session, { reason: 'base_runtime_skills', emitEvent: true });
|
|
2012
|
+
|
|
2013
|
+
if (mcpConfig.servers.length > 0) {
|
|
2014
|
+
mcpStatus = await mcpManager.connectAll(mcpConfig.servers);
|
|
2015
|
+
session.mcpManager = mcpManager;
|
|
2016
|
+
if (session.status) {
|
|
2017
|
+
session.status.mcpServers = mcpStatus.connected;
|
|
2018
|
+
session.status.mcpFailed = mcpStatus.failed;
|
|
2019
|
+
session.status.mcpSkipped = mcpConfig.skipped || [];
|
|
2020
|
+
}
|
|
2021
|
+
if (activeRuntimeKey === BASE_RUNTIME_KEY) {
|
|
2022
|
+
activateBaseRuntime();
|
|
2023
|
+
}
|
|
2024
|
+
hydrateYeaftStatusFromSession(session, { reason: 'base_runtime_mcp', emitEvent: true });
|
|
2025
|
+
try { broadcastMcpUpdated({ reason: 'base-runtime-load' }); } catch { /* best-effort */ }
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
return { skillManager, mcpManager, mcpStatus, mcpConfig };
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function scheduleBaseRuntimeLoad() {
|
|
2032
|
+
if (!session) return null;
|
|
2033
|
+
if (baseRuntimeLoadPromises.has(BASE_RUNTIME_KEY)) return baseRuntimeLoadPromises.get(BASE_RUNTIME_KEY);
|
|
2034
|
+
const promise = new Promise(resolve => setTimeout(resolve, 0))
|
|
2035
|
+
.then(() => loadBaseRuntime())
|
|
2036
|
+
.catch((err) => {
|
|
2037
|
+
console.warn('[Yeaft] async base runtime load failed:', err?.message || err);
|
|
2038
|
+
return null;
|
|
2039
|
+
})
|
|
2040
|
+
.finally(() => { baseRuntimeLoadPromises.delete(BASE_RUNTIME_KEY); });
|
|
2041
|
+
baseRuntimeLoadPromises.set(BASE_RUNTIME_KEY, promise);
|
|
2042
|
+
return promise;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
1948
2045
|
async function loadProjectRuntime(workDir) {
|
|
1949
2046
|
if (!session) return null;
|
|
1950
2047
|
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
@@ -1967,9 +2064,6 @@ async function loadProjectRuntime(workDir) {
|
|
|
1967
2064
|
const mcpConfig = loadMCPConfig(yeaftDir, undefined, normalizedWorkDir);
|
|
1968
2065
|
const mcpManager = new MCPManager();
|
|
1969
2066
|
let mcpStatus = { connected: [], failed: [] };
|
|
1970
|
-
if (mcpConfig.servers.length > 0) {
|
|
1971
|
-
mcpStatus = await mcpManager.connectAll(mcpConfig.servers);
|
|
1972
|
-
}
|
|
1973
2067
|
const runtime = {
|
|
1974
2068
|
workDir: normalizedWorkDir,
|
|
1975
2069
|
skillManager,
|
|
@@ -1985,10 +2079,41 @@ async function loadProjectRuntime(workDir) {
|
|
|
1985
2079
|
},
|
|
1986
2080
|
};
|
|
1987
2081
|
projectRuntimes.set(key, runtime);
|
|
2082
|
+
// Skill metadata is available after the filesystem scan; publish it before
|
|
2083
|
+
// any MCP server startup so autocomplete does not wait on external processes.
|
|
1988
2084
|
activateProjectRuntime(runtime);
|
|
2085
|
+
if (mcpConfig.servers.length > 0) {
|
|
2086
|
+
mcpStatus = await mcpManager.connectAll(mcpConfig.servers);
|
|
2087
|
+
runtime.mcpStatus = mcpStatus;
|
|
2088
|
+
runtime.status = {
|
|
2089
|
+
...runtime.status,
|
|
2090
|
+
mcpServers: mcpStatus.connected,
|
|
2091
|
+
mcpFailed: mcpStatus.failed,
|
|
2092
|
+
mcpSkipped: mcpConfig.skipped || [],
|
|
2093
|
+
tools: session.toolRegistry?.size || 0,
|
|
2094
|
+
};
|
|
2095
|
+
activateProjectRuntime(runtime);
|
|
2096
|
+
}
|
|
1989
2097
|
return runtime;
|
|
1990
2098
|
}
|
|
1991
2099
|
|
|
2100
|
+
|
|
2101
|
+
function scheduleProjectRuntimeLoad(workDir) {
|
|
2102
|
+
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
2103
|
+
if (!normalizedWorkDir || !session) return null;
|
|
2104
|
+
const key = projectRuntimeKey(normalizedWorkDir);
|
|
2105
|
+
if (projectRuntimes.has(key)) return projectRuntimes.get(key);
|
|
2106
|
+
if (projectRuntimeLoadPromises.has(key)) return projectRuntimeLoadPromises.get(key);
|
|
2107
|
+
const promise = loadProjectRuntime(normalizedWorkDir)
|
|
2108
|
+
.catch((err) => {
|
|
2109
|
+
console.warn('[Yeaft] async project runtime load failed for %s: %s', normalizedWorkDir, err?.message || err);
|
|
2110
|
+
return null;
|
|
2111
|
+
})
|
|
2112
|
+
.finally(() => { projectRuntimeLoadPromises.delete(key); });
|
|
2113
|
+
projectRuntimeLoadPromises.set(key, promise);
|
|
2114
|
+
return promise;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
1992
2117
|
async function ensureProjectRuntimeForSessionMeta(sessionMeta) {
|
|
1993
2118
|
const workDir = normalizeSessionWorkDir(sessionMeta?.workDir);
|
|
1994
2119
|
if (!workDir) {
|
|
@@ -2004,6 +2129,24 @@ async function ensureProjectRuntimeForSessionMeta(sessionMeta) {
|
|
|
2004
2129
|
}
|
|
2005
2130
|
}
|
|
2006
2131
|
|
|
2132
|
+
function getProjectRuntimeForTurn(sessionMeta) {
|
|
2133
|
+
const workDir = normalizeSessionWorkDir(sessionMeta?.workDir);
|
|
2134
|
+
if (!workDir) {
|
|
2135
|
+
activateBaseRuntime();
|
|
2136
|
+
return null;
|
|
2137
|
+
}
|
|
2138
|
+
const cached = projectRuntimes.get(projectRuntimeKey(workDir)) || null;
|
|
2139
|
+
if (cached) {
|
|
2140
|
+
activateProjectRuntime(cached);
|
|
2141
|
+
return cached;
|
|
2142
|
+
}
|
|
2143
|
+
scheduleProjectRuntimeLoad(workDir);
|
|
2144
|
+
// Do not let a previous workDir's MCP tools leak into this turn while the
|
|
2145
|
+
// requested project runtime is still loading in the background.
|
|
2146
|
+
activateBaseRuntime();
|
|
2147
|
+
return null;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2007
2150
|
function mergedStatusForProjectRuntime(runtime) {
|
|
2008
2151
|
if (!session?.status || !runtime?.status) return session?.status || { skills: 0, mcpServers: [], tools: 0 };
|
|
2009
2152
|
return {
|
|
@@ -3274,7 +3417,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
3274
3417
|
* - No legacy "no-session" fallback paths — they were the source of the
|
|
3275
3418
|
* router_unavailable bug fixed in v0.1.671.
|
|
3276
3419
|
*/
|
|
3277
|
-
|
|
3420
|
+
async function runYeaftSessionSend(msg) {
|
|
3278
3421
|
if (!msg || typeof msg !== 'object') return;
|
|
3279
3422
|
const { text } = msg;
|
|
3280
3423
|
// PR #721: image-only send is allowed — text may be empty when the
|
|
@@ -3343,10 +3486,10 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
3343
3486
|
return;
|
|
3344
3487
|
}
|
|
3345
3488
|
|
|
3346
|
-
// Open the session metadata first so a workDir-backed Session can
|
|
3347
|
-
// project
|
|
3348
|
-
//
|
|
3349
|
-
//
|
|
3489
|
+
// Open the session metadata first so a workDir-backed Session can schedule
|
|
3490
|
+
// project runtime loading without blocking the user-message hot path. If
|
|
3491
|
+
// metadata is missing we still boot the agent runtime for a useful error
|
|
3492
|
+
// response, then fail on the session open check.
|
|
3350
3493
|
let sessionHandle = null;
|
|
3351
3494
|
let sessionRoot = null;
|
|
3352
3495
|
let sessionMetaForRuntime = null;
|
|
@@ -3374,7 +3517,7 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
3374
3517
|
|
|
3375
3518
|
const ensureSessionStart = perfNowMs();
|
|
3376
3519
|
await ensureSessionLoaded({ sessionMeta: sessionMetaForRuntime, perfTraceId });
|
|
3377
|
-
|
|
3520
|
+
scheduleProjectRuntimeLoad(sessionMetaForRuntime?.workDir);
|
|
3378
3521
|
traceDuration('session_send.ensure_session_loaded', ensureSessionStart);
|
|
3379
3522
|
|
|
3380
3523
|
|
|
@@ -3740,8 +3883,8 @@ async function ensureSessionLoaded(opts = {}) {
|
|
|
3740
3883
|
session = await loadSession({
|
|
3741
3884
|
...(yeaftDir && { dir: yeaftDir }),
|
|
3742
3885
|
...(normalizedWorkDir && { workDir: normalizedWorkDir }),
|
|
3743
|
-
skipMCP:
|
|
3744
|
-
skipSkills:
|
|
3886
|
+
skipMCP: true,
|
|
3887
|
+
skipSkills: true,
|
|
3745
3888
|
serverMode: true,
|
|
3746
3889
|
});
|
|
3747
3890
|
|
|
@@ -3772,7 +3915,11 @@ async function ensureSessionLoaded(opts = {}) {
|
|
|
3772
3915
|
}
|
|
3773
3916
|
|
|
3774
3917
|
ensureYeaftConversationId();
|
|
3775
|
-
|
|
3918
|
+
scheduleBaseRuntimeLoad();
|
|
3919
|
+
const bootProjectRuntime = normalizedWorkDir ? projectRuntimes.get(projectRuntimeKey(normalizedWorkDir)) || null : null;
|
|
3920
|
+
if (normalizedWorkDir && !bootProjectRuntime) {
|
|
3921
|
+
scheduleProjectRuntimeLoad(normalizedWorkDir);
|
|
3922
|
+
}
|
|
3776
3923
|
const bootStatus = mergedStatusForProjectRuntime(bootProjectRuntime);
|
|
3777
3924
|
hydrateYeaftStatusFromSession({ ...session, status: bootStatus }, { reason: 'session_ready', emitEvent: true });
|
|
3778
3925
|
broadcastSkillSlashCommands(session, bootProjectRuntime ? [bootProjectRuntime.skillManager] : []);
|
|
@@ -4068,7 +4215,9 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
4068
4215
|
envelope: inboundEnvelope,
|
|
4069
4216
|
threadId,
|
|
4070
4217
|
});
|
|
4071
|
-
|
|
4218
|
+
let turnSessionMeta = null;
|
|
4219
|
+
try { turnSessionMeta = sessionCoordinator?.group?.getMeta?.() || null; } catch { turnSessionMeta = null; }
|
|
4220
|
+
const projectRuntime = getProjectRuntimeForTurn(turnSessionMeta);
|
|
4072
4221
|
|
|
4073
4222
|
vpEngine = getOrCreateVpEngine(sessionId, vpId, threadId);
|
|
4074
4223
|
if (projectRuntime) {
|
|
@@ -5166,6 +5315,10 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
|
|
|
5166
5315
|
});
|
|
5167
5316
|
}
|
|
5168
5317
|
|
|
5318
|
+
export async function handleYeaftSessionSend(msg) {
|
|
5319
|
+
return runYeaftSessionSend(msg);
|
|
5320
|
+
}
|
|
5321
|
+
|
|
5169
5322
|
export function handleYeaftSubAgentPrompt(msg) {
|
|
5170
5323
|
const sessionId = typeof msg?.sessionId === 'string' ? msg.sessionId.trim() : '';
|
|
5171
5324
|
const taskId = typeof msg?.taskId === 'string' ? msg.taskId.trim() : '';
|
|
@@ -5694,13 +5847,14 @@ export async function resetYeaftSession() {
|
|
|
5694
5847
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
5695
5848
|
session = await loadSession({
|
|
5696
5849
|
...(yeaftDir && { dir: yeaftDir }),
|
|
5697
|
-
skipMCP:
|
|
5698
|
-
skipSkills:
|
|
5850
|
+
skipMCP: true,
|
|
5851
|
+
skipSkills: true,
|
|
5699
5852
|
serverMode: true,
|
|
5700
5853
|
});
|
|
5701
5854
|
installYeaftRuntimeBridge(session);
|
|
5702
5855
|
|
|
5703
5856
|
yeaftConversationId = `yeaft-${Date.now()}`;
|
|
5857
|
+
scheduleBaseRuntimeLoad();
|
|
5704
5858
|
hydrateYeaftStatusFromSession(session, { reason: 'reset', emitEvent: true });
|
|
5705
5859
|
broadcastSkillSlashCommands(session);
|
|
5706
5860
|
|
|
@@ -5955,6 +6109,9 @@ export const __testHooks = {
|
|
|
5955
6109
|
loadVisibleGroupHistoryPage,
|
|
5956
6110
|
persistInboundMessageOnceByMsgId,
|
|
5957
6111
|
buildPendingRescueEnvelope,
|
|
6112
|
+
runYeaftSessionSendForTest(msg) {
|
|
6113
|
+
return runYeaftSessionSend(msg);
|
|
6114
|
+
},
|
|
5958
6115
|
setSessionForTest(nextSession) {
|
|
5959
6116
|
session = nextSession || null;
|
|
5960
6117
|
sessionLoadPromise = null;
|
|
@@ -5965,6 +6122,9 @@ export const __testHooks = {
|
|
|
5965
6122
|
preloadYeaftSkillSlashCommandsForTest() {
|
|
5966
6123
|
return broadcastSkillSlashCommands(session);
|
|
5967
6124
|
},
|
|
6125
|
+
loadAndBroadcastYeaftSkillSlashCommandsForTest() {
|
|
6126
|
+
return loadAndBroadcastYeaftSkillSlashCommands();
|
|
6127
|
+
},
|
|
5968
6128
|
resetAbortState() {
|
|
5969
6129
|
turnAbortCtrls.clear();
|
|
5970
6130
|
turnAbortMeta.clear();
|
|
@@ -5982,6 +6142,30 @@ export const __testHooks = {
|
|
|
5982
6142
|
async loadProjectRuntime(workDir) {
|
|
5983
6143
|
return loadProjectRuntime(workDir);
|
|
5984
6144
|
},
|
|
6145
|
+
seedSessionContext(sessionId, meta) {
|
|
6146
|
+
const group = {
|
|
6147
|
+
getMeta() { return structuredClone(meta); },
|
|
6148
|
+
appendMessage(record) {
|
|
6149
|
+
return {
|
|
6150
|
+
...record,
|
|
6151
|
+
id: record?.id || `msg-${Date.now()}`,
|
|
6152
|
+
ts: record?.ts || new Date().toISOString(),
|
|
6153
|
+
role: record?.role || (record?.from === 'user' ? 'user' : 'assistant'),
|
|
6154
|
+
meta: record?.meta || {},
|
|
6155
|
+
};
|
|
6156
|
+
},
|
|
6157
|
+
};
|
|
6158
|
+
const coord = createCoordinator(group, {
|
|
6159
|
+
deliver: (vpId, envelope) => enqueueForVp(sessionId, vpId, envelope),
|
|
6160
|
+
});
|
|
6161
|
+
const entry = makeGroupContextStub();
|
|
6162
|
+
entry.coord = coord;
|
|
6163
|
+
entry.router = createRouter({ coordinator: coord });
|
|
6164
|
+
entry.sessionHandle = group;
|
|
6165
|
+
entry.historyHydrated = true;
|
|
6166
|
+
sessionContexts.set(sessionId, entry);
|
|
6167
|
+
return entry;
|
|
6168
|
+
},
|
|
5985
6169
|
seedProjectRuntime(workDir, runtime) {
|
|
5986
6170
|
const normalizedWorkDir = normalizeSessionWorkDir(workDir);
|
|
5987
6171
|
const seeded = {
|