@yeaft/webchat-agent 0.1.948 → 0.1.949

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.
@@ -2,9 +2,10 @@ import WebSocket from 'ws';
2
2
  import ctx from '../context.js';
3
3
  import { encrypt, decrypt, isEncrypted } from '../encryption.js';
4
4
 
5
- // 需要在断连期间缓冲的消息类型(Claude 输出相关的关键消息)
5
+ // 需要在断连期间缓冲的消息类型(CLI / Session 输出相关的关键消息)
6
6
  export const BUFFERABLE_TYPES = new Set([
7
- 'claude_output', 'turn_completed', 'conversation_closed',
7
+ 'claude_output', 'yeaft_output', 'yeaft_session_output', 'session_output',
8
+ 'turn_completed', 'conversation_closed',
8
9
  'session_id_update', 'compact_status', 'slash_commands_update',
9
10
  'background_task_started', 'background_task_output',
10
11
  'subagent_started', 'subagent_message', 'subagent_completed',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.948",
3
+ "version": "0.1.949",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -442,7 +442,7 @@ export function findCutIndex(messages, keepRecent) {
442
442
  /**
443
443
  * Wrap a summary string into the canonical "session continued" recovery
444
444
  * message. The wording is deliberately close to Claude Code's compact
445
- * marker so frontend filters (already in `web/stores/helpers/claudeOutput.js`,
445
+ * marker so frontend filters (already in `web/stores/helpers/assistantOutput.js`,
446
446
  * `server/db/message-db.js`) recognise it.
447
447
  *
448
448
  * @param {string} summary
@@ -12,7 +12,7 @@
12
12
  * • color / avatar → web-derived, NOT emitted here
13
13
  *
14
14
  * task-334h — live diff:
15
- * • subscribers (Set of sendYeaftEvent fns) receive per-vp `vp_updated`
15
+ * • subscribers (Set of sendSessionEvent fns) receive per-vp `vp_updated`
16
16
  * and `vp_removed` events as VpLoader's debounced rescan commits
17
17
  * added / updated / removed vpIds.
18
18
  * • Event shape: `{ type, vpId, vp?, reason? }` where `reason` ∈
@@ -232,19 +232,19 @@ export function buildVpSnapshot(registry = defaultRegistry) {
232
232
  /**
233
233
  * Handle an `yeaft_vp_subscribe` request from the web client.
234
234
  *
235
- * Registers `sendYeaftEvent` as a live-diff subscriber, emits an initial
235
+ * Registers `sendSessionEvent` as a live-diff subscriber, emits an initial
236
236
  * `vp_snapshot`, and (on first call process-wide) starts the VpLoader
237
237
  * whose debounced rescan fans out `vp_updated` / `vp_removed` events.
238
238
  *
239
239
  * Returns an unsubscribe fn the caller MAY invoke on WS close to prevent
240
240
  * sending to a dead socket. Web-bridge is expected to manage this.
241
241
  *
242
- * @param {(event: object) => void} sendYeaftEvent
242
+ * @param {(event: object) => void} sendSessionEvent
243
243
  * @param {import('./registry.js').Registry} [registry]
244
244
  * @param {{dir?: string}} [options]
245
245
  * @returns {() => void} unsubscribe fn
246
246
  */
247
- export function handleVpSubscribe(sendYeaftEvent, registry = defaultRegistry, options = {}) {
247
+ export function handleVpSubscribe(sendSessionEvent, registry = defaultRegistry, options = {}) {
248
248
  const { loader, fresh } = ensureLoader(registry, options);
249
249
  // task-338-F2 + task-339-followup: replay semantics.
250
250
  //
@@ -273,13 +273,13 @@ export function handleVpSubscribe(sendYeaftEvent, registry = defaultRegistry, op
273
273
  if (shouldRescan && loader && typeof loader.rescanNow === 'function') {
274
274
  try { loader.rescanNow(); } catch { /* never crash subscribe on rescan */ }
275
275
  }
276
- _subscribers.add(sendYeaftEvent);
276
+ _subscribers.add(sendSessionEvent);
277
277
  try {
278
- sendYeaftEvent(buildVpSnapshot(registry));
278
+ sendSessionEvent(buildVpSnapshot(registry));
279
279
  } catch {
280
280
  // Never crash the WS pipeline from snapshot serialisation.
281
281
  }
282
- return () => { _subscribers.delete(sendYeaftEvent); };
282
+ return () => { _subscribers.delete(sendSessionEvent); };
283
283
  }
284
284
 
285
285
  /**
@@ -6,8 +6,8 @@
6
6
  * inbox, abort, todo, persistence, and frontend timeline boundaries. Legacy
7
7
  * 1:1 chat paths still use the default `main` thread.
8
8
  *
9
- * Translates Engine events into claude_output-format messages so the
10
- * frontend can fully reuse the standard Chat rendering pipeline
9
+ * Translates Engine events into provider-neutral assistant output frames so
10
+ * the frontend can fully reuse the standard Chat rendering pipeline
11
11
  * (MessageList, AssistantTurn, ToolLine, AskCard, waiting cat, etc.).
12
12
  *
13
13
  * task-330c lint guard:
@@ -119,7 +119,7 @@ const inflightScopedDreamGroups = new Set();
119
119
  async function sendDreamSnapshotForSession(sessionId, extra = {}) {
120
120
  const snapshot = await buildDreamOutputSnapshot(session, sessionId);
121
121
  if (!snapshot) return null;
122
- sendYeaftEvent({ type: 'yeaft_dream_snapshot', ...extra, snapshot }, { sessionId });
122
+ sendSessionEvent({ type: 'yeaft_dream_snapshot', ...extra, snapshot }, { sessionId });
123
123
  return snapshot;
124
124
  }
125
125
 
@@ -157,7 +157,7 @@ const turnAbortMeta = new Map();
157
157
 
158
158
  /**
159
159
  * Per-VP status broker — the agent-side authority for VP timeline
160
- * status. Lazy-initialized on first use because `sendYeaftEvent` is
160
+ * status. Lazy-initialized on first use because `sendSessionEvent` is
161
161
  * declared below; trying to call it at module top-level would crash
162
162
  * with a TDZ error during agent boot.
163
163
  *
@@ -173,7 +173,7 @@ function getVpStatusBroker() {
173
173
  vpStatusBroker = createVpStatusBroker({
174
174
  send: (event) => {
175
175
  // The broker emits both `vp_status_changed` and
176
- // `vp_status_snapshot`. Both ride the standard sendYeaftEvent
176
+ // `vp_status_snapshot`. Both ride the standard sendSessionEvent
177
177
  // envelope so the frontend's existing yeaft_output dispatcher
178
178
  // sees them. We stamp sessionId/vpId on the envelope for
179
179
  // events that target a specific VP so the server's per-client
@@ -185,7 +185,7 @@ function getVpStatusBroker() {
185
185
  if (event.vpId) env.vpId = event.vpId;
186
186
  if (event.turnId) env.turnId = event.turnId;
187
187
  }
188
- sendYeaftEvent(event, env);
188
+ sendSessionEvent(event, env);
189
189
  },
190
190
  });
191
191
  }
@@ -1023,7 +1023,7 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
1023
1023
  });
1024
1024
  thread.updatedAt = Date.now();
1025
1025
  try {
1026
- sendYeaftEvent({
1026
+ sendSessionEvent({
1027
1027
  type: 'vp_thread_user_appended',
1028
1028
  sessionId,
1029
1029
  vpId,
@@ -1045,7 +1045,7 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
1045
1045
  inbox.push({ envelope, turnId, thread });
1046
1046
 
1047
1047
  try {
1048
- sendYeaftEvent({
1048
+ sendSessionEvent({
1049
1049
  type: 'vp_typing_start',
1050
1050
  sessionId,
1051
1051
  vpId,
@@ -1137,7 +1137,7 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
1137
1137
  turnAbortMeta.delete(turnId);
1138
1138
  if (vpAborts.get(key) === vpAbort) vpAborts.delete(key);
1139
1139
  try {
1140
- sendYeaftEvent({
1140
+ sendSessionEvent({
1141
1141
  type: 'vp_typing_end',
1142
1142
  sessionId,
1143
1143
  vpId,
@@ -1149,7 +1149,7 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
1149
1149
  }
1150
1150
  try {
1151
1151
  if (text && envelope?.msg) {
1152
- sendYeaftEvent({
1152
+ sendSessionEvent({
1153
1153
  type: 'session_message',
1154
1154
  sessionId,
1155
1155
  vpId,
@@ -1217,7 +1217,7 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
1217
1217
  console.warn('[Yeaft] vp-status typing transition (rescue) failed:', err?.message || err);
1218
1218
  }
1219
1219
  try {
1220
- sendYeaftEvent({
1220
+ sendSessionEvent({
1221
1221
  type: 'vp_typing_start',
1222
1222
  sessionId,
1223
1223
  vpId,
@@ -1285,9 +1285,11 @@ export async function __testResetVpState() {
1285
1285
 
1286
1286
 
1287
1287
  /**
1288
- * Send a yeaft_output message carrying claude_output-format data.
1289
- * Envelope fields: conversationId, sessionId, vpId, turnId, threadId let the
1290
- * frontend route incremental deltas to the correct per-VP/thread block.
1288
+ * Send a provider-neutral assistant output frame for a Yeaft Session.
1289
+ *
1290
+ * Wire compatibility: this still emits the legacy `yeaft_output` envelope so
1291
+ * upgraded agents continue to work with older servers. The frame in `data` is
1292
+ * intentionally generic and is consumed by the web's assistant-output handler.
1291
1293
  */
1292
1294
  function resolveGroupDefaultVpId(sessionId) {
1293
1295
  if (!sessionId) return null;
@@ -1300,7 +1302,7 @@ function resolveGroupDefaultVpId(sessionId) {
1300
1302
  }
1301
1303
  }
1302
1304
 
1303
- function sendYeaftOutput(data, { sessionId, chatId, vpId, turnId, threadId } = {}) {
1305
+ function sendSessionOutputFrame(data, { sessionId, chatId, vpId, turnId, threadId } = {}) {
1304
1306
  const resolvedVpId = vpId || (sessionId ? resolveGroupDefaultVpId(sessionId) : null);
1305
1307
  sendToServer({
1306
1308
  type: 'yeaft_output',
@@ -1314,8 +1316,8 @@ function sendYeaftOutput(data, { sessionId, chatId, vpId, turnId, threadId } = {
1314
1316
  });
1315
1317
  }
1316
1318
 
1317
- /** Send a yeaft_output event (non-claude_output metadata). */
1318
- function sendYeaftEvent(event, { sessionId, chatId, vpId, turnId, threadId } = {}) {
1319
+ /** Send a Yeaft Session metadata event over the legacy-compatible envelope. */
1320
+ function sendSessionEvent(event, { sessionId, chatId, vpId, turnId, threadId } = {}) {
1319
1321
  sendToServer({
1320
1322
  type: 'yeaft_output',
1321
1323
  conversationId: yeaftConversationId,
@@ -1345,7 +1347,7 @@ export function handleYeaftVpSubscribe(_msg) {
1345
1347
  }
1346
1348
  const { libDir } = configuredVpPaths();
1347
1349
  _vpUnsubscribe = handleVpSubscribe(
1348
- sendYeaftEvent,
1350
+ sendSessionEvent,
1349
1351
  undefined,
1350
1352
  libDir ? { dir: libDir } : {},
1351
1353
  );
@@ -1355,7 +1357,7 @@ export function handleYeaftVpSubscribe(_msg) {
1355
1357
  * VP CRUD from the web client. See historic doc for full message shapes.
1356
1358
  */
1357
1359
  function sendVpCrudResult(payload) {
1358
- sendYeaftEvent({ type: 'vp_crud_result', ...payload });
1360
+ sendSessionEvent({ type: 'vp_crud_result', ...payload });
1359
1361
  }
1360
1362
 
1361
1363
  export function handleYeaftVpCreate(msg) {
@@ -1462,7 +1464,7 @@ export function handleYeaftVpRead(msg) {
1462
1464
  * Session CRUD wired to WS events.
1463
1465
  */
1464
1466
  function sendSessionCrudResult(payload) {
1465
- sendYeaftEvent({ type: 'session_crud_result', ...payload });
1467
+ sendSessionEvent({ type: 'session_crud_result', ...payload });
1466
1468
  }
1467
1469
 
1468
1470
  function sendSessionSnapshotBroadcast() {
@@ -1470,7 +1472,7 @@ function sendSessionSnapshotBroadcast() {
1470
1472
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1471
1473
  if (!yeaftDir) return;
1472
1474
  const sessions = snapshotSessions(yeaftDir);
1473
- sendYeaftEvent({ type: 'session_list_updated', sessions });
1475
+ sendSessionEvent({ type: 'session_list_updated', sessions });
1474
1476
  } catch (err) {
1475
1477
  console.warn('[Yeaft] sendSessionSnapshotBroadcast failed:', err?.message || err);
1476
1478
  }
@@ -1502,7 +1504,7 @@ function sendSessionRosterChanged(session) {
1502
1504
  defaultVpId: session.defaultVpId,
1503
1505
  workDir: session.workDir || '',
1504
1506
  };
1505
- sendYeaftEvent({ type: 'session_roster_changed', ...payload });
1507
+ sendSessionEvent({ type: 'session_roster_changed', ...payload });
1506
1508
  }
1507
1509
 
1508
1510
  function sessionErrorPayload(err) {
@@ -1859,11 +1861,11 @@ export function installYeaftRuntimeBridge(s) {
1859
1861
  try {
1860
1862
  if (evt.type === 'turn_open' || evt.type === 'turn_close' || evt.type === 'loop') {
1861
1863
  const tag = evt && evt.sessionId ? { sessionId: evt.sessionId } : {};
1862
- sendYeaftEvent(evt, tag);
1864
+ sendSessionEvent(evt, tag);
1863
1865
  } else {
1864
1866
  const out = { type: 'dream_progress', ...evt };
1865
1867
  const tag = evt && evt.sessionId ? { sessionId: evt.sessionId } : {};
1866
- sendYeaftEvent(out, tag);
1868
+ sendSessionEvent(out, tag);
1867
1869
  }
1868
1870
  } catch { /* never let event delivery throw */ }
1869
1871
  };
@@ -1893,14 +1895,14 @@ export function installYeaftRuntimeBridge(s) {
1893
1895
  };
1894
1896
 
1895
1897
  // Wire the post-compact WS sink. Compactor is constructed in
1896
- // session.js with a no-op sink; bridge owns `sendYeaftEvent` /
1898
+ // session.js with a no-op sink; bridge owns `sendSessionEvent` /
1897
1899
  // `yeaftConversationId`, so the sink is wired here once a session is
1898
1900
  // present. Per-group `yeaft_history_compacted` events fire after a
1899
1901
  // successful summarize+swap.
1900
1902
  if (s.compactor && typeof s.compactor.setOnCompacted === 'function') {
1901
1903
  s.compactor.setOnCompacted((sessionId, result) => {
1902
1904
  try {
1903
- sendYeaftEvent({
1905
+ sendSessionEvent({
1904
1906
  type: 'yeaft_history_compacted',
1905
1907
  reason: result?.reason ?? null,
1906
1908
  beforeTurns: result?.beforeTurns,
@@ -1979,7 +1981,7 @@ function handleEngineEvent(event, hctx) {
1979
1981
  switch (event.type) {
1980
1982
  case 'text_delta':
1981
1983
  hctx.assistantTextParts.push(event.text);
1982
- sendYeaftOutput({
1984
+ sendSessionOutputFrame({
1983
1985
  type: 'assistant',
1984
1986
  message: { content: [{ type: 'text', text: event.text }] },
1985
1987
  }, envelope);
@@ -1990,7 +1992,7 @@ function handleEngineEvent(event, hctx) {
1990
1992
  break;
1991
1993
 
1992
1994
  case 'thinking_delta':
1993
- sendYeaftEvent({ type: 'thinking_delta', text: event.text }, envelope);
1995
+ sendSessionEvent({ type: 'thinking_delta', text: event.text }, envelope);
1994
1996
  break;
1995
1997
 
1996
1998
  case 'thinking_block_end':
@@ -1999,7 +2001,7 @@ function handleEngineEvent(event, hctx) {
1999
2001
  // turns / VPs includes it. Without this echo Anthropic 400s the
2000
2002
  // next request with "content[].thinking in the thinking mode must
2001
2003
  // be passed back to the API". The signature stays server-side
2002
- // only — wire serializers (stripMetaForWire / sendYeaftOutput)
2004
+ // only — wire serializers (stripMetaForWire / sendSessionOutputFrame)
2003
2005
  // never reference thinkingBlocks, so it cannot leak to the UI.
2004
2006
  if (hctx.thinkingBlocksAccum && event.signature) {
2005
2007
  if (event.redacted) {
@@ -2029,11 +2031,11 @@ function handleEngineEvent(event, hctx) {
2029
2031
  });
2030
2032
  }
2031
2033
  // Finish any in-progress text streaming so UI shows typing dots
2032
- sendYeaftOutput({
2034
+ sendSessionOutputFrame({
2033
2035
  type: 'assistant',
2034
2036
  message: { content: [] },
2035
2037
  }, envelope);
2036
- sendYeaftOutput({
2038
+ sendSessionOutputFrame({
2037
2039
  type: 'assistant',
2038
2040
  message: {
2039
2041
  content: [{
@@ -2048,7 +2050,7 @@ function handleEngineEvent(event, hctx) {
2048
2050
  break;
2049
2051
 
2050
2052
  case 'tool_start':
2051
- sendYeaftEvent({
2053
+ sendSessionEvent({
2052
2054
  type: 'tool_start',
2053
2055
  id: event.id,
2054
2056
  name: event.name,
@@ -2064,7 +2066,7 @@ function handleEngineEvent(event, hctx) {
2064
2066
  isError: !!event.isError,
2065
2067
  });
2066
2068
  }
2067
- sendYeaftOutput({
2069
+ sendSessionOutputFrame({
2068
2070
  type: 'user',
2069
2071
  tool_use_result: [{
2070
2072
  type: 'tool_result',
@@ -2109,7 +2111,7 @@ function handleEngineEvent(event, hctx) {
2109
2111
  } catch (err) {
2110
2112
  console.warn('[Yeaft] vp-status settleIdle (route_forward) failed:', err?.message || err);
2111
2113
  }
2112
- sendYeaftEvent({
2114
+ sendSessionEvent({
2113
2115
  type: 'vp_turn_end',
2114
2116
  sessionId: hctx.sessionId,
2115
2117
  vpId: hctx.vpId,
@@ -2125,7 +2127,7 @@ function handleEngineEvent(event, hctx) {
2125
2127
  break;
2126
2128
 
2127
2129
  case 'usage':
2128
- sendYeaftEvent({
2130
+ sendSessionEvent({
2129
2131
  type: 'context_usage',
2130
2132
  inputTokens: event.inputTokens,
2131
2133
  outputTokens: event.outputTokens,
@@ -2133,7 +2135,7 @@ function handleEngineEvent(event, hctx) {
2133
2135
  break;
2134
2136
 
2135
2137
  case 'recall':
2136
- sendYeaftEvent({
2138
+ sendSessionEvent({
2137
2139
  type: 'recall',
2138
2140
  entryCount: event.entryCount,
2139
2141
  cached: event.cached,
@@ -2144,7 +2146,7 @@ function handleEngineEvent(event, hctx) {
2144
2146
  // Engine compressed the context — clear THIS group's accumulated
2145
2147
  // history. Other groups' histories stay intact.
2146
2148
  if (hctx.sessionId) setGroupHistory(hctx.sessionId, []);
2147
- sendYeaftEvent({
2149
+ sendSessionEvent({
2148
2150
  type: 'consolidate',
2149
2151
  archivedCount: event.archivedCount,
2150
2152
  extractedCount: event.extractedCount,
@@ -2152,7 +2154,7 @@ function handleEngineEvent(event, hctx) {
2152
2154
  break;
2153
2155
 
2154
2156
  case 'fallback':
2155
- sendYeaftEvent({
2157
+ sendSessionEvent({
2156
2158
  type: 'fallback',
2157
2159
  from: event.from,
2158
2160
  to: event.to,
@@ -2161,7 +2163,7 @@ function handleEngineEvent(event, hctx) {
2161
2163
  break;
2162
2164
 
2163
2165
  case 'reflection':
2164
- sendYeaftEvent({
2166
+ sendSessionEvent({
2165
2167
  type: 'reflection',
2166
2168
  // feat-6af5f9f1 PR B: stamp turnId/loopNumber so the debug panel
2167
2169
  // can attach reflection cards to the matching loop.
@@ -2178,7 +2180,7 @@ function handleEngineEvent(event, hctx) {
2178
2180
  break;
2179
2181
 
2180
2182
  case 'turn_open':
2181
- sendYeaftEvent({
2183
+ sendSessionEvent({
2182
2184
  type: 'turn_open',
2183
2185
  turnId: event.turnId,
2184
2186
  userPrompt: event.userPrompt,
@@ -2189,7 +2191,7 @@ function handleEngineEvent(event, hctx) {
2189
2191
  break;
2190
2192
 
2191
2193
  case 'turn_close':
2192
- sendYeaftEvent({
2194
+ sendSessionEvent({
2193
2195
  type: 'turn_close',
2194
2196
  turnId: event.turnId,
2195
2197
  totalMs: event.totalMs,
@@ -2199,7 +2201,7 @@ function handleEngineEvent(event, hctx) {
2199
2201
  break;
2200
2202
 
2201
2203
  case 'memory_used':
2202
- sendYeaftEvent({
2204
+ sendSessionEvent({
2203
2205
  type: 'memory_used',
2204
2206
  turnId: event.turnId,
2205
2207
  loaded: event.loaded || [],
@@ -2207,7 +2209,7 @@ function handleEngineEvent(event, hctx) {
2207
2209
  break;
2208
2210
 
2209
2211
  case 'dream_memory_loaded':
2210
- sendYeaftEvent({
2212
+ sendSessionEvent({
2211
2213
  type: 'dream_memory_loaded',
2212
2214
  turnId: event.turnId,
2213
2215
  vpId: event.vpId || null,
@@ -2218,7 +2220,7 @@ function handleEngineEvent(event, hctx) {
2218
2220
  break;
2219
2221
 
2220
2222
  case 'memory_adjust':
2221
- sendYeaftEvent({
2223
+ sendSessionEvent({
2222
2224
  type: 'memory_adjust',
2223
2225
  turnId: event.turnId,
2224
2226
  sessionKey: event.sessionKey,
@@ -2233,7 +2235,7 @@ function handleEngineEvent(event, hctx) {
2233
2235
  if (hctx && Array.isArray(hctx.appendedUserPrompts) && event.preview) {
2234
2236
  hctx.appendedUserPrompts.push(String(event.preview));
2235
2237
  }
2236
- sendYeaftEvent({
2238
+ sendSessionEvent({
2237
2239
  type: 'vp_thread_user_append_consumed',
2238
2240
  turnId: event.turnId,
2239
2241
  threadId: event.threadId,
@@ -2244,7 +2246,7 @@ function handleEngineEvent(event, hctx) {
2244
2246
  break;
2245
2247
 
2246
2248
  case 'tool_exec':
2247
- sendYeaftEvent({
2249
+ sendSessionEvent({
2248
2250
  type: 'tool_exec',
2249
2251
  turnId: event.turnId,
2250
2252
  loopNumber: event.loopNumber,
@@ -2258,7 +2260,7 @@ function handleEngineEvent(event, hctx) {
2258
2260
  case 'loop':
2259
2261
  // feat-6af5f9f1 PR B: replaces the old `debug_turn` event. Same
2260
2262
  // payload shape plus turnId + loopNumber + usage.totalTokens.
2261
- sendYeaftEvent({
2263
+ sendSessionEvent({
2262
2264
  type: 'loop',
2263
2265
  turnId: event.turnId,
2264
2266
  loopNumber: event.loopNumber,
@@ -2281,7 +2283,7 @@ function handleEngineEvent(event, hctx) {
2281
2283
  if (isPermissionErrorMsg(errMsg)) {
2282
2284
  if (!_permissionDiagnosticSent) {
2283
2285
  _permissionDiagnosticSent = true;
2284
- sendYeaftOutput({
2286
+ sendSessionOutputFrame({
2285
2287
  type: 'assistant',
2286
2288
  message: {
2287
2289
  content: [{
@@ -2292,7 +2294,7 @@ function handleEngineEvent(event, hctx) {
2292
2294
  }, envelope);
2293
2295
  }
2294
2296
  } else {
2295
- sendYeaftOutput({
2297
+ sendSessionOutputFrame({
2296
2298
  type: 'assistant',
2297
2299
  message: {
2298
2300
  content: [{ type: 'text', text: `⚠️ Error: ${errMsg}` }],
@@ -2358,11 +2360,11 @@ export async function handleYeaftSessionSend(msg) {
2358
2360
  // session lying around.
2359
2361
  const yeaftDir = ctx.CONFIG?.yeaftDir;
2360
2362
  if (!yeaftDir) {
2361
- sendYeaftOutput({
2363
+ sendSessionOutputFrame({
2362
2364
  type: 'assistant',
2363
2365
  message: { content: [{ type: 'text', text: '⚠️ Yeaft session error: no yeaft directory configured.' }] },
2364
2366
  }, { sessionId });
2365
- sendYeaftOutput({ type: 'result', result_text: '' }, { sessionId });
2367
+ sendSessionOutputFrame({ type: 'result', result_text: '' }, { sessionId });
2366
2368
  return;
2367
2369
  }
2368
2370
 
@@ -2393,11 +2395,11 @@ export async function handleYeaftSessionSend(msg) {
2393
2395
  }
2394
2396
 
2395
2397
  if (!sessionHandle) {
2396
- sendYeaftOutput({
2398
+ sendSessionOutputFrame({
2397
2399
  type: 'assistant',
2398
2400
  message: { content: [{ type: 'text', text: `⚠️ Session ${sessionId} not found.` }] },
2399
2401
  }, { sessionId });
2400
- sendYeaftOutput({ type: 'result', result_text: '' }, { sessionId });
2402
+ sendSessionOutputFrame({ type: 'result', result_text: '' }, { sessionId });
2401
2403
  return;
2402
2404
  }
2403
2405
 
@@ -2477,7 +2479,7 @@ export async function handleYeaftSessionSend(msg) {
2477
2479
  const detail = attachmentBundle.failed
2478
2480
  .map((f) => ` - ${f.name}: ${f.error}`)
2479
2481
  .join('\n');
2480
- sendYeaftOutput({
2482
+ sendSessionOutputFrame({
2481
2483
  type: 'assistant',
2482
2484
  message: { content: [{ type: 'text', text: `⚠️ ${attachmentBundle.failed.length} file(s) could not be attached:\n${detail}` }] },
2483
2485
  }, { sessionId });
@@ -2507,11 +2509,11 @@ export async function handleYeaftSessionSend(msg) {
2507
2509
  });
2508
2510
  } catch (err) {
2509
2511
  console.warn('[Yeaft] yeaft_session_chat: coord.ingest failed', err?.message || err);
2510
- sendYeaftOutput({
2512
+ sendSessionOutputFrame({
2511
2513
  type: 'assistant',
2512
2514
  message: { content: [{ type: 'text', text: `⚠️ Session dispatch error: ${err?.message || err}` }] },
2513
2515
  }, { sessionId });
2514
- sendYeaftOutput({ type: 'result', result_text: '' }, { sessionId });
2516
+ sendSessionOutputFrame({ type: 'result', result_text: '' }, { sessionId });
2515
2517
  return;
2516
2518
  }
2517
2519
 
@@ -2528,11 +2530,11 @@ export async function handleYeaftSessionSend(msg) {
2528
2530
  // with a healthy roster. Surface the failure explicitly rather than
2529
2531
  // silently retrying as a single-VP turn (the legacy fallback masked
2530
2532
  // group-roster bugs).
2531
- sendYeaftOutput({
2533
+ sendSessionOutputFrame({
2532
2534
  type: 'assistant',
2533
2535
  message: { content: [{ type: 'text', text: '⚠️ No VP available to respond — check the group roster.' }] },
2534
2536
  }, { sessionId });
2535
- sendYeaftOutput({ type: 'result', result_text: '' }, { sessionId });
2537
+ sendSessionOutputFrame({ type: 'result', result_text: '' }, { sessionId });
2536
2538
  return;
2537
2539
  }
2538
2540
 
@@ -2727,7 +2729,7 @@ async function ensureSessionLoaded() {
2727
2729
  if (session.engine && typeof session.engine.setSubAgentEventSink === 'function') {
2728
2730
  session.engine.setSubAgentEventSink((agentId, evt) => {
2729
2731
  try {
2730
- sendYeaftEvent({ type: 'sub_agent_event', agentId, payload: evt });
2732
+ sendSessionEvent({ type: 'sub_agent_event', agentId, payload: evt });
2731
2733
  } catch { /* ignore */ }
2732
2734
  });
2733
2735
  }
@@ -2753,7 +2755,7 @@ async function ensureSessionLoaded() {
2753
2755
  // Per-group history is hydrated lazily on first `getOrCreateSessionHistory`
2754
2756
  // — there's no global "all conversations" tape any more.
2755
2757
 
2756
- sendYeaftEvent({
2758
+ sendSessionEvent({
2757
2759
  type: 'session_ready',
2758
2760
  conversationId: yeaftConversationId,
2759
2761
  model: session.config.model,
@@ -2812,7 +2814,7 @@ async function runVpTurnWithEscalation(args) {
2812
2814
  `[Yeaft] runVpTurn watchdog escalation: VP ${vpId} did not return ${deadlineMs}ms after enqueue — emitting synthetic stop and unblocking driver`,
2813
2815
  );
2814
2816
  try {
2815
- sendYeaftOutput(
2817
+ sendSessionOutputFrame(
2816
2818
  { type: 'result', result_text: '', stopped: true },
2817
2819
  { sessionId, vpId, turnId, threadId },
2818
2820
  );
@@ -2918,7 +2920,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
2918
2920
  if (turnEndEmitted) return;
2919
2921
  turnEndEmitted = true;
2920
2922
  try {
2921
- sendYeaftEvent({
2923
+ sendSessionEvent({
2922
2924
  type: 'vp_turn_end',
2923
2925
  sessionId,
2924
2926
  vpId,
@@ -2952,7 +2954,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
2952
2954
  resetQueryTimer();
2953
2955
 
2954
2956
  // Emit turn_start so frontend can create the message block.
2955
- sendYeaftEvent({ type: 'vp_turn_start', vpId, threadId, turnId, sessionId, title: thread?.title || '' }, envelope);
2957
+ sendSessionEvent({ type: 'vp_turn_start', vpId, threadId, turnId, sessionId, title: thread?.title || '' }, envelope);
2956
2958
  // vp-status: LLM call about to start, no text/tool yet → 'thinking'.
2957
2959
  try {
2958
2960
  getVpStatusBroker().transition({ sessionId, vpId, threadId, title: thread?.title || '', state: 'thinking', turnId, messageCount: thread?.messageIds?.length || 0 });
@@ -3034,11 +3036,11 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3034
3036
  // Turn completed — atomically append this VP's output to shared history.
3035
3037
  appendTurnToSessionHistory(sessionId, threadId, vpId, [prompt, ...appendedUserPrompts], assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum);
3036
3038
 
3037
- sendYeaftOutput({
3039
+ sendSessionOutputFrame({
3038
3040
  type: 'assistant',
3039
3041
  message: { content: [] },
3040
3042
  }, envelope);
3041
- sendYeaftOutput({
3043
+ sendSessionOutputFrame({
3042
3044
  type: 'result',
3043
3045
  result_text: '',
3044
3046
  }, envelope);
@@ -3052,7 +3054,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3052
3054
  } catch (err) {
3053
3055
  const isAbort = err && (err.name === 'AbortError' || err.name === 'LLMAbortError');
3054
3056
  if (isAbort) {
3055
- sendYeaftOutput({
3057
+ sendSessionOutputFrame({
3056
3058
  type: 'result',
3057
3059
  result_text: '',
3058
3060
  stopped: true,
@@ -3079,7 +3081,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3079
3081
  if (isPermissionErrorMsg(err.message)) {
3080
3082
  if (!_permissionDiagnosticSent) {
3081
3083
  _permissionDiagnosticSent = true;
3082
- sendYeaftOutput({
3084
+ sendSessionOutputFrame({
3083
3085
  type: 'assistant',
3084
3086
  message: {
3085
3087
  content: [{
@@ -3090,7 +3092,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3090
3092
  }, envelope);
3091
3093
  }
3092
3094
  } else {
3093
- sendYeaftOutput({
3095
+ sendSessionOutputFrame({
3094
3096
  type: 'assistant',
3095
3097
  message: {
3096
3098
  content: [{
@@ -3100,7 +3102,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3100
3102
  },
3101
3103
  }, envelope);
3102
3104
  }
3103
- sendYeaftOutput({
3105
+ sendSessionOutputFrame({
3104
3106
  type: 'result',
3105
3107
  result_text: '',
3106
3108
  }, envelope);
@@ -3374,7 +3376,7 @@ export function handleYeaftAbortThread(_msg = {}) {
3374
3376
  const thread = vpMap.get(targetThreadId);
3375
3377
  if (thread) thread.status = 'aborted';
3376
3378
  }
3377
- sendYeaftEvent({ type: 'yeaft_aborted', aborted, all: false, threadId: targetThreadId });
3379
+ sendSessionEvent({ type: 'yeaft_aborted', aborted, all: false, threadId: targetThreadId });
3378
3380
  return { aborted, all: false };
3379
3381
  }
3380
3382
 
@@ -3388,7 +3390,7 @@ export function handleYeaftAbortThread(_msg = {}) {
3388
3390
  turnAbortCtrls.clear();
3389
3391
  turnAbortMeta.clear();
3390
3392
  abortAllVpRuntime(aborted);
3391
- sendYeaftEvent({ type: 'yeaft_aborted', aborted, all: false });
3393
+ sendSessionEvent({ type: 'yeaft_aborted', aborted, all: false });
3392
3394
  return { aborted, all: false };
3393
3395
  }
3394
3396
 
@@ -3409,7 +3411,7 @@ export function handleYeaftAbortAll() {
3409
3411
  turnAbortCtrls.clear();
3410
3412
  turnAbortMeta.clear();
3411
3413
  abortAllVpRuntime(aborted);
3412
- sendYeaftEvent({ type: 'yeaft_aborted', aborted, all: true });
3414
+ sendSessionEvent({ type: 'yeaft_aborted', aborted, all: true });
3413
3415
  return { aborted, all: true };
3414
3416
  }
3415
3417
 
@@ -3421,7 +3423,7 @@ export function handleYeaftAbortAll() {
3421
3423
  export function handleYeaftAbortTurn(msg = {}) {
3422
3424
  const { turnId } = msg;
3423
3425
  if (!turnId) {
3424
- sendYeaftEvent({ type: 'yeaft_turn_aborted', turnId: null, success: false });
3426
+ sendSessionEvent({ type: 'yeaft_turn_aborted', turnId: null, success: false });
3425
3427
  return;
3426
3428
  }
3427
3429
  const ctrl = turnAbortCtrls.get(turnId);
@@ -3429,11 +3431,11 @@ export function handleYeaftAbortTurn(msg = {}) {
3429
3431
  try { ctrl.abort(); } catch { /* best-effort */ }
3430
3432
  turnAbortCtrls.delete(turnId);
3431
3433
  turnAbortMeta.delete(turnId);
3432
- sendYeaftEvent({ type: 'yeaft_turn_aborted', turnId, success: true });
3434
+ sendSessionEvent({ type: 'yeaft_turn_aborted', turnId, success: true });
3433
3435
  } else {
3434
3436
  turnAbortCtrls.delete(turnId);
3435
3437
  turnAbortMeta.delete(turnId);
3436
- sendYeaftEvent({ type: 'yeaft_turn_aborted', turnId, success: false });
3438
+ sendSessionEvent({ type: 'yeaft_turn_aborted', turnId, success: false });
3437
3439
  }
3438
3440
  }
3439
3441
 
@@ -3445,7 +3447,7 @@ export function abortYeaftSession(opts = {}) {
3445
3447
  if (opts && opts.all) return handleYeaftAbortAll();
3446
3448
  if (opts && opts.threadId) return handleYeaftAbortThread({ threadId: opts.threadId });
3447
3449
  // No payload — conservative no-op ack.
3448
- sendYeaftEvent({ type: 'yeaft_aborted', aborted: [], all: false });
3450
+ sendSessionEvent({ type: 'yeaft_aborted', aborted: [], all: false });
3449
3451
  return { aborted: [], all: false };
3450
3452
  }
3451
3453
 
@@ -3812,7 +3814,7 @@ export function handleYeaftModelSwitch(msg) {
3812
3814
 
3813
3815
  session.config.model = msg.model;
3814
3816
 
3815
- sendYeaftEvent({
3817
+ sendSessionEvent({
3816
3818
  type: 'model_switched',
3817
3819
  model: msg.model,
3818
3820
  });
@@ -3870,7 +3872,7 @@ export async function handleYeaftLoadHistory(msg) {
3870
3872
  }
3871
3873
 
3872
3874
  // Always replay session_ready so refresh / reconnect rebuilds UI state.
3873
- sendYeaftEvent({
3875
+ sendSessionEvent({
3874
3876
  type: 'session_ready',
3875
3877
  conversationId: yeaftConversationId,
3876
3878
  model: session.config.model,
@@ -3907,7 +3909,7 @@ export async function handleYeaftLoadHistory(msg) {
3907
3909
  const delta = session.conversationStore.loadAfterSeqByGroup(sessionId, afterSeq);
3908
3910
  for (const entry of delta.messages) {
3909
3911
  if (entry.role === 'user') {
3910
- sendYeaftOutput({
3912
+ sendSessionOutputFrame({
3911
3913
  type: 'user',
3912
3914
  message: {
3913
3915
  content: entry.content,
@@ -3923,15 +3925,15 @@ export async function handleYeaftLoadHistory(msg) {
3923
3925
  turnId: entry.turnId || entry.threadId || 'main',
3924
3926
  };
3925
3927
  if (entry.speakerVpId) envelopeOpts.vpId = entry.speakerVpId;
3926
- sendYeaftOutput({
3928
+ sendSessionOutputFrame({
3927
3929
  type: 'assistant',
3928
3930
  message: { id: entry.id || null, content: [{ type: 'text', text: entry.content }] },
3929
3931
  ts: entry.ts || null,
3930
3932
  }, envelopeOpts);
3931
- sendYeaftOutput({ type: 'result', result_text: '' }, envelopeOpts);
3933
+ sendSessionOutputFrame({ type: 'result', result_text: '' }, envelopeOpts);
3932
3934
  }
3933
3935
  }
3934
- sendYeaftEvent({
3936
+ sendSessionEvent({
3935
3937
  type: 'history_loaded',
3936
3938
  mode: 'delta',
3937
3939
  count: delta.messages.length,
@@ -3962,7 +3964,7 @@ export async function handleYeaftLoadHistory(msg) {
3962
3964
 
3963
3965
  for (const entry of replayEntries) {
3964
3966
  if (entry.role === 'user') {
3965
- sendYeaftOutput({
3967
+ sendSessionOutputFrame({
3966
3968
  type: 'user',
3967
3969
  message: {
3968
3970
  content: entry.content,
@@ -3982,12 +3984,12 @@ export async function handleYeaftLoadHistory(msg) {
3982
3984
  turnId: entry.turnId || entry.threadId || 'main',
3983
3985
  };
3984
3986
  if (entry.speakerVpId) envelopeOpts.vpId = entry.speakerVpId;
3985
- sendYeaftOutput({
3987
+ sendSessionOutputFrame({
3986
3988
  type: 'assistant',
3987
3989
  message: { id: entry.id || null, content: [{ type: 'text', text: entry.content }] },
3988
3990
  ts: entry.ts || null,
3989
3991
  }, envelopeOpts);
3990
- sendYeaftOutput({ type: 'result', result_text: '' }, envelopeOpts);
3992
+ sendSessionOutputFrame({ type: 'result', result_text: '' }, envelopeOpts);
3991
3993
  }
3992
3994
  }
3993
3995
 
@@ -4021,7 +4023,7 @@ export async function handleYeaftLoadHistory(msg) {
4021
4023
  if (last && last.id) latestSeq = session.conversationStore.getMessageSeqById(last.id);
4022
4024
  }
4023
4025
 
4024
- sendYeaftEvent({
4026
+ sendSessionEvent({
4025
4027
  type: 'history_loaded',
4026
4028
  mode: 'recent',
4027
4029
  count: replayEntries.length,
@@ -4168,7 +4170,7 @@ export async function resetYeaftSession() {
4168
4170
  // Per-group history hydrates lazily via getOrCreateSessionHistory on
4169
4171
  // first read. Nothing to seed here.
4170
4172
 
4171
- sendYeaftEvent({
4173
+ sendSessionEvent({
4172
4174
  type: 'session_ready',
4173
4175
  conversationId: yeaftConversationId,
4174
4176
  model: session.config.model,