@yeaft/webchat-agent 1.0.58 → 1.0.59

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/yeaft/web-bridge.js +123 -84
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.58",
3
+ "version": "1.0.59",
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",
@@ -81,6 +81,13 @@ const SKILL_COMMAND_PREFIX = 'skill:';
81
81
  /** @type {import('./session.js').Session | null} */
82
82
  let session = null;
83
83
 
84
+ /**
85
+ * Single-flight runtime boot. History replay must not wait for this promise on
86
+ * cold load; message send still awaits it through ensureSessionLoaded().
87
+ * @type {Promise<import('./session.js').Session> | null}
88
+ */
89
+ let sessionLoadPromise = null;
90
+
84
91
  let threadClassifier = defaultClassifyThread;
85
92
 
86
93
  function applyLiveLanguage(language) {
@@ -1160,6 +1167,7 @@ export function __testGroupHistory(sessionId) {
1160
1167
  */
1161
1168
  export function __testSetSession(sessionLike) {
1162
1169
  session = sessionLike;
1170
+ sessionLoadPromise = null;
1163
1171
  if (!sessionLike) yeaftConversationId = null;
1164
1172
  }
1165
1173
 
@@ -3315,7 +3323,7 @@ export async function handleYeaftSessionSend(msg) {
3315
3323
  traceDuration('session_send.open_session', openSessionStart, { ok: !!sessionHandle });
3316
3324
 
3317
3325
  const ensureSessionStart = perfNowMs();
3318
- await ensureSessionLoaded();
3326
+ await ensureSessionLoaded({ sessionMeta: sessionMetaForRuntime, perfTraceId });
3319
3327
  await ensureProjectRuntimeForSessionMeta(sessionMetaForRuntime);
3320
3328
  traceDuration('session_send.ensure_session_loaded', ensureSessionStart);
3321
3329
 
@@ -3666,80 +3674,125 @@ export function buildVpQueryOpts({ vpId, sessionCoordinator, sessionId, envelope
3666
3674
  }
3667
3675
 
3668
3676
  /**
3669
- * Lazy session boot. Idempotent: subsequent calls are no-ops once `session`
3670
- * is set. Emits `session_ready` on first init so the frontend can finalize
3671
- * its handshake.
3677
+ * Lazy session boot. Idempotent: concurrent callers share one in-flight promise.
3678
+ * Emits `session_ready` on first init so the frontend can finalize its handshake.
3679
+ *
3680
+ * @param {{ workDir?: string, sessionId?: string|null, sessionMeta?: object, perfTraceId?: string|null, messageType?: string }} [opts]
3681
+ * @returns {Promise<import('./session.js').Session>}
3672
3682
  */
3673
- async function ensureSessionLoaded() {
3674
- if (session) return;
3683
+ async function ensureSessionLoaded(opts = {}) {
3684
+ if (session) return session;
3685
+ if (sessionLoadPromise) return sessionLoadPromise;
3675
3686
 
3676
- const yeaftDir = ctx.CONFIG?.yeaftDir;
3677
- session = await loadSession({
3678
- ...(yeaftDir && { dir: yeaftDir }),
3679
- skipMCP: false,
3680
- skipSkills: false,
3681
- serverMode: true,
3682
- });
3687
+ sessionLoadPromise = (async () => {
3688
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
3689
+ const normalizedWorkDir = normalizeSessionWorkDir(opts?.workDir || opts?.sessionMeta?.workDir);
3690
+ session = await loadSession({
3691
+ ...(yeaftDir && { dir: yeaftDir }),
3692
+ ...(normalizedWorkDir && { workDir: normalizedWorkDir }),
3693
+ skipMCP: false,
3694
+ skipSkills: false,
3695
+ serverMode: true,
3696
+ });
3683
3697
 
3684
- installYeaftRuntimeBridge(session);
3698
+ installYeaftRuntimeBridge(session);
3685
3699
 
3686
- try {
3687
- if (session.engine && typeof session.engine.setSubAgentEventSink === 'function') {
3688
- session.engine.setSubAgentEventSink((agentId, evt) => {
3689
- try {
3690
- sendSessionEvent({ type: 'sub_agent_event', agentId, payload: evt });
3691
- } catch { /* ignore */ }
3692
- });
3700
+ try {
3701
+ if (session.engine && typeof session.engine.setSubAgentEventSink === 'function') {
3702
+ session.engine.setSubAgentEventSink((agentId, evt) => {
3703
+ try {
3704
+ sendSessionEvent({ type: 'sub_agent_event', agentId, payload: evt });
3705
+ } catch { /* ignore */ }
3706
+ });
3707
+ }
3708
+ } catch (err) {
3709
+ console.warn('[Yeaft] setSubAgentEventSink wiring failed:', err?.message || err);
3693
3710
  }
3694
- } catch (err) {
3695
- console.warn('[Yeaft] setSubAgentEventSink wiring failed:', err?.message || err);
3696
- }
3697
3711
 
3698
- // Bug 8: clean up legacy `.archived-*` group dirs at boot.
3699
- try {
3700
- if (yeaftDir) {
3701
- const removed = purgeArchivedSessions(yeaftDir);
3702
- if (removed && removed.length > 0) {
3703
- console.log(`[Yeaft] purged ${removed.length} legacy .archived group dir(s)`);
3712
+ // Bug 8: clean up legacy `.archived-*` group dirs at boot.
3713
+ try {
3714
+ if (yeaftDir) {
3715
+ const removed = purgeArchivedSessions(yeaftDir);
3716
+ if (removed && removed.length > 0) {
3717
+ console.log(`[Yeaft] purged ${removed.length} legacy .archived group dir(s)`);
3718
+ }
3704
3719
  }
3720
+ } catch (err) {
3721
+ console.warn('[Yeaft] purgeArchivedSessions failed:', err?.message || err);
3705
3722
  }
3706
- } catch (err) {
3707
- console.warn('[Yeaft] purgeArchivedSessions failed:', err?.message || err);
3708
- }
3709
3723
 
3710
- yeaftConversationId = `yeaft-${Date.now()}`;
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] : []);
3724
+ ensureYeaftConversationId();
3725
+ const bootProjectRuntime = normalizedWorkDir ? await ensureProjectRuntimeForSessionMeta({ workDir: normalizedWorkDir }) : null;
3726
+ const bootStatus = mergedStatusForProjectRuntime(bootProjectRuntime);
3727
+ hydrateYeaftStatusFromSession({ ...session, status: bootStatus }, { reason: 'session_ready', emitEvent: true });
3728
+ broadcastSkillSlashCommands(session, bootProjectRuntime ? [bootProjectRuntime.skillManager] : []);
3715
3729
 
3716
- // Per-group history is hydrated lazily on first `getOrCreateSessionHistory`
3717
- // — there's no global "all conversations" tape any more.
3730
+ // Per-group history is hydrated lazily on first `getOrCreateSessionHistory`
3731
+ // — there's no global "all conversations" tape any more.
3732
+
3733
+ sendSessionEvent({
3734
+ type: 'session_ready',
3735
+ conversationId: yeaftConversationId,
3736
+ model: session.config.primaryModel || session.config.model,
3737
+ modelEffort: session.config.modelEffort || null,
3738
+ availableModels: session.config.availableModels || [],
3739
+ skills: bootStatus.skills,
3740
+ mcpServers: bootStatus.mcpServers,
3741
+ tools: bootStatus.tools,
3742
+ yeaftDir: ctx.CONFIG?.yeaftDir || null,
3743
+ tasks: session.taskManager ? session.taskManager.listActiveTasks() : [],
3744
+ }, opts?.perfTraceId ? { sessionId: opts?.sessionMeta?.id || opts?.sessionId || null, perfTraceId: opts.perfTraceId } : undefined);
3745
+ sendSessionSnapshotBroadcast();
3746
+ // vp-status: rebuild frontend status table from authoritative agent
3747
+ // memory. Sent unconditionally so reconnect/refresh paths get the same
3748
+ // bootstrap as first-load (the broker dedup logic makes a redundant
3749
+ // snapshot harmless).
3750
+ try {
3751
+ getVpStatusBroker().broadcastSnapshot();
3752
+ } catch (err) {
3753
+ console.warn('[Yeaft] vp-status snapshot broadcast failed:', err?.message || err);
3754
+ }
3755
+
3756
+ return session;
3757
+ })();
3718
3758
 
3719
- sendSessionEvent({
3720
- type: 'session_ready',
3721
- conversationId: yeaftConversationId,
3722
- model: session.config.primaryModel || session.config.model,
3723
- modelEffort: session.config.modelEffort || null,
3724
- availableModels: session.config.availableModels || [],
3725
- skills: bootStatus.skills,
3726
- mcpServers: bootStatus.mcpServers,
3727
- tools: bootStatus.tools,
3728
- yeaftDir: ctx.CONFIG?.yeaftDir || null,
3729
- tasks: session.taskManager ? session.taskManager.listActiveTasks() : [],
3730
- });
3731
- sendSessionSnapshotBroadcast();
3732
- // vp-status: rebuild frontend status table from authoritative agent
3733
- // memory. Sent unconditionally so reconnect/refresh paths get the same
3734
- // bootstrap as first-load (the broker dedup logic makes a redundant
3735
- // snapshot harmless).
3736
3759
  try {
3737
- getVpStatusBroker().broadcastSnapshot();
3760
+ return await sessionLoadPromise;
3738
3761
  } catch (err) {
3739
- console.warn('[Yeaft] vp-status snapshot broadcast failed:', err?.message || err);
3762
+ session = null;
3763
+ throw err;
3764
+ } finally {
3765
+ sessionLoadPromise = null;
3740
3766
  }
3741
3767
  }
3742
3768
 
3769
+ function startSessionLoadInBackground({ sessionId = null, sessionMeta = null, perfTraceId = null, traceDuration = null, tracePerf = null, phase = 'history.load_session_runtime' } = {}) {
3770
+ if (session) return null;
3771
+ const start = perfNowMs();
3772
+ const promise = ensureSessionLoaded({ sessionId, sessionMeta, perfTraceId })
3773
+ .then(async (loaded) => {
3774
+ if (typeof traceDuration === 'function') traceDuration(phase, start, { detail: { background: true } });
3775
+ if (sessionId && loaded?.conversationStore) {
3776
+ const hydrateStart = perfNowMs();
3777
+ setGroupHistory(sessionId, hydrateGroupHistory(sessionId));
3778
+ if (typeof traceDuration === 'function') traceDuration('history.hydrate_group_history', hydrateStart, { detail: { background: true } });
3779
+ sendDreamSnapshotForSession(sessionId, { trigger: 'load_history' }).catch(() => null);
3780
+ }
3781
+ return loaded;
3782
+ })
3783
+ .catch((err) => {
3784
+ if (typeof tracePerf === 'function') {
3785
+ tracePerf('history.load_session_runtime_error', {
3786
+ ok: false,
3787
+ detail: { background: true, errorName: err?.name || null, errorMessage: err?.message || String(err) },
3788
+ });
3789
+ }
3790
+ console.warn('[Yeaft] background session load failed:', err?.message || err);
3791
+ return null;
3792
+ });
3793
+ return promise;
3794
+ }
3795
+
3743
3796
  /**
3744
3797
  * Wrap {@link runVpTurn} with a hard escalation deadline.
3745
3798
  *
@@ -5423,28 +5476,11 @@ export async function handleYeaftLoadHistory(msg) {
5423
5476
  sessionMetaForRuntime = loadSessionMeta(metaDir);
5424
5477
  } catch { /* best-effort metadata hint */ }
5425
5478
  }
5426
- const bootStart = perfNowMs();
5427
- session = await loadSession({
5428
- dir: yeaftDir,
5429
- ...(normalizeSessionWorkDir(sessionMetaForRuntime?.workDir) && { workDir: normalizeSessionWorkDir(sessionMetaForRuntime.workDir) }),
5430
- skipMCP: false,
5431
- skipSkills: false,
5432
- serverMode: true,
5433
- });
5434
- traceDuration('history.load_session_runtime', bootStart);
5435
- installYeaftRuntimeBridge(session);
5436
- await ensureProjectRuntimeForSessionMeta(sessionMetaForRuntime);
5437
-
5438
- // Per-group history hydrates lazily via getOrCreateSessionHistory.
5439
- // When the load-history call carries a sessionId, force-refresh THAT
5440
- // group's tape so the next user message sees on-disk state. When
5441
- // it doesn't (legacy callers), do nothing — the per-group lazy
5442
- // hydration handles it.
5443
- if (sessionId) {
5444
- const hydrateStart = perfNowMs();
5445
- setGroupHistory(sessionId, hydrateGroupHistory(sessionId));
5446
- traceDuration('history.hydrate_group_history', hydrateStart);
5447
- }
5479
+ // Full runtime boot can be expensive (memory FTS sync, skills, MCP, dream
5480
+ // boot checks). It is not needed to render persisted history, so keep this
5481
+ // request short and let message-send await the same single-flight boot when
5482
+ // the user actually submits a turn.
5483
+ startSessionLoadInBackground({ sessionId, sessionMeta: sessionMetaForRuntime, perfTraceId, traceDuration, tracePerf });
5448
5484
  } else {
5449
5485
  const replayStart = perfNowMs();
5450
5486
  replayHistoryFromStore();
@@ -5452,10 +5488,12 @@ export async function handleYeaftLoadHistory(msg) {
5452
5488
  historyAlreadyReplayed = true;
5453
5489
  }
5454
5490
 
5455
- if (sessionId) {
5491
+ if (session && sessionId) {
5456
5492
  // Re-entering an existing session with a (possibly new) group filter:
5457
5493
  // re-seed THIS group's history from disk so it doesn't carry stale
5458
- // in-memory state into the next turn's context.
5494
+ // in-memory state into the next turn's context. Do not mark it hydrated
5495
+ // before the runtime exists; that would cache an empty tape and starve the
5496
+ // next user turn of persisted context.
5459
5497
  const hydrateStart = perfNowMs();
5460
5498
  setGroupHistory(sessionId, hydrateGroupHistory(sessionId));
5461
5499
  traceDuration('history.hydrate_group_history_final', hydrateStart);
@@ -5465,7 +5503,7 @@ export async function handleYeaftLoadHistory(msg) {
5465
5503
  // never make the history response wait for bulky metadata snapshots. The
5466
5504
  // first visible chunk has already been sent above; defer metadata to the next
5467
5505
  // tick so the browser can paint messages before VP/session/dream snapshots.
5468
- scheduleYeaftLoadHistoryMetadataReplay(sessionId);
5506
+ if (session) scheduleYeaftLoadHistoryMetadataReplay(sessionId);
5469
5507
 
5470
5508
  if (historyAlreadyReplayed) {
5471
5509
  traceDuration('history.handler_total', perfStart, { ok: true });
@@ -5863,6 +5901,7 @@ export const __testHooks = {
5863
5901
  buildPendingRescueEnvelope,
5864
5902
  setSessionForTest(nextSession) {
5865
5903
  session = nextSession || null;
5904
+ sessionLoadPromise = null;
5866
5905
  },
5867
5906
  resetAbortState() {
5868
5907
  turnAbortCtrls.clear();