@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.70",
3
+ "version": "1.0.71",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
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 (!match) {
110
- return { skillName: null, cleanedPrompt: prompt };
110
+ if (match) {
111
+ return {
112
+ skillName: match[1],
113
+ cleanedPrompt: prompt.slice(match[0].length),
114
+ };
111
115
  }
112
- return {
113
- skillName: match[1],
114
- cleanedPrompt: prompt.slice(match[0].length),
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)
@@ -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 commandName = `${YEAFT_SKILL_COMMAND_PREFIX}${skill.name}`;
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] = skill.description || skill.trigger || 'Load Yeaft skill';
1899
+ descriptions[commandName] = description;
1871
1900
 
1872
- // Legacy alias for older typed commands and persisted drafts. Do not add it
1873
- // to the visible command list; autocomplete should show the same plugin-style
1874
- // names users see in Claude Code, e.g. /yeaft-skills:code-review.
1875
- const legacyCommandName = `${LEGACY_SKILL_COMMAND_PREFIX}${skill.name}`;
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 commands = [];
1884
- const descriptions = {};
1911
+ const byName = new Map();
1885
1912
  for (const manager of skillManagers) {
1886
- const built = buildSkillSlashCommands(manager);
1887
- for (const cmd of built.commands) commands.push(cmd);
1888
- Object.assign(descriptions, built.descriptions);
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 { commands: [...new Set(commands)].sort((a, b) => a.localeCompare(b)), descriptions };
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
- export function preloadYeaftSkillSlashCommands() {
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
- && (cmd.startsWith(LEGACY_SKILL_COMMAND_PREFIX) || cmd.startsWith(YEAFT_SKILL_COMMAND_PREFIX));
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
- export async function handleYeaftSessionSend(msg) {
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 load its
3347
- // project-tier skills and MCP before the first engine turn. The runtime boot
3348
- // below uses the same workDir; if metadata is missing we still boot the agent
3349
- // runtime for a useful error response, then fail on the session open check.
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
- await ensureProjectRuntimeForSessionMeta(sessionMetaForRuntime);
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: false,
3744
- skipSkills: false,
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
- const bootProjectRuntime = normalizedWorkDir ? await ensureProjectRuntimeForSessionMeta({ workDir: normalizedWorkDir }) : null;
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
- const projectRuntime = await ensureProjectRuntimeForSessionMeta(sessionCoordinator?.group?.getMeta?.());
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: false,
5698
- skipSkills: false,
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 = {