@yeaft/webchat-agent 0.1.874 → 0.1.875

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 (46) hide show
  1. package/connection/message-router.js +20 -13
  2. package/package.json +1 -1
  3. package/yeaft/attachments.js +2 -2
  4. package/yeaft/cli.js +13 -13
  5. package/yeaft/compact/compactor.js +20 -20
  6. package/yeaft/conversation/persist.js +95 -95
  7. package/yeaft/debug-trace.js +12 -12
  8. package/yeaft/dream-v2/apply.js +15 -15
  9. package/yeaft/dream-v2/merge.js +12 -12
  10. package/yeaft/dream-v2/prompts/{extract-group.md → extract-session.md} +12 -12
  11. package/yeaft/dream-v2/prompts/index.js +3 -3
  12. package/yeaft/dream-v2/prompts/triage-pass1.md +1 -1
  13. package/yeaft/dream-v2/runner.js +37 -37
  14. package/yeaft/dream-v2/segment.js +3 -3
  15. package/yeaft/dream-v2/session-wiring.js +22 -22
  16. package/yeaft/dream-v2/state.js +7 -7
  17. package/yeaft/dream-v2/triage.js +22 -22
  18. package/yeaft/engine.js +65 -65
  19. package/yeaft/memory/ams-registry.js +22 -22
  20. package/yeaft/memory/seed-backfill.js +9 -9
  21. package/yeaft/memory/store-v2.js +27 -27
  22. package/yeaft/prompts.js +9 -9
  23. package/yeaft/routing/loop-guard.js +14 -14
  24. package/yeaft/routing/router.js +5 -5
  25. package/yeaft/session.js +5 -5
  26. package/yeaft/sessions/coordinator.js +96 -20
  27. package/yeaft/{groups → sessions}/ids.js +3 -3
  28. package/yeaft/{groups → sessions}/index.js +28 -28
  29. package/yeaft/sessions/pre-flow.js +178 -42
  30. package/yeaft/{groups → sessions}/seed-default.js +19 -19
  31. package/yeaft/{groups/group-config.js → sessions/session-config.js} +29 -29
  32. package/yeaft/{groups/group-crud.js → sessions/session-crud.js} +113 -113
  33. package/yeaft/sessions/session-store.js +85 -154
  34. package/yeaft/stop-hooks.js +4 -4
  35. package/yeaft/tools/todo-write.js +1 -1
  36. package/yeaft/tools/types.js +2 -2
  37. package/yeaft/vp/registry.js +1 -1
  38. package/yeaft/vp/vp-crud.js +1 -1
  39. package/yeaft/vp-status-broker.js +28 -28
  40. package/yeaft/web-bridge.js +411 -398
  41. package/yeaft/groups/coordinator.js +0 -221
  42. package/yeaft/groups/group-store.js +0 -212
  43. package/yeaft/groups/pre-flow.js +0 -329
  44. /package/yeaft/{groups → sessions}/feature-flag.js +0 -0
  45. /package/yeaft/{groups → sessions}/project-doc.js +0 -0
  46. /package/yeaft/{groups → sessions}/roster.js +0 -0
@@ -2,7 +2,7 @@
2
2
  * web-bridge.js — Bridge between web UI and Yeaft Yeaft Engine.
3
3
  *
4
4
  * PR #797: group VP runtime is threaded again. Each group VP can own multiple
5
- * classified threads, keyed by (groupId, vpId, threadId), with separate engine,
5
+ * classified threads, keyed by (sessionId, vpId, threadId), with separate engine,
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
  *
@@ -30,25 +30,25 @@ import { createVp, updateVp, deleteVp, readVp, VpCrudError } from './vp/vp-crud.
30
30
  import { scanVpLibrary } from './vp/vp-store.js';
31
31
  import { createRouter } from './routing/router.js';
32
32
  import {
33
- GroupCrudError,
34
- createGroupFromSpec,
35
- renameGroup,
36
- updateGroupAnnouncement,
37
- archiveGroup,
38
- deleteGroup,
39
- purgeArchivedGroups,
33
+ SessionCrudError,
34
+ createSessionFromSpec,
35
+ renameSession,
36
+ updateSessionAnnouncement,
37
+ archiveSession,
38
+ deleteSession,
39
+ purgeArchivedSessions,
40
40
  addMember,
41
41
  removeMember,
42
- setGroupDefaultVp,
43
- snapshotGroups,
44
- resolveGroupYeaftDir,
45
- groupsRoot,
46
- } from './groups/group-crud.js';
47
- import { openGroup, loadGroupMeta } from './groups/group-store.js';
48
- import { loadGroupConfig, resolveGroupConfig, GroupConfigError } from './groups/group-config.js';
49
- import { updateGroupConfig } from './groups/group-crud.js';
50
- import { createCoordinator } from './groups/coordinator.js';
51
- import { seedDefaultGroup } from './groups/seed-default.js';
42
+ setSessionDefaultVp,
43
+ snapshotSessions,
44
+ resolveSessionYeaftDir,
45
+ sessionsRoot,
46
+ } from './sessions/session-crud.js';
47
+ import { openSession, loadSessionMeta } from './sessions/session-store.js';
48
+ import { loadSessionConfig, resolveSessionConfig, SessionConfigError } from './sessions/session-config.js';
49
+ import { updateSessionConfig } from './sessions/session-crud.js';
50
+ import { createCoordinator } from './sessions/coordinator.js';
51
+ import { seedDefaultSession } from './sessions/seed-default.js';
52
52
  import {
53
53
  trimSnapshotForBudget,
54
54
  } from './history-compact.js';
@@ -70,7 +70,7 @@ export function __testSetThreadClassifier(fn) {
70
70
 
71
71
  /**
72
72
  * Tracks scoped-dream triggers that are currently inflight, keyed by
73
- * groupId. Used by `handleYeaftDreamTrigger` to reject any overlapping
73
+ * sessionId. Used by `handleYeaftDreamTrigger` to reject any overlapping
74
74
  * scoped trigger rather than racing the sink-wrapping logic against
75
75
  * itself.
76
76
  *
@@ -79,7 +79,7 @@ export function __testSetThreadClassifier(fn) {
79
79
  * the first's inflight promise and dropped its own scope filter. So
80
80
  * "B during A's run" doesn't actually produce a separate scoped pass
81
81
  * for B — letting B install a second sink wrapper would only mis-stamp
82
- * A's events with B's groupId. Reporting B as an explicit skipped
82
+ * A's events with B's sessionId. Reporting B as an explicit skipped
83
83
  * result is the honest answer; the user can re-click after A settles.
84
84
  * @type {Set<string>}
85
85
  */
@@ -90,7 +90,7 @@ const inflightScopedDreamGroups = new Set();
90
90
  * cancels the prior round (if any).
91
91
  *
92
92
  * Group VP turns do not flow through this slot. They each get their own
93
- * controller in `vpAborts` keyed by `${groupId}::${vpId}::${threadId}`.
93
+ * controller in `vpAborts` keyed by `${sessionId}::${vpId}::${threadId}`.
94
94
  * The `currentAbortCtrl` here is only mutated by 1:1 chat paths, the test
95
95
  * seeder, and the session-reset cleanup. Don't reach for it from group-flow
96
96
  * code; selective abort, abort-all, and abort-turn already operate against
@@ -112,7 +112,7 @@ const turnAbortCtrls = new Map();
112
112
  /**
113
113
  * Per-turn runtime ownership. Targeted thread aborts use this instead of
114
114
  * blindly aborting every turn controller in the process.
115
- * @type {Map<string, { groupId: string, vpId: string, threadId: string, key: string }>}
115
+ * @type {Map<string, { sessionId: string, vpId: string, threadId: string, key: string }>}
116
116
  */
117
117
  const turnAbortMeta = new Map();
118
118
 
@@ -136,13 +136,13 @@ function getVpStatusBroker() {
136
136
  // The broker emits both `vp_status_changed` and
137
137
  // `vp_status_snapshot`. Both ride the standard sendYeaftEvent
138
138
  // envelope so the frontend's existing yeaft_output dispatcher
139
- // sees them. We stamp groupId/vpId on the envelope for
139
+ // sees them. We stamp sessionId/vpId on the envelope for
140
140
  // events that target a specific VP so the server's per-client
141
- // routing (groupId scoping, etc.) works the same way as
141
+ // routing (sessionId scoping, etc.) works the same way as
142
142
  // typing events.
143
143
  const env = {};
144
144
  if (event && typeof event === 'object') {
145
- if (event.groupId) env.groupId = event.groupId;
145
+ if (event.sessionId) env.sessionId = event.sessionId;
146
146
  if (event.vpId) env.vpId = event.vpId;
147
147
  if (event.turnId) env.turnId = event.turnId;
148
148
  }
@@ -168,7 +168,7 @@ function getVpStatusBroker() {
168
168
  * (`#currentAbortCtrl`, `#__queryCounter`, `#pendingT2`,
169
169
  * `#abortReason`, `#adjustRanByGroup`, `#execLog`, `#currentThreadId`)
170
170
  * does not collide across concurrent VP turns. Engines are keyed by
171
- * `${groupId}::${vpId}::${threadId}` rather than vpId alone because
171
+ * `${sessionId}::${vpId}::${threadId}` rather than vpId alone because
172
172
  * Engine cannot serve two concurrent queries safely — even if AMS state
173
173
  * partitions correctly by groupKey, the non-group-keyed private state
174
174
  * would collide if the same VP ran turns in two groups or two threads
@@ -183,7 +183,7 @@ const vpEngines = new Map();
183
183
  /** @type {Map<string, AbortController>} */
184
184
  const vpAborts = new Map();
185
185
  /**
186
- * Per-(groupId, vpId) current TodoWrite list. Each VP in a group keeps
186
+ * Per-(sessionId, vpId) current TodoWrite list. Each VP in a group keeps
187
187
  * its own todo state so two VPs in the same group can independently
188
188
  * track multi-step tasks without overwriting each other. Threaded into
189
189
  * the engine's tool ctx via buildVpQueryOpts → getCurrentTodos /
@@ -193,7 +193,7 @@ const vpAborts = new Map();
193
193
  * loses the "what was the most recent list?" peek without breaking the
194
194
  * UI replay.
195
195
  *
196
- * Key: `${groupId}::${vpId}` (matches vpEngines/vpAborts convention).
196
+ * Key: `${sessionId}::${vpId}` (matches vpEngines/vpAborts convention).
197
197
  * Value: `Array<{content, status, activeForm}>` — the last full list
198
198
  * the VP wrote with TodoWrite.
199
199
  *
@@ -202,33 +202,33 @@ const vpAborts = new Map();
202
202
  const vpCurrentTodos = new Map();
203
203
  /**
204
204
  * Per-group cached coordinator + router. Created on first
205
- * `handleYeaftGroupChat` for a given groupId; reused across user messages
205
+ * `handleYeaftSessionSend` for a given sessionId; reused across user messages
206
206
  * AND across `route_forward` deliveries inside running VP turns (the
207
207
  * router is wired into engine ctx; if we recreated coord per turn the
208
208
  * route_forward path would deliver into a freshly-created `captured[]`
209
209
  * that nobody consumes — exactly the pre-707 bug).
210
210
  *
211
211
  * Purge sites:
212
- * - `invalidateGroupContext(groupId)` — called from every group CRUD
212
+ * - `invalidateGroupContext(sessionId)` — called from every group CRUD
213
213
  * handler that mutates roster / meta / lifecycle state on disk
214
214
  * (rename, update announcement, archive, delete, add/remove member,
215
215
  * set default VP).
216
- * - `handleYeaftGroupChat` — invalidates inline when its own
216
+ * - `handleYeaftSessionSend` — invalidates inline when its own
217
217
  * auto-add / default-VP-heal pass mutated the roster.
218
218
  * - `resetYeaftSession` and `__testResetVpState` clear the whole map.
219
219
  *
220
220
  * @type {Map<string, { coord: ReturnType<typeof createCoordinator>,
221
221
  * router: ReturnType<typeof createRouter>,
222
- * groupHandle: object }>}
222
+ * sessionHandle: object }>}
223
223
  */
224
224
  const groupContexts = new Map();
225
225
 
226
- function vpKey(groupId, vpId) {
227
- return `${groupId}::${vpId}`;
226
+ function vpKey(sessionId, vpId) {
227
+ return `${sessionId}::${vpId}`;
228
228
  }
229
229
 
230
- function threadKey(groupId, vpId, threadId) {
231
- return `${groupId}::${vpId}::${threadId || 'main'}`;
230
+ function threadKey(sessionId, vpId, threadId) {
231
+ return `${sessionId}::${vpId}::${threadId || 'main'}`;
232
232
  }
233
233
 
234
234
  function createThreadId() {
@@ -241,8 +241,8 @@ const vpThreads = new Map();
241
241
  /** @type {Map<string, Set<Promise<string|null>>>} */
242
242
  const routePromisesByMsgId = new Map();
243
243
 
244
- function getVpThreadMap(groupId, vpId) {
245
- const key = vpKey(groupId, vpId);
244
+ function getVpThreadMap(sessionId, vpId) {
245
+ const key = vpKey(sessionId, vpId);
246
246
  let map = vpThreads.get(key);
247
247
  if (!map) {
248
248
  map = new Map();
@@ -251,20 +251,20 @@ function getVpThreadMap(groupId, vpId) {
251
251
  return map;
252
252
  }
253
253
 
254
- function getRunningThreads(groupId, vpId) {
255
- return Array.from(getVpThreadMap(groupId, vpId).values())
254
+ function getRunningThreads(sessionId, vpId) {
255
+ return Array.from(getVpThreadMap(sessionId, vpId).values())
256
256
  .filter(t => t && RUNNING_THREAD_STATES.has(t.status));
257
257
  }
258
258
 
259
- function getOrCreateVpThread({ groupId, vpId, threadId, title }) {
260
- const map = getVpThreadMap(groupId, vpId);
259
+ function getOrCreateVpThread({ sessionId, vpId, threadId, title }) {
260
+ const map = getVpThreadMap(sessionId, vpId);
261
261
  const id = threadId || createThreadId();
262
262
  let thread = map.get(id);
263
263
  const now = Date.now();
264
264
  if (!thread) {
265
265
  thread = {
266
266
  threadId: id,
267
- groupId,
267
+ sessionId,
268
268
  vpId,
269
269
  status: 'queued',
270
270
  title: title || '新任务',
@@ -348,23 +348,23 @@ async function waitForRoutePromises(msgId) {
348
348
  * Drop the cached coordinator + router for a group AND abort/clear any
349
349
  * in-flight VP turns belonging to it. Call this from every CRUD handler
350
350
  * that mutates the group's roster, meta, or lifecycle state on disk —
351
- * the cached coord holds a closed `groupHandle`, so without invalidation
351
+ * the cached coord holds a closed `sessionHandle`, so without invalidation
352
352
  * later route_forward / ingest calls would read zombie meta (stale
353
353
  * roster, pre-rename announcement, kicked members still routable).
354
354
  *
355
355
  * Idempotent — safe to call when no entry exists.
356
356
  */
357
- function invalidateGroupContext(groupId) {
358
- if (!groupId) return;
359
- groupContexts.delete(groupId);
360
- const prefix = `${groupId}::`;
357
+ function invalidateGroupContext(sessionId) {
358
+ if (!sessionId) return;
359
+ groupContexts.delete(sessionId);
360
+ const prefix = `${sessionId}::`;
361
361
  for (const [k, ctrl] of vpAborts) {
362
362
  if (!k.startsWith(prefix)) continue;
363
363
  try { if (!ctrl.signal.aborted) ctrl.abort(); } catch { /* best-effort */ }
364
364
  vpAborts.delete(k);
365
365
  }
366
366
  for (const [turnId, meta] of Array.from(turnAbortMeta.entries())) {
367
- if (meta?.groupId !== groupId) continue;
367
+ if (meta?.sessionId !== sessionId) continue;
368
368
  turnAbortCtrls.delete(turnId);
369
369
  turnAbortMeta.delete(turnId);
370
370
  }
@@ -382,7 +382,7 @@ function invalidateGroupContext(groupId) {
382
382
  }
383
383
  // Engines are NOT torn down here on purpose. They hold subordinate
384
384
  // state (AMS adjustments) that should survive a meta change and a
385
- // closed groupHandle — they don't reach the on-disk group meta
385
+ // closed sessionHandle — they don't reach the on-disk group meta
386
386
  // directly. They *are* dropped on `resetYeaftSession`.
387
387
  }
388
388
 
@@ -458,7 +458,7 @@ let _vpUnsubscribe = null;
458
458
 
459
459
  /**
460
460
  * Per-group conversation history lives on the GroupContext entry
461
- * (`groupContexts.get(groupId).history`). The pre-refactor module-level
461
+ * (`groupContexts.get(sessionId).history`). The pre-refactor module-level
462
462
  * `conversationMessages` was a single array shared across every group —
463
463
  * a user prompt in group-A would leak into group-B's next-turn snapshot
464
464
  * because the bridge appended every turn to the same array regardless
@@ -466,7 +466,7 @@ let _vpUnsubscribe = null;
466
466
  * the in-memory tape was unified.
467
467
  *
468
468
  * Post-refactor: each GroupContext owns its own `history`, lazily
469
- * hydrated from `conversationStore.loadRecentByGroup(groupId)` on first
469
+ * hydrated from `conversationStore.loadRecentByGroup(sessionId)` on first
470
470
  * access. Group-A and group-B are isolated.
471
471
  *
472
472
  * @typedef {Array<{role:'user'|'assistant'|'tool', content:string|Array, toolCalls?:Array, toolCallId?:string, isError?:boolean}>} GroupHistory
@@ -474,9 +474,9 @@ let _vpUnsubscribe = null;
474
474
 
475
475
  /**
476
476
  * @typedef {Object} GroupContextEntry
477
- * @property {object|null} coord — group coordinator (lazily built by getOrCreateGroupContext)
478
- * @property {object|null} router — message router (lazily built by getOrCreateGroupContext)
479
- * @property {object|null} groupHandle — opened group handle (lazily built by getOrCreateGroupContext)
477
+ * @property {object|null} coord — group coordinator (lazily built by getOrCreateSessionContext)
478
+ * @property {object|null} router — message router (lazily built by getOrCreateSessionContext)
479
+ * @property {object|null} sessionHandle — opened group handle (lazily built by getOrCreateSessionContext)
480
480
  * @property {GroupHistory} history — per-group conversation tape
481
481
  * @property {boolean} historyHydrated — true once history has been loaded
482
482
  * from disk (or explicitly assigned). The flag is required because an
@@ -491,7 +491,7 @@ function makeGroupContextStub() {
491
491
  return {
492
492
  coord: null,
493
493
  router: null,
494
- groupHandle: null,
494
+ sessionHandle: null,
495
495
  history: [],
496
496
  historyHydrated: false,
497
497
  };
@@ -564,7 +564,7 @@ function projectPersistedToHistoryEntry(m) {
564
564
  if (m.id) entry.id = m.id;
565
565
  entry.threadId = m.threadId || m.turnId || 'main';
566
566
  entry.turnId = m.turnId || entry.threadId;
567
- if (m.groupId) entry.groupId = m.groupId;
567
+ if (m.sessionId) entry.sessionId = m.sessionId;
568
568
  if (m.speakerVpId) entry.speakerVpId = m.speakerVpId;
569
569
  if (m.toolCallId) entry.toolCallId = m.toolCallId;
570
570
  if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
@@ -597,13 +597,13 @@ function hydrateHistoryAttachmentPreviews(attachments) {
597
597
  });
598
598
  }
599
599
 
600
- function loadVisibleGroupHistoryPage(store, groupId, limit, beforeSeq = null) {
601
- if (!store || !groupId || !(limit > 0)) return { messages: [], oldestSeq: null, hasMore: false };
600
+ function loadVisibleGroupHistoryPage(store, sessionId, limit, beforeSeq = null) {
601
+ if (!store || !sessionId || !(limit > 0)) return { messages: [], oldestSeq: null, hasMore: false };
602
602
 
603
603
  let rows = [];
604
604
  try {
605
605
  if (typeof store.loadVisibleByGroup === 'function') {
606
- const page = store.loadVisibleByGroup(groupId, beforeSeq, limit);
606
+ const page = store.loadVisibleByGroup(sessionId, beforeSeq, limit);
607
607
  return {
608
608
  messages: (page.messages || []).map(projectPersistedToVisibleHistoryEntry).filter(Boolean),
609
609
  oldestSeq: (typeof page.oldestSeq === 'number') ? page.oldestSeq : null,
@@ -612,16 +612,16 @@ function loadVisibleGroupHistoryPage(store, groupId, limit, beforeSeq = null) {
612
612
  } else if (typeof store.loadOlderByGroup === 'function') {
613
613
  // Compatibility fallback for older test doubles: use an unbounded raw
614
614
  // prefix, then project/slice visible rows below.
615
- rows = store.loadOlderByGroup(groupId, beforeSeq, Infinity).messages || [];
615
+ rows = store.loadOlderByGroup(sessionId, beforeSeq, Infinity).messages || [];
616
616
  } else if (Number.isFinite(beforeSeq)) {
617
617
  const all = typeof store.loadAllByGroup === 'function'
618
- ? store.loadAllByGroup(groupId)
619
- : store.loadRecentByGroup(groupId, Infinity);
618
+ ? store.loadAllByGroup(sessionId)
619
+ : store.loadRecentByGroup(sessionId, Infinity);
620
620
  rows = all.filter(m => parseSeqFromId(m?.id) < beforeSeq);
621
621
  } else if (typeof store.loadAllByGroup === 'function') {
622
- rows = store.loadAllByGroup(groupId);
622
+ rows = store.loadAllByGroup(sessionId);
623
623
  } else {
624
- rows = store.loadRecentByGroup(groupId, Infinity);
624
+ rows = store.loadRecentByGroup(sessionId, Infinity);
625
625
  }
626
626
  } catch (err) {
627
627
  console.error('[Yeaft] visible history page load failed:', err?.message || err);
@@ -651,16 +651,16 @@ function loadVisibleGroupHistoryPage(store, groupId, limit, beforeSeq = null) {
651
651
  * conversation store. Returns an empty array if the session isn't
652
652
  * loaded yet (sub-agent / test paths) or if the load throws.
653
653
  *
654
- * @param {string} groupId
654
+ * @param {string} sessionId
655
655
  * @returns {GroupHistory}
656
656
  */
657
- function hydrateGroupHistory(groupId) {
658
- if (!session?.conversationStore || !groupId) return [];
657
+ function hydrateGroupHistory(sessionId) {
658
+ if (!session?.conversationStore || !sessionId) return [];
659
659
  let recent;
660
660
  try {
661
- recent = session.conversationStore.loadRecentByGroup(groupId);
661
+ recent = session.conversationStore.loadRecentByGroup(sessionId);
662
662
  } catch (err) {
663
- console.warn('[Yeaft] hydrateGroupHistory failed (groupId=%s):', groupId, err?.message || err);
663
+ console.warn('[Yeaft] hydrateGroupHistory failed (sessionId=%s):', sessionId, err?.message || err);
664
664
  return [];
665
665
  }
666
666
  const out = [];
@@ -675,7 +675,7 @@ function hydrateGroupHistory(groupId) {
675
675
  * Get-or-create the per-group history array. Used everywhere the bridge
676
676
  * needs to read/append/snapshot a group's conversation tape. Lazily
677
677
  * inserts an entry into `groupContexts` on first access — no
678
- * `groupHandle` required (history is independent of coord/router
678
+ * `sessionHandle` required (history is independent of coord/router
679
679
  * lifecycle, so a sub-agent / route_forward path that hasn't yet
680
680
  * opened the group can still read history).
681
681
  *
@@ -684,24 +684,24 @@ function hydrateGroupHistory(groupId) {
684
684
  * compact (race guard checks reference equality), `consolidate`
685
685
  * events, and session reset.
686
686
  *
687
- * @param {string} groupId
687
+ * @param {string} sessionId
688
688
  * @returns {GroupHistory}
689
689
  */
690
- function getOrCreateGroupHistory(groupId) {
691
- if (!groupId) return [];
692
- let entry = groupContexts.get(groupId);
690
+ function getOrCreateGroupHistory(sessionId) {
691
+ if (!sessionId) return [];
692
+ let entry = groupContexts.get(sessionId);
693
693
  // Use `historyHydrated` rather than truthiness on `history` itself —
694
694
  // an empty array (post-consolidate, post-clear, or a partial entry
695
- // seeded by an early `getOrCreateGroupContext` call before data was
695
+ // seeded by an early `getOrCreateSessionContext` call before data was
696
696
  // loaded) is legitimate state that does NOT mean "needs hydration"...
697
697
  // unless we never loaded from disk in the first place. The flag
698
698
  // separates the two cases.
699
699
  if (entry && entry.historyHydrated) return entry.history;
700
700
  if (!entry) {
701
701
  entry = makeGroupContextStub();
702
- groupContexts.set(groupId, entry);
702
+ groupContexts.set(sessionId, entry);
703
703
  }
704
- entry.history = hydrateGroupHistory(groupId);
704
+ entry.history = hydrateGroupHistory(sessionId);
705
705
  entry.historyHydrated = true;
706
706
  return entry.history;
707
707
  }
@@ -715,15 +715,15 @@ function getOrCreateGroupHistory(groupId) {
715
715
  * a hydration — even setting `[]` after `consolidate` means "this is the
716
716
  * canonical state right now, don't re-load from disk".
717
717
  *
718
- * @param {string} groupId
718
+ * @param {string} sessionId
719
719
  * @param {GroupHistory} next
720
720
  */
721
- function setGroupHistory(groupId, next) {
722
- if (!groupId) return;
723
- let entry = groupContexts.get(groupId);
721
+ function setGroupHistory(sessionId, next) {
722
+ if (!sessionId) return;
723
+ let entry = groupContexts.get(sessionId);
724
724
  if (!entry) {
725
725
  entry = makeGroupContextStub();
726
- groupContexts.set(groupId, entry);
726
+ groupContexts.set(sessionId, entry);
727
727
  }
728
728
  entry.history = next;
729
729
  entry.historyHydrated = true;
@@ -734,10 +734,10 @@ function setGroupHistory(groupId, next) {
734
734
  * `__testGroupHistory`. Lets tests pin the per-group isolation contract
735
735
  * without booting a full session.
736
736
  *
737
- * @param {string} groupId
737
+ * @param {string} sessionId
738
738
  */
739
- export function __testGroupHistory(groupId) {
740
- return getOrCreateGroupHistory(groupId);
739
+ export function __testGroupHistory(sessionId) {
740
+ return getOrCreateGroupHistory(sessionId);
741
741
  }
742
742
 
743
743
  /**
@@ -746,8 +746,8 @@ export function __testGroupHistory(groupId) {
746
746
  *
747
747
  * Tests that need to verify the hydrate-from-disk path can construct a
748
748
  * `ConversationStore` against a tmp dir, write per-group records via
749
- * `store.append({groupId, ...})`, then call this helper to wire the
750
- * store into the bridge before calling `__testGroupHistory(groupId)`.
749
+ * `store.append({sessionId, ...})`, then call this helper to wire the
750
+ * store into the bridge before calling `__testGroupHistory(sessionId)`.
751
751
  *
752
752
  * @param {{ conversationStore: object } | null} sessionLike
753
753
  */
@@ -760,10 +760,10 @@ export function __testSetSession(sessionLike) {
760
760
  * if never seeded). Lets tests assert the `historyHydrated` flag without
761
761
  * exporting the entire `groupContexts` Map.
762
762
  *
763
- * @param {string} groupId
763
+ * @param {string} sessionId
764
764
  */
765
- export function __testGroupContextEntry(groupId) {
766
- return groupContexts.get(groupId);
765
+ export function __testGroupContextEntry(sessionId) {
766
+ return groupContexts.get(sessionId);
767
767
  }
768
768
 
769
769
  /**
@@ -772,20 +772,20 @@ export function __testGroupContextEntry(groupId) {
772
772
  * dependencies (notably `toolStats`) come from the session reference —
773
773
  * see `test/agent/web-bridge-vp-engine-tool-stats.test.js`.
774
774
  *
775
- * @param {string} groupId
775
+ * @param {string} sessionId
776
776
  * @param {string} vpId
777
777
  */
778
- export function __testGetOrCreateVpEngine(groupId, vpId, threadId = 'main') {
779
- return getOrCreateVpEngine(groupId, vpId, threadId);
778
+ export function __testGetOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
779
+ return getOrCreateVpEngine(sessionId, vpId, threadId);
780
780
  }
781
781
 
782
782
 
783
783
  /** Test-only: inspect runtime thread rows for a VP. */
784
- export function __testGetVpThreads(groupId, vpId) {
785
- const map = vpThreads.get(vpKey(groupId, vpId));
784
+ export function __testGetVpThreads(sessionId, vpId) {
785
+ const map = vpThreads.get(vpKey(sessionId, vpId));
786
786
  return Array.from((map || new Map()).values()).map((thread) => ({
787
787
  threadId: thread.threadId,
788
- groupId: thread.groupId,
788
+ sessionId: thread.sessionId,
789
789
  vpId: thread.vpId,
790
790
  status: thread.status,
791
791
  title: thread.title,
@@ -800,8 +800,8 @@ export async function __testWaitForRoutePromises(msgId) {
800
800
  }
801
801
 
802
802
  /** Test-only: route one coordinator envelope into the VP thread runtime. */
803
- export function __testEnqueueForVp(groupId, vpId, envelope) {
804
- return enqueueForVp(groupId, vpId, envelope);
803
+ export function __testEnqueueForVp(sessionId, vpId, envelope) {
804
+ return enqueueForVp(sessionId, vpId, envelope);
805
805
  }
806
806
 
807
807
  /** Whether we've already sent a permission warning to the UI */
@@ -825,12 +825,12 @@ function isPermissionErrorMsg(msg) {
825
825
  * adapter / trace / config / stores so memory recall, conversation
826
826
  * persistence, and tool registry remain consistent.
827
827
  *
828
- * @param {string} groupId
828
+ * @param {string} sessionId
829
829
  * @param {string} vpId
830
830
  * @returns {import('./engine.js').Engine}
831
831
  */
832
- function getOrCreateVpEngine(groupId, vpId, threadId = 'main') {
833
- const key = threadKey(groupId, vpId, threadId);
832
+ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
833
+ const key = threadKey(sessionId, vpId, threadId);
834
834
  let eng = vpEngines.get(key);
835
835
  if (eng) return eng;
836
836
  if (!session) throw new Error('getOrCreateVpEngine: session not loaded');
@@ -838,8 +838,8 @@ function getOrCreateVpEngine(groupId, vpId, threadId = 'main') {
838
838
  // session's user-level config when no override is set. The resolver
839
839
  // never mutates session.config — it returns a new object.
840
840
  const yeaftDir = ctx.CONFIG?.yeaftDir || session.yeaftDir;
841
- const groupCfg = loadGroupConfig(yeaftDir, groupId);
842
- const effectiveConfig = resolveGroupConfig(session.config, groupCfg);
841
+ const groupCfg = loadSessionConfig(yeaftDir, sessionId);
842
+ const effectiveConfig = resolveSessionConfig(session.config, groupCfg);
843
843
  eng = new Engine({
844
844
  adapter: session.adapter,
845
845
  trace: session.trace,
@@ -857,11 +857,11 @@ function getOrCreateVpEngine(groupId, vpId, threadId = 'main') {
857
857
  // (`if (this.#toolStats && ...)`) is false and group VP tool calls
858
858
  // are silently dropped.
859
859
  toolStats: session.toolStats || null,
860
- // Per-VP fan-out: bind the engine to its (groupId, vpId) so post-turn
860
+ // Per-VP fan-out: bind the engine to its (sessionId, vpId) so post-turn
861
861
  // compact reads/writes a scoped summary instead of the legacy global
862
862
  // compact.md (which every VP would otherwise share, producing
863
863
  // identical, ever-growing summaries across groups).
864
- groupId,
864
+ sessionId,
865
865
  vpId,
866
866
  });
867
867
  vpEngines.set(key, eng);
@@ -873,42 +873,42 @@ function getOrCreateVpEngine(groupId, vpId, threadId = 'main') {
873
873
  *
874
874
  * The coordinator MUST be reused across user turns AND across in-flight
875
875
  * tool calls (route_forward) — its `deliver` callback is the only way
876
- * envelopes reach `vpInboxes`. If we recreated it per `handleYeaftGroupChat`
876
+ * envelopes reach `vpInboxes`. If we recreated it per `handleYeaftSessionSend`
877
877
  * call (the pre-707 design), `route_forward` running mid-turn would
878
878
  * deliver into a doomed `captured[]` while the new dispatch ran against
879
879
  * a fresh coordinator. The persistent coordinator + module-level inboxes
880
880
  * close that gap.
881
881
  *
882
- * Caller is responsible for passing in a freshly-opened groupHandle on
882
+ * Caller is responsible for passing in a freshly-opened sessionHandle on
883
883
  * first creation; subsequent calls reuse the cached coord.
884
884
  *
885
- * @param {string} groupId
886
- * @param {object} groupHandle — only used on first creation
887
- * @returns {{ coord: object, router: object, groupHandle: object }}
885
+ * @param {string} sessionId
886
+ * @param {object} sessionHandle — only used on first creation
887
+ * @returns {{ coord: object, router: object, sessionHandle: object }}
888
888
  */
889
- function getOrCreateGroupContext(groupId, groupHandle) {
890
- let entry = groupContexts.get(groupId);
889
+ function getOrCreateSessionContext(sessionId, sessionHandle) {
890
+ let entry = groupContexts.get(sessionId);
891
891
  if (entry && entry.coord && entry.router) return entry;
892
892
  // Either no entry, or a partial entry seeded by `getOrCreateGroupHistory`
893
893
  // (no coord/router yet). Build the coord/router and merge into the
894
894
  // existing record so the per-group history reference and hydration
895
895
  // flag are preserved.
896
- const coord = createCoordinator(groupHandle, {
897
- deliver: (vpId, envelope) => enqueueForVp(groupId, vpId, envelope),
896
+ const coord = createCoordinator(sessionHandle, {
897
+ deliver: (vpId, envelope) => enqueueForVp(sessionId, vpId, envelope),
898
898
  });
899
899
  const router = createRouter({ coordinator: coord });
900
900
  if (!entry) {
901
901
  entry = makeGroupContextStub();
902
- groupContexts.set(groupId, entry);
902
+ groupContexts.set(sessionId, entry);
903
903
  }
904
904
  entry.coord = coord;
905
905
  entry.router = router;
906
- entry.groupHandle = groupHandle;
906
+ entry.sessionHandle = sessionHandle;
907
907
  // Defend against a future caller that builds a coord/router without
908
908
  // having gone through `getOrCreateGroupHistory` first: a partial entry
909
909
  // could exist with `historyHydrated:false`, so do the load now.
910
910
  if (!entry.historyHydrated) {
911
- entry.history = hydrateGroupHistory(groupId);
911
+ entry.history = hydrateGroupHistory(sessionId);
912
912
  entry.historyHydrated = true;
913
913
  }
914
914
  return entry;
@@ -923,23 +923,23 @@ function getOrCreateGroupContext(groupId, groupHandle) {
923
923
  * `route_forward` makes the target VP's typing dot light up before the
924
924
  * engine even starts that turn.
925
925
  *
926
- * @param {string} groupId
926
+ * @param {string} sessionId
927
927
  * @param {string} vpId
928
- * @param {object} envelope — coordinator envelope `{groupId, taskId, msg, trigger}`
928
+ * @param {object} envelope — coordinator envelope `{sessionId, taskId, msg, trigger}`
929
929
  */
930
- function enqueueForVp(groupId, vpId, envelope) {
931
- const routePromise = routeEnvelopeToVpThread(groupId, vpId, envelope);
930
+ function enqueueForVp(sessionId, vpId, envelope) {
931
+ const routePromise = routeEnvelopeToVpThread(sessionId, vpId, envelope);
932
932
  registerRoutePromise(envelope?.msg?.id, routePromise);
933
933
  }
934
934
 
935
- async function routeEnvelopeToVpThread(groupId, vpId, envelope) {
935
+ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
936
936
  const { text, prompt, promptParts } = buildVpPromptPayload(vpId, envelope);
937
- const runningThreads = getRunningThreads(groupId, vpId);
937
+ const runningThreads = getRunningThreads(sessionId, vpId);
938
938
  let thread = null;
939
939
  let related = false;
940
940
 
941
941
  if (runningThreads.length === 0) {
942
- thread = getOrCreateVpThread({ groupId, vpId, title: fallbackTitle(text) });
942
+ thread = getOrCreateVpThread({ sessionId, vpId, title: fallbackTitle(text) });
943
943
  } else {
944
944
  const decision = await threadClassifier({
945
945
  adapter: session?.adapter,
@@ -951,7 +951,7 @@ async function routeEnvelopeToVpThread(groupId, vpId, envelope) {
951
951
  const targetIsRunning = runningThreads.some((t) => t.threadId === decision.targetThreadId);
952
952
  if (decision.decision === 'related' && decision.targetThreadId && targetIsRunning) {
953
953
  thread = getOrCreateVpThread({
954
- groupId,
954
+ sessionId,
955
955
  vpId,
956
956
  threadId: decision.targetThreadId,
957
957
  title: decision.title,
@@ -959,7 +959,7 @@ async function routeEnvelopeToVpThread(groupId, vpId, envelope) {
959
959
  related = true;
960
960
  } else {
961
961
  thread = getOrCreateVpThread({
962
- groupId,
962
+ sessionId,
963
963
  vpId,
964
964
  title: decision.title || fallbackTitle(text),
965
965
  });
@@ -976,7 +976,7 @@ async function routeEnvelopeToVpThread(groupId, vpId, envelope) {
976
976
  persistInboundMessageOnceByMsgId({
977
977
  msgId: envelope?.msg?.id,
978
978
  text,
979
- groupId,
979
+ sessionId,
980
980
  threadId: thread.threadId,
981
981
  role: envelope?.msg?.meta?.injectedBy === 'route_forward' ? 'assistant' : 'user',
982
982
  speakerVpId: envelope?.msg?.meta?.senderVpId || envelope?.msg?.from || null,
@@ -986,18 +986,18 @@ async function routeEnvelopeToVpThread(groupId, vpId, envelope) {
986
986
  try {
987
987
  sendYeaftEvent({
988
988
  type: 'vp_thread_user_appended',
989
- groupId,
989
+ sessionId,
990
990
  vpId,
991
991
  threadId: thread.threadId,
992
992
  title: thread.title,
993
993
  turnId,
994
994
  ts: Date.now(),
995
- }, { groupId, vpId, threadId: thread.threadId, turnId });
995
+ }, { sessionId, vpId, threadId: thread.threadId, turnId });
996
996
  } catch { /* never crash WS pipeline */ }
997
997
  return thread.threadId;
998
998
  }
999
999
 
1000
- const key = threadKey(groupId, vpId, thread.threadId);
1000
+ const key = threadKey(sessionId, vpId, thread.threadId);
1001
1001
  let inbox = vpInboxes.get(key);
1002
1002
  if (!inbox) {
1003
1003
  inbox = [];
@@ -1008,18 +1008,18 @@ async function routeEnvelopeToVpThread(groupId, vpId, envelope) {
1008
1008
  try {
1009
1009
  sendYeaftEvent({
1010
1010
  type: 'vp_typing_start',
1011
- groupId,
1011
+ sessionId,
1012
1012
  vpId,
1013
1013
  threadId: thread.threadId,
1014
1014
  turnId,
1015
1015
  ts: Date.now(),
1016
- }, { groupId, vpId, threadId: thread.threadId, turnId });
1016
+ }, { sessionId, vpId, threadId: thread.threadId, turnId });
1017
1017
  } catch { /* never crash WS pipeline */ }
1018
1018
 
1019
1019
  try {
1020
1020
  thread.status = 'typing';
1021
1021
  getVpStatusBroker().transition({
1022
- groupId,
1022
+ sessionId,
1023
1023
  vpId,
1024
1024
  threadId: thread.threadId,
1025
1025
  title: thread.title,
@@ -1031,24 +1031,24 @@ async function routeEnvelopeToVpThread(groupId, vpId, envelope) {
1031
1031
  console.warn('[Yeaft] vp-status typing transition failed:', err?.message || err);
1032
1032
  }
1033
1033
 
1034
- ensureDriverRunning(groupId, vpId, thread.threadId);
1034
+ ensureDriverRunning(sessionId, vpId, thread.threadId);
1035
1035
  return thread.threadId;
1036
1036
  }
1037
1037
 
1038
- function ensureDriverRunning(groupId, vpId, threadId = 'main') {
1039
- const key = threadKey(groupId, vpId, threadId);
1038
+ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
1039
+ const key = threadKey(sessionId, vpId, threadId);
1040
1040
  if (vpDrivers.has(key)) return;
1041
1041
  const promise = (async () => {
1042
1042
  while (true) {
1043
1043
  const inbox = vpInboxes.get(key);
1044
1044
  if (!inbox || inbox.length === 0) break;
1045
1045
  const { envelope, turnId, thread: queuedThread } = inbox.shift();
1046
- const thread = queuedThread || getOrCreateVpThread({ groupId, vpId, threadId });
1046
+ const thread = queuedThread || getOrCreateVpThread({ sessionId, vpId, threadId });
1047
1047
  const vpAbort = new AbortController();
1048
1048
  vpAborts.set(key, vpAbort);
1049
1049
  turnAbortCtrls.set(turnId, vpAbort);
1050
- turnAbortMeta.set(turnId, { groupId, vpId, threadId: thread.threadId, key });
1051
- const baseSnapshot = getOrCreateGroupHistory(groupId)
1050
+ turnAbortMeta.set(turnId, { sessionId, vpId, threadId: thread.threadId, key });
1051
+ const baseSnapshot = getOrCreateGroupHistory(sessionId)
1052
1052
  .filter((m) => !m.threadId || m.threadId === 'main' || m.threadId === thread.threadId);
1053
1053
  const trigger = envelope?.trigger || 'fallback';
1054
1054
  const { text, prompt, promptParts } = buildVpPromptPayload(vpId, envelope);
@@ -1062,7 +1062,7 @@ function ensureDriverRunning(groupId, vpId, threadId = 'main') {
1062
1062
  persistInboundMessageOnceByMsgId({
1063
1063
  msgId: envMsgId,
1064
1064
  text,
1065
- groupId,
1065
+ sessionId,
1066
1066
  threadId: thread.threadId,
1067
1067
  role: isForward ? 'assistant' : 'user',
1068
1068
  speakerVpId: senderVpId,
@@ -1075,7 +1075,7 @@ function ensureDriverRunning(groupId, vpId, threadId = 'main') {
1075
1075
  await runVpTurnWithEscalation({
1076
1076
  prompt,
1077
1077
  promptParts,
1078
- groupId,
1078
+ sessionId,
1079
1079
  vpId,
1080
1080
  threadId: thread.threadId,
1081
1081
  thread,
@@ -1093,19 +1093,19 @@ function ensureDriverRunning(groupId, vpId, threadId = 'main') {
1093
1093
  try {
1094
1094
  sendYeaftEvent({
1095
1095
  type: 'vp_typing_end',
1096
- groupId,
1096
+ sessionId,
1097
1097
  vpId,
1098
1098
  threadId: thread.threadId,
1099
1099
  turnId,
1100
1100
  ts: Date.now(),
1101
- }, { groupId, vpId, threadId: thread.threadId, turnId });
1101
+ }, { sessionId, vpId, threadId: thread.threadId, turnId });
1102
1102
  } catch { /* never crash WS pipeline */ }
1103
1103
  }
1104
1104
  try {
1105
1105
  if (text && envelope?.msg) {
1106
1106
  sendYeaftEvent({
1107
1107
  type: 'group_message',
1108
- groupId,
1108
+ sessionId,
1109
1109
  vpId,
1110
1110
  threadId: thread.threadId,
1111
1111
  speakerVpId: vpId,
@@ -1113,7 +1113,7 @@ function ensureDriverRunning(groupId, vpId, threadId = 'main') {
1113
1113
  mentions: Array.isArray(envelope?.msg?.mentions) ? envelope.msg.mentions : [],
1114
1114
  trigger,
1115
1115
  ts: Date.now(),
1116
- }, { groupId, vpId, threadId: thread.threadId, turnId });
1116
+ }, { sessionId, vpId, threadId: thread.threadId, turnId });
1117
1117
  }
1118
1118
  } catch { /* never crash WS pipeline */ }
1119
1119
 
@@ -1142,7 +1142,7 @@ function ensureDriverRunning(groupId, vpId, threadId = 'main') {
1142
1142
  if (!replayText && !replayParts) continue;
1143
1143
  const followUpId = `followup_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
1144
1144
  const followUpEnvelope = {
1145
- groupId,
1145
+ sessionId,
1146
1146
  taskId: envelope?.taskId || null,
1147
1147
  trigger: 'pending_rescue',
1148
1148
  msg: {
@@ -1159,7 +1159,7 @@ function ensureDriverRunning(groupId, vpId, threadId = 'main') {
1159
1159
  thread.status = 'typing';
1160
1160
  thread.updatedAt = Date.now();
1161
1161
  getVpStatusBroker().transition({
1162
- groupId,
1162
+ sessionId,
1163
1163
  vpId,
1164
1164
  threadId: thread.threadId,
1165
1165
  title: thread.title || '',
@@ -1173,19 +1173,19 @@ function ensureDriverRunning(groupId, vpId, threadId = 'main') {
1173
1173
  try {
1174
1174
  sendYeaftEvent({
1175
1175
  type: 'vp_typing_start',
1176
- groupId,
1176
+ sessionId,
1177
1177
  vpId,
1178
1178
  threadId: thread.threadId,
1179
1179
  turnId: followUpTurnId,
1180
1180
  ts: Date.now(),
1181
- }, { groupId, vpId, threadId: thread.threadId, turnId: followUpTurnId });
1181
+ }, { sessionId, vpId, threadId: thread.threadId, turnId: followUpTurnId });
1182
1182
  } catch { /* never crash WS pipeline */ }
1183
1183
  }
1184
1184
  }
1185
1185
  }
1186
1186
  vpDrivers.delete(key);
1187
1187
  const tail = vpInboxes.get(key);
1188
- if (tail && tail.length > 0) ensureDriverRunning(groupId, vpId, threadId);
1188
+ if (tail && tail.length > 0) ensureDriverRunning(sessionId, vpId, threadId);
1189
1189
  })();
1190
1190
  vpDrivers.set(key, promise);
1191
1191
  }
@@ -1240,13 +1240,13 @@ export async function __testResetVpState() {
1240
1240
 
1241
1241
  /**
1242
1242
  * Send a yeaft_output message carrying claude_output-format data.
1243
- * Envelope fields: conversationId, groupId, vpId, turnId, threadId let the
1243
+ * Envelope fields: conversationId, sessionId, vpId, turnId, threadId let the
1244
1244
  * frontend route incremental deltas to the correct per-VP/thread block.
1245
1245
  */
1246
- function resolveGroupDefaultVpId(groupId) {
1247
- if (!groupId) return null;
1246
+ function resolveGroupDefaultVpId(sessionId) {
1247
+ if (!sessionId) return null;
1248
1248
  try {
1249
- const meta = ensureGroupCoordinator(groupId)?.group?.getMeta?.();
1249
+ const meta = ensureGroupCoordinator(sessionId)?.group?.getMeta?.();
1250
1250
  const vpId = typeof meta?.defaultVpId === 'string' ? meta.defaultVpId.trim() : '';
1251
1251
  return vpId || null;
1252
1252
  } catch {
@@ -1254,12 +1254,12 @@ function resolveGroupDefaultVpId(groupId) {
1254
1254
  }
1255
1255
  }
1256
1256
 
1257
- function sendYeaftOutput(data, { groupId, chatId, vpId, turnId, threadId } = {}) {
1258
- const resolvedVpId = vpId || (groupId ? resolveGroupDefaultVpId(groupId) : null);
1257
+ function sendYeaftOutput(data, { sessionId, chatId, vpId, turnId, threadId } = {}) {
1258
+ const resolvedVpId = vpId || (sessionId ? resolveGroupDefaultVpId(sessionId) : null);
1259
1259
  sendToServer({
1260
1260
  type: 'yeaft_output',
1261
1261
  conversationId: yeaftConversationId,
1262
- ...(groupId ? { groupId } : {}),
1262
+ ...(sessionId ? { sessionId } : {}),
1263
1263
  ...(chatId ? { chatId } : {}),
1264
1264
  ...(resolvedVpId ? { vpId: resolvedVpId } : {}),
1265
1265
  ...(turnId ? { turnId } : {}),
@@ -1269,11 +1269,11 @@ function sendYeaftOutput(data, { groupId, chatId, vpId, turnId, threadId } = {})
1269
1269
  }
1270
1270
 
1271
1271
  /** Send a yeaft_output event (non-claude_output metadata). */
1272
- function sendYeaftEvent(event, { groupId, chatId, vpId, turnId, threadId } = {}) {
1272
+ function sendYeaftEvent(event, { sessionId, chatId, vpId, turnId, threadId } = {}) {
1273
1273
  sendToServer({
1274
1274
  type: 'yeaft_output',
1275
1275
  conversationId: yeaftConversationId,
1276
- ...(groupId ? { groupId } : {}),
1276
+ ...(sessionId ? { sessionId } : {}),
1277
1277
  ...(chatId ? { chatId } : {}),
1278
1278
  ...(vpId ? { vpId } : {}),
1279
1279
  ...(turnId ? { turnId } : {}),
@@ -1375,7 +1375,7 @@ export function handleYeaftVpDelete(msg) {
1375
1375
  try {
1376
1376
  const broker = getVpStatusBroker();
1377
1377
  for (const row of broker.snapshot()) {
1378
- if (row.vpId === vpId) broker.forget({ groupId: row.groupId, vpId });
1378
+ if (row.vpId === vpId) broker.forget({ sessionId: row.sessionId, vpId });
1379
1379
  }
1380
1380
  } catch (err) {
1381
1381
  console.warn('[Yeaft] vp-status forget on delete failed:', err?.message || err);
@@ -1416,60 +1416,73 @@ export function handleYeaftVpRead(msg) {
1416
1416
  * Group CRUD wired to WS events.
1417
1417
  */
1418
1418
  function sendGroupCrudResult(payload) {
1419
- sendYeaftEvent({ type: 'group_crud_result', ...payload });
1419
+ // Wire-compat: emit BOTH legacy `group_crud_result` (old web bundles)
1420
+ // and the new `session_crud_result` alias. Payload includes both
1421
+ // `groupId` and `sessionId` for the same reason — see below.
1422
+ const out = { ...payload };
1423
+ if (out.sessionId != null && out.groupId == null) out.groupId = out.sessionId;
1424
+ if (out.groupId != null && out.sessionId == null) out.sessionId = out.groupId;
1425
+ sendYeaftEvent({ type: 'group_crud_result', ...out });
1426
+ sendYeaftEvent({ type: 'session_crud_result', ...out });
1420
1427
  }
1421
1428
 
1422
1429
  function sendGroupSnapshotBroadcast() {
1423
1430
  try {
1424
1431
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1425
1432
  if (!yeaftDir) return;
1426
- const groups = snapshotGroups(yeaftDir);
1427
- sendYeaftEvent({ type: 'group_list_updated', groups });
1433
+ const sessions = snapshotSessions(yeaftDir);
1434
+ // Wire-compat: emit both legacy `group_list_updated` (groups field)
1435
+ // and new `session_list_updated` (sessions field). Old web bundles
1436
+ // listen on the former.
1437
+ sendYeaftEvent({ type: 'group_list_updated', groups: sessions });
1438
+ sendYeaftEvent({ type: 'session_list_updated', sessions });
1428
1439
  } catch (err) {
1429
1440
  console.warn('[Yeaft] sendGroupSnapshotBroadcast failed:', err?.message || err);
1430
1441
  }
1431
1442
  }
1432
1443
 
1433
- function sendGroupRosterChanged(group) {
1434
- if (!group) return;
1435
- sendYeaftEvent({
1436
- type: 'group_roster_changed',
1437
- groupId: group.id,
1438
- name: group.name,
1439
- roster: group.roster,
1440
- defaultVpId: group.defaultVpId,
1441
- workDir: group.workDir || '',
1442
- });
1444
+ function sendGroupRosterChanged(session) {
1445
+ if (!session) return;
1446
+ const payload = {
1447
+ sessionId: session.id,
1448
+ groupId: session.id, // wire-compat for old web bundles
1449
+ name: session.name,
1450
+ roster: session.roster,
1451
+ defaultVpId: session.defaultVpId,
1452
+ workDir: session.workDir || '',
1453
+ };
1454
+ sendYeaftEvent({ type: 'group_roster_changed', ...payload });
1455
+ sendYeaftEvent({ type: 'session_roster_changed', ...payload });
1443
1456
  }
1444
1457
 
1445
1458
  function groupErrorPayload(err) {
1446
1459
  let code = 'unknown';
1447
- if (err instanceof GroupCrudError) code = err.code;
1448
- else if (err instanceof GroupConfigError) code = err.code;
1460
+ if (err instanceof SessionCrudError) code = err.code;
1461
+ else if (err instanceof SessionConfigError) code = err.code;
1449
1462
  return {
1450
1463
  code,
1451
- groupId: err && err.groupId,
1464
+ sessionId: err && err.sessionId,
1452
1465
  message: err && err.message,
1453
1466
  };
1454
1467
  }
1455
1468
 
1456
- export function handleYeaftListGroups(msg) {
1469
+ export function handleYeaftListSessions(msg) {
1457
1470
  const requestId = msg && msg.requestId;
1458
1471
  try {
1459
1472
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1460
- const groups = snapshotGroups(yeaftDir);
1473
+ const groups = snapshotSessions(yeaftDir);
1461
1474
  sendGroupCrudResult({ op: 'list', requestId, ok: true, groups });
1462
1475
  } catch (err) {
1463
1476
  sendGroupCrudResult({ op: 'list', requestId, ok: false, error: groupErrorPayload(err) });
1464
1477
  }
1465
1478
  }
1466
1479
 
1467
- export function handleYeaftCreateGroup(msg) {
1480
+ export function handleYeaftCreateSession(msg) {
1468
1481
  const requestId = msg && msg.requestId;
1469
1482
  const payload = (msg && msg.payload) || {};
1470
1483
  try {
1471
1484
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1472
- const group = createGroupFromSpec(yeaftDir, payload);
1485
+ const group = createSessionFromSpec(yeaftDir, payload);
1473
1486
  sendGroupCrudResult({ op: 'create', requestId, ok: true, group });
1474
1487
  sendGroupSnapshotBroadcast();
1475
1488
  } catch (err) {
@@ -1477,14 +1490,14 @@ export function handleYeaftCreateGroup(msg) {
1477
1490
  }
1478
1491
  }
1479
1492
 
1480
- export function handleYeaftRenameGroup(msg) {
1493
+ export function handleYeaftRenameSession(msg) {
1481
1494
  const requestId = msg && msg.requestId;
1482
- const groupId = msg && msg.groupId;
1495
+ const sessionId = msg && msg.sessionId;
1483
1496
  const name = msg && msg.name;
1484
1497
  try {
1485
1498
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1486
- const group = renameGroup(yeaftDir, groupId, name);
1487
- invalidateGroupContext(groupId);
1499
+ const group = renameSession(yeaftDir, sessionId, name);
1500
+ invalidateGroupContext(sessionId);
1488
1501
  sendGroupCrudResult({ op: 'rename', requestId, ok: true, group });
1489
1502
  sendGroupSnapshotBroadcast();
1490
1503
  } catch (err) {
@@ -1496,7 +1509,7 @@ export function handleYeaftRenameGroup(msg) {
1496
1509
  * `yeaft_update_group` — generalised group meta patch. Currently accepts
1497
1510
  * `name` and `announcement` keys. Empty patch is rejected; an empty/
1498
1511
  * whitespace-only `name` is also rejected up front rather than letting
1499
- * `renameGroup` raise a less-specific error deeper in the call stack.
1512
+ * `renameSession` raise a less-specific error deeper in the call stack.
1500
1513
  *
1501
1514
  * Partial-success contract: when a single patch contains BOTH `name` and
1502
1515
  * `announcement`, the rename is committed first; if the announcement
@@ -1506,25 +1519,25 @@ export function handleYeaftRenameGroup(msg) {
1506
1519
  * so this is theoretical; readers extending the patch shape should know
1507
1520
  * the contract permits half-commits.
1508
1521
  */
1509
- export function handleYeaftUpdateGroup(msg) {
1522
+ export function handleYeaftUpdateSession(msg) {
1510
1523
  const requestId = msg && msg.requestId;
1511
- const groupId = msg && msg.groupId;
1524
+ const sessionId = msg && msg.sessionId;
1512
1525
  const patch = (msg && msg.patch && typeof msg.patch === 'object') ? msg.patch : null;
1513
1526
  try {
1514
1527
  const hasName = patch && typeof patch.name === 'string' && patch.name.trim().length > 0;
1515
1528
  const hasAnnouncement = patch && typeof patch.announcement === 'string';
1516
1529
  if (!patch || (!hasName && !hasAnnouncement)) {
1517
- throw new GroupCrudError('invalid_patch', groupId);
1530
+ throw new SessionCrudError('invalid_patch', sessionId);
1518
1531
  }
1519
1532
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1520
1533
  let group = null;
1521
1534
  if (hasName) {
1522
- group = renameGroup(yeaftDir, groupId, patch.name);
1535
+ group = renameSession(yeaftDir, sessionId, patch.name);
1523
1536
  }
1524
1537
  if (hasAnnouncement) {
1525
- group = updateGroupAnnouncement(yeaftDir, groupId, patch.announcement);
1538
+ group = updateSessionAnnouncement(yeaftDir, sessionId, patch.announcement);
1526
1539
  }
1527
- invalidateGroupContext(groupId);
1540
+ invalidateGroupContext(sessionId);
1528
1541
  sendGroupCrudResult({ op: 'update', requestId, ok: true, group });
1529
1542
  sendGroupSnapshotBroadcast();
1530
1543
  } catch (err) {
@@ -1534,54 +1547,54 @@ export function handleYeaftUpdateGroup(msg) {
1534
1547
 
1535
1548
  /**
1536
1549
  * Persist the model selected in the group conversation header. Cache invalidation:
1537
- * drop every cached Engine whose key starts with `${groupId}::` so the
1550
+ * drop every cached Engine whose key starts with `${sessionId}::` so the
1538
1551
  * next turn picks up the new model. The group meta itself is untouched.
1539
1552
  *
1540
- * Payload: { groupId, requestId, config: { model?: string|null } }
1553
+ * Payload: { sessionId, requestId, config: { model?: string|null } }
1541
1554
  * - `model: ''` or `null` clears the selected group model (falls back to user default).
1542
1555
  */
1543
- export function handleYeaftUpdateGroupConfig(msg) {
1556
+ export function handleYeaftUpdateSessionConfig(msg) {
1544
1557
  const requestId = msg && msg.requestId;
1545
- const groupId = msg && msg.groupId;
1558
+ const sessionId = msg && msg.sessionId;
1546
1559
  const partial = (msg && msg.config && typeof msg.config === 'object') ? msg.config : null;
1547
1560
  try {
1548
- if (!groupId) throw new GroupConfigError('missing_group_id', 'groupId required');
1549
- if (!partial) throw new GroupConfigError('invalid_patch', 'config object required');
1561
+ if (!sessionId) throw new SessionConfigError('missing_group_id', 'sessionId required');
1562
+ if (!partial) throw new SessionConfigError('invalid_patch', 'config object required');
1550
1563
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1551
- const savedConfig = updateGroupConfig(yeaftDir, groupId, partial);
1564
+ const savedConfig = updateSessionConfig(yeaftDir, sessionId, partial);
1552
1565
  // Drop cached engines so the next VP turn rebuilds with the new model.
1553
- const prefix = `${groupId}::`;
1566
+ const prefix = `${sessionId}::`;
1554
1567
  for (const k of Array.from(vpEngines.keys())) {
1555
1568
  if (k.startsWith(prefix)) vpEngines.delete(k);
1556
1569
  }
1557
- invalidateGroupContext(groupId);
1558
- sendGroupCrudResult({ op: 'update_config', requestId, ok: true, groupId, config: savedConfig });
1570
+ invalidateGroupContext(sessionId);
1571
+ sendGroupCrudResult({ op: 'update_config', requestId, ok: true, sessionId, config: savedConfig });
1559
1572
  sendGroupSnapshotBroadcast();
1560
1573
  } catch (err) {
1561
1574
  sendGroupCrudResult({ op: 'update_config', requestId, ok: false, error: groupErrorPayload(err) });
1562
1575
  }
1563
1576
  }
1564
1577
 
1565
- export function handleYeaftArchiveGroup(msg) {
1578
+ export function handleYeaftArchiveSession(msg) {
1566
1579
  const requestId = msg && msg.requestId;
1567
- const groupId = msg && msg.groupId;
1580
+ const sessionId = msg && msg.sessionId;
1568
1581
  try {
1569
1582
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1570
- const result = archiveGroup(yeaftDir, groupId);
1571
- invalidateGroupContext(groupId);
1572
- sendGroupCrudResult({ op: 'archive', requestId, ok: true, groupId: result.groupId });
1583
+ const result = archiveSession(yeaftDir, sessionId);
1584
+ invalidateGroupContext(sessionId);
1585
+ sendGroupCrudResult({ op: 'archive', requestId, ok: true, sessionId: result.sessionId });
1573
1586
  sendGroupSnapshotBroadcast();
1574
1587
  } catch (err) {
1575
1588
  sendGroupCrudResult({ op: 'archive', requestId, ok: false, error: groupErrorPayload(err) });
1576
1589
  }
1577
1590
  }
1578
1591
 
1579
- export function handleYeaftDeleteGroup(msg) {
1592
+ export function handleYeaftDeleteSession(msg) {
1580
1593
  const requestId = msg && msg.requestId;
1581
- const groupId = msg && msg.groupId;
1594
+ const sessionId = msg && msg.sessionId;
1582
1595
  try {
1583
1596
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1584
- const result = deleteGroup(yeaftDir, groupId);
1597
+ const result = deleteSession(yeaftDir, sessionId);
1585
1598
  // Cascade: remove every persisted message stamped with this group id.
1586
1599
  // Hard delete (per user spec): no soft-archive, the bytes are gone.
1587
1600
  // Skipped silently if the session/store isn't initialized — the next
@@ -1589,17 +1602,17 @@ export function handleYeaftDeleteGroup(msg) {
1589
1602
  let messagesRemoved = 0;
1590
1603
  try {
1591
1604
  if (session && session.conversationStore) {
1592
- messagesRemoved = session.conversationStore.deleteByGroup(groupId);
1605
+ messagesRemoved = session.conversationStore.deleteByGroup(sessionId);
1593
1606
  }
1594
1607
  } catch (cascadeErr) {
1595
- console.warn(`[Yeaft] cascade delete for group ${groupId} failed: ${cascadeErr.message}`);
1608
+ console.warn(`[Yeaft] cascade delete for group ${sessionId} failed: ${cascadeErr.message}`);
1596
1609
  }
1597
1610
  // Drop the cached coord/router and abort/clear any in-flight VP
1598
1611
  // turns for the deleted group. Engines for the deleted group are
1599
1612
  // also dropped — unlike rename/announcement updates, the group is
1600
1613
  // gone for good and there's nothing to preserve.
1601
- invalidateGroupContext(groupId);
1602
- const prefix = `${groupId}::`;
1614
+ invalidateGroupContext(sessionId);
1615
+ const prefix = `${sessionId}::`;
1603
1616
  for (const k of Array.from(vpEngines.keys())) {
1604
1617
  if (k.startsWith(prefix)) vpEngines.delete(k);
1605
1618
  }
@@ -1607,7 +1620,7 @@ export function handleYeaftDeleteGroup(msg) {
1607
1620
  op: 'delete',
1608
1621
  requestId,
1609
1622
  ok: true,
1610
- groupId: result.groupId,
1623
+ sessionId: result.sessionId,
1611
1624
  messagesRemoved,
1612
1625
  });
1613
1626
  sendGroupSnapshotBroadcast();
@@ -1616,14 +1629,14 @@ export function handleYeaftDeleteGroup(msg) {
1616
1629
  }
1617
1630
  }
1618
1631
 
1619
- export function handleYeaftAddMember(msg) {
1632
+ export function handleYeaftSessionAddMember(msg) {
1620
1633
  const requestId = msg && msg.requestId;
1621
- const groupId = msg && msg.groupId;
1634
+ const sessionId = msg && msg.sessionId;
1622
1635
  const vpId = msg && msg.vpId;
1623
1636
  try {
1624
1637
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1625
- const group = addMember(yeaftDir, groupId, vpId);
1626
- invalidateGroupContext(groupId);
1638
+ const group = addMember(yeaftDir, sessionId, vpId);
1639
+ invalidateGroupContext(sessionId);
1627
1640
  sendGroupCrudResult({ op: 'add_member', requestId, ok: true, group });
1628
1641
  sendGroupRosterChanged(group);
1629
1642
  } catch (err) {
@@ -1631,17 +1644,17 @@ export function handleYeaftAddMember(msg) {
1631
1644
  }
1632
1645
  }
1633
1646
 
1634
- export function handleYeaftRemoveMember(msg) {
1647
+ export function handleYeaftSessionRemoveMember(msg) {
1635
1648
  const requestId = msg && msg.requestId;
1636
- const groupId = msg && msg.groupId;
1649
+ const sessionId = msg && msg.sessionId;
1637
1650
  const vpId = msg && msg.vpId;
1638
1651
  try {
1639
1652
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1640
- const group = removeMember(yeaftDir, groupId, vpId);
1641
- invalidateGroupContext(groupId);
1653
+ const group = removeMember(yeaftDir, sessionId, vpId);
1654
+ invalidateGroupContext(sessionId);
1642
1655
  // Also drop the kicked VP's thread engines — the next time they're
1643
1656
  // added back they should start with fresh per-thread state.
1644
- const removedPrefix = `${groupId}::${vpId}::`;
1657
+ const removedPrefix = `${sessionId}::${vpId}::`;
1645
1658
  for (const key of Array.from(vpEngines.keys())) {
1646
1659
  if (key.startsWith(removedPrefix)) vpEngines.delete(key);
1647
1660
  }
@@ -1652,14 +1665,14 @@ export function handleYeaftRemoveMember(msg) {
1652
1665
  }
1653
1666
  }
1654
1667
 
1655
- export function handleYeaftSetDefaultVp(msg) {
1668
+ export function handleYeaftSessionSetDefaultVp(msg) {
1656
1669
  const requestId = msg && msg.requestId;
1657
- const groupId = msg && msg.groupId;
1670
+ const sessionId = msg && msg.sessionId;
1658
1671
  const vpId = msg && msg.vpId;
1659
1672
  try {
1660
1673
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1661
- const group = setGroupDefaultVp(yeaftDir, groupId, vpId);
1662
- invalidateGroupContext(groupId);
1674
+ const group = setSessionDefaultVp(yeaftDir, sessionId, vpId);
1675
+ invalidateGroupContext(sessionId);
1663
1676
  sendGroupCrudResult({ op: 'set_default_vp', requestId, ok: true, group });
1664
1677
  sendGroupRosterChanged(group);
1665
1678
  } catch (err) {
@@ -1709,14 +1722,14 @@ export function installYeaftRuntimeBridge(s) {
1709
1722
  //
1710
1723
  // Group-id stamping is NO LONGER done here. It used to be: this sink
1711
1724
  // read a module-level `activeScopedDreamGroupId` that
1712
- // `handleYeaftDreamTrigger({groupId})` parked before awaiting the
1725
+ // `handleYeaftDreamTrigger({sessionId})` parked before awaiting the
1713
1726
  // scope-filtered pass. That created a race when two scoped triggers
1714
1727
  // overlapped (auto-tick during a manual click; or two manual clicks
1715
1728
  // for different groups): the second handler's `finally` could clear
1716
1729
  // the module slot while the first run was still emitting events,
1717
1730
  // dropping the stamp from the tail of the first pass. The new design:
1718
1731
  // `handleYeaftDreamTrigger` wraps THIS sink for the lifetime of the
1719
- // trigger to inject `groupId` per-call (see that function below). The
1732
+ // trigger to inject `sessionId` per-call (see that function below). The
1720
1733
  // base sink is intentionally a pure passthrough.
1721
1734
  //
1722
1735
  // Bug 2: also forward turn_open / turn_close / loop events emitted by
@@ -1724,11 +1737,11 @@ export function installYeaftRuntimeBridge(s) {
1724
1737
  s._dreamProgressSink = (evt) => {
1725
1738
  try {
1726
1739
  if (evt.type === 'turn_open' || evt.type === 'turn_close' || evt.type === 'loop') {
1727
- const tag = evt && evt.groupId ? { groupId: evt.groupId } : {};
1740
+ const tag = evt && evt.sessionId ? { sessionId: evt.sessionId } : {};
1728
1741
  sendYeaftEvent(evt, tag);
1729
1742
  } else {
1730
1743
  const out = { type: 'dream_progress', ...evt };
1731
- const tag = evt && evt.groupId ? { groupId: evt.groupId } : {};
1744
+ const tag = evt && evt.sessionId ? { sessionId: evt.sessionId } : {};
1732
1745
  sendYeaftEvent(out, tag);
1733
1746
  }
1734
1747
  } catch { /* never let event delivery throw */ }
@@ -1740,7 +1753,7 @@ export function installYeaftRuntimeBridge(s) {
1740
1753
  // present. Per-group `yeaft_history_compacted` events fire after a
1741
1754
  // successful summarize+swap.
1742
1755
  if (s.compactor && typeof s.compactor.setOnCompacted === 'function') {
1743
- s.compactor.setOnCompacted((groupId, result) => {
1756
+ s.compactor.setOnCompacted((sessionId, result) => {
1744
1757
  try {
1745
1758
  sendYeaftEvent({
1746
1759
  type: 'yeaft_history_compacted',
@@ -1751,7 +1764,7 @@ export function installYeaftRuntimeBridge(s) {
1751
1764
  afterTokens: result?.afterTokens,
1752
1765
  archivedCount: result?.archivedCount,
1753
1766
  ts: Date.now(),
1754
- }, { groupId });
1767
+ }, { sessionId });
1755
1768
  } catch { /* WS pipeline failure must not crash compact */ }
1756
1769
  });
1757
1770
  }
@@ -1769,7 +1782,7 @@ export function installYeaftRuntimeBridge(s) {
1769
1782
 
1770
1783
  /**
1771
1784
  * Mid-turn vp-status transitions (text_delta / tool_call / tool_end).
1772
- * Tolerates `hctx` missing groupId/vpId — pre-707 1:1 chat paths don't
1785
+ * Tolerates `hctx` missing sessionId/vpId — pre-707 1:1 chat paths don't
1773
1786
  * have either; they're tracked as the default broker key but the
1774
1787
  * frontend ignores rows it doesn't recognize.
1775
1788
  *
@@ -1784,7 +1797,7 @@ function maybeTransitionVpStatus(hctx, state) {
1784
1797
  hctx.thread.updatedAt = Date.now();
1785
1798
  }
1786
1799
  getVpStatusBroker().transition({
1787
- groupId: hctx.groupId || null,
1800
+ sessionId: hctx.sessionId || null,
1788
1801
  vpId: hctx.vpId,
1789
1802
  state,
1790
1803
  turnId: hctx.turnId || null,
@@ -1803,7 +1816,7 @@ function maybeTransitionVpStatus(hctx, state) {
1803
1816
  * todos, debug cards, and persistence all share the same boundary.
1804
1817
  *
1805
1818
  * @param {object} event — engine event (text_delta / tool_call / …)
1806
- * @param {{assistantTextParts:string[], toolCallsAccum:Array, toolResultsAccum:Array, thinkingBlocksAccum?:Array, resetQueryTimer:Function, groupId?:string, vpId?:string, turnId?:string}} hctx
1819
+ * @param {{assistantTextParts:string[], toolCallsAccum:Array, toolResultsAccum:Array, thinkingBlocksAccum?:Array, resetQueryTimer:Function, sessionId?:string, vpId?:string, turnId?:string}} hctx
1807
1820
  */
1808
1821
  export function __testHandleEngineEvent(event, hctx) {
1809
1822
  return handleEngineEvent(event, hctx);
@@ -1812,7 +1825,7 @@ export function __testHandleEngineEvent(event, hctx) {
1812
1825
  function handleEngineEvent(event, hctx) {
1813
1826
  hctx.resetQueryTimer();
1814
1827
  const envelope = {
1815
- groupId: hctx.groupId,
1828
+ sessionId: hctx.sessionId,
1816
1829
  vpId: hctx.vpId,
1817
1830
  turnId: hctx.turnId,
1818
1831
  threadId: hctx.threadId || event.threadId,
@@ -1942,7 +1955,7 @@ function handleEngineEvent(event, hctx) {
1942
1955
  hctx.thread.updatedAt = Date.now();
1943
1956
  }
1944
1957
  getVpStatusBroker().settleIdle({
1945
- groupId: hctx.groupId || null,
1958
+ sessionId: hctx.sessionId || null,
1946
1959
  vpId: hctx.vpId,
1947
1960
  threadId: hctx.threadId || 'main',
1948
1961
  title: hctx.thread?.title || '',
@@ -1953,7 +1966,7 @@ function handleEngineEvent(event, hctx) {
1953
1966
  }
1954
1967
  sendYeaftEvent({
1955
1968
  type: 'vp_turn_end',
1956
- groupId: hctx.groupId,
1969
+ sessionId: hctx.sessionId,
1957
1970
  vpId: hctx.vpId,
1958
1971
  threadId: hctx.threadId || event.threadId || 'main',
1959
1972
  turnId: hctx.turnId,
@@ -1983,7 +1996,7 @@ function handleEngineEvent(event, hctx) {
1983
1996
  case 'consolidate':
1984
1997
  // Engine compressed the context — clear THIS group's accumulated
1985
1998
  // history. Other groups' histories stay intact.
1986
- if (hctx.groupId) setGroupHistory(hctx.groupId, []);
1999
+ if (hctx.sessionId) setGroupHistory(hctx.sessionId, []);
1987
2000
  sendYeaftEvent({
1988
2001
  type: 'consolidate',
1989
2002
  archivedCount: event.archivedCount,
@@ -2023,7 +2036,7 @@ function handleEngineEvent(event, hctx) {
2023
2036
  turnId: event.turnId,
2024
2037
  userPrompt: event.userPrompt,
2025
2038
  vpId: event.vpId,
2026
- groupId: event.groupId,
2039
+ sessionId: event.sessionId,
2027
2040
  at: event.at,
2028
2041
  }, envelope);
2029
2042
  break;
@@ -2142,19 +2155,19 @@ function handleEngineEvent(event, hctx) {
2142
2155
  * conversation entry point.
2143
2156
  *
2144
2157
  * Contract (post-consolidation, was previously split between handleYeaftChat
2145
- * and handleYeaftGroupChat):
2158
+ * and handleYeaftSessionSend):
2146
2159
  * - Frontend ALWAYS sends `yeaft_group_chat`. There is no `yeaft_chat`.
2147
- * - `groupId` defaults to `'grp_default'` if missing — Yeaft is a single
2160
+ * - `sessionId` defaults to `'grp_default'` if missing — Yeaft is a single
2148
2161
  * conversation backed by the default group; the user is never "outside"
2149
2162
  * a group.
2150
2163
  * - If the group dir doesn't exist and the resolved id is `'grp_default'`,
2151
- * it is seeded on the fly. Any other unknown groupId surfaces an error.
2164
+ * it is seeded on the fly. Any other unknown sessionId surfaces an error.
2152
2165
  * - Coordinator is MANDATORY (this is what guarantees ctx.router is wired
2153
2166
  * so the `route_forward` tool can never trip `router_unavailable`).
2154
2167
  * - No legacy "no-group" fallback paths — they were the source of the
2155
2168
  * router_unavailable bug fixed in v0.1.671.
2156
2169
  */
2157
- export async function handleYeaftGroupChat(msg) {
2170
+ export async function handleYeaftSessionSend(msg) {
2158
2171
  if (!msg || typeof msg !== 'object') return;
2159
2172
  const { text } = msg;
2160
2173
  // PR #721: image-only send is allowed — text may be empty when the
@@ -2165,8 +2178,8 @@ export async function handleYeaftGroupChat(msg) {
2165
2178
  const hasFiles = Array.isArray(msg.files) && msg.files.length > 0;
2166
2179
  if (!text?.trim() && !hasFiles) return;
2167
2180
  const mentions = Array.isArray(msg.mentions) ? msg.mentions : [];
2168
- const groupId = (typeof msg.groupId === 'string' && msg.groupId.trim())
2169
- ? msg.groupId.trim()
2181
+ const sessionId = (typeof msg.sessionId === 'string' && msg.sessionId.trim())
2182
+ ? msg.sessionId.trim()
2170
2183
  : 'grp_default';
2171
2184
 
2172
2185
  // Entry gate: if a compact is in flight from the previous turn IN
@@ -2178,7 +2191,7 @@ export async function handleYeaftGroupChat(msg) {
2178
2191
  // a session has loaded (or in test paths that never call
2179
2192
  // `ensureSessionLoaded`) it may be unavailable; skip gracefully.
2180
2193
  if (session?.compactor) {
2181
- await session.compactor.awaitInFlight(groupId);
2194
+ await session.compactor.awaitInFlight(sessionId);
2182
2195
  }
2183
2196
 
2184
2197
  // yeaftDir is a hard prerequisite for both session boot and group seeding;
@@ -2189,8 +2202,8 @@ export async function handleYeaftGroupChat(msg) {
2189
2202
  sendYeaftOutput({
2190
2203
  type: 'assistant',
2191
2204
  message: { content: [{ type: 'text', text: '⚠️ Yeaft session error: no yeaft directory configured.' }] },
2192
- }, { groupId });
2193
- sendYeaftOutput({ type: 'result', result_text: '' }, { groupId });
2205
+ }, { sessionId });
2206
+ sendYeaftOutput({ type: 'result', result_text: '' }, { sessionId });
2194
2207
  return;
2195
2208
  }
2196
2209
 
@@ -2199,46 +2212,46 @@ export async function handleYeaftGroupChat(msg) {
2199
2212
  // Open the group; seed grp_default on the fly if absent. Track
2200
2213
  // seedFailed separately so a seed crash surfaces a different message
2201
2214
  // than a genuinely-missing group.
2202
- let groupHandle = null;
2203
- let groupRoot = null;
2215
+ let sessionHandle = null;
2216
+ let sessionRoot = null;
2204
2217
  let seedFailed = false;
2205
2218
  try {
2206
- const groupYeaftDir = resolveGroupYeaftDir(yeaftDir, groupId);
2207
- groupRoot = groupsRoot(groupYeaftDir);
2208
- const dir = join(groupRoot, groupId);
2209
- if (existsSync(dir) && loadGroupMeta(dir)) {
2210
- groupHandle = openGroup(groupRoot, groupId);
2211
- } else if (groupId === 'grp_default') {
2219
+ const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
2220
+ sessionRoot = sessionsRoot(groupYeaftDir);
2221
+ const dir = join(sessionRoot, sessionId);
2222
+ if (existsSync(dir) && loadSessionMeta(dir)) {
2223
+ sessionHandle = openSession(sessionRoot, sessionId);
2224
+ } else if (sessionId === 'grp_default') {
2212
2225
  try {
2213
- const seeded = seedDefaultGroup(groupYeaftDir, { memoryRoot: join(groupYeaftDir, 'memory') });
2214
- groupHandle = seeded.group;
2226
+ const seeded = seedDefaultSession(groupYeaftDir, { memoryRoot: join(groupYeaftDir, 'memory') });
2227
+ sessionHandle = seeded.group;
2215
2228
  } catch (seedErr) {
2216
2229
  seedFailed = true;
2217
- console.warn('[Yeaft] yeaft_group_chat: seedDefaultGroup failed', seedErr?.message || seedErr);
2230
+ console.warn('[Yeaft] yeaft_group_chat: seedDefaultSession failed', seedErr?.message || seedErr);
2218
2231
  }
2219
2232
  } else {
2220
- console.warn('[Yeaft] yeaft_group_chat: groupId %s not found', groupId);
2233
+ console.warn('[Yeaft] yeaft_group_chat: sessionId %s not found', sessionId);
2221
2234
  }
2222
2235
  } catch (err) {
2223
2236
  console.warn('[Yeaft] yeaft_group_chat: group open failed', err?.message || err);
2224
2237
  }
2225
2238
 
2226
- if (!groupHandle) {
2239
+ if (!sessionHandle) {
2227
2240
  const errText = seedFailed
2228
- ? `⚠️ Failed to seed default group ${groupId} — check group .yeaft permissions.`
2229
- : `⚠️ Group ${groupId} not found.`;
2241
+ ? `⚠️ Failed to seed default group ${sessionId} — check group .yeaft permissions.`
2242
+ : `⚠️ Group ${sessionId} not found.`;
2230
2243
  sendYeaftOutput({
2231
2244
  type: 'assistant',
2232
2245
  message: { content: [{ type: 'text', text: errText }] },
2233
- }, { groupId });
2234
- sendYeaftOutput({ type: 'result', result_text: '' }, { groupId });
2246
+ }, { sessionId });
2247
+ sendYeaftOutput({ type: 'result', result_text: '' }, { sessionId });
2235
2248
  return;
2236
2249
  }
2237
2250
 
2238
2251
  // Auto-add @-mentioned VPs from the library, heal missing defaultVpId.
2239
2252
  let rosterMutated = false;
2240
2253
  try {
2241
- const meta = groupHandle.getMeta();
2254
+ const meta = sessionHandle.getMeta();
2242
2255
  const wantsAdd = mentions.filter(
2243
2256
  (m) => m && m !== 'all' && !meta.roster.includes(m)
2244
2257
  );
@@ -2247,23 +2260,23 @@ export async function handleYeaftGroupChat(msg) {
2247
2260
  try {
2248
2261
  const vp = readVp(vpId);
2249
2262
  if (!vp) continue;
2250
- addMember(yeaftDir, groupId, vpId);
2263
+ addMember(yeaftDir, sessionId, vpId);
2251
2264
  rosterMutated = true;
2252
2265
  } catch { /* skip strangers */ }
2253
2266
  }
2254
2267
  if (rosterMutated) {
2255
- try { groupHandle.close && groupHandle.close(); } catch { /* best-effort */ }
2256
- groupHandle = openGroup(groupRoot, groupId);
2257
- sendGroupRosterChanged(groupHandle.getMeta());
2268
+ try { sessionHandle.close && sessionHandle.close(); } catch { /* best-effort */ }
2269
+ sessionHandle = openSession(sessionRoot, sessionId);
2270
+ sendGroupRosterChanged(sessionHandle.getMeta());
2258
2271
  }
2259
2272
  }
2260
- const meta2 = groupHandle.getMeta();
2273
+ const meta2 = sessionHandle.getMeta();
2261
2274
  if (!meta2.defaultVpId && meta2.roster.length) {
2262
2275
  try {
2263
- setGroupDefaultVp(yeaftDir, groupId, meta2.roster[0]);
2264
- try { groupHandle.close && groupHandle.close(); } catch { /* best-effort */ }
2265
- groupHandle = openGroup(groupRoot, groupId);
2266
- sendGroupRosterChanged(groupHandle.getMeta());
2276
+ setSessionDefaultVp(yeaftDir, sessionId, meta2.roster[0]);
2277
+ try { sessionHandle.close && sessionHandle.close(); } catch { /* best-effort */ }
2278
+ sessionHandle = openSession(sessionRoot, sessionId);
2279
+ sendGroupRosterChanged(sessionHandle.getMeta());
2267
2280
  rosterMutated = true;
2268
2281
  } catch { /* best-effort */ }
2269
2282
  }
@@ -2272,15 +2285,15 @@ export async function handleYeaftGroupChat(msg) {
2272
2285
  }
2273
2286
 
2274
2287
  // task-707: per-group persistent coordinator/router. Created once per
2275
- // groupId; reused across user messages AND across in-flight tool calls
2288
+ // sessionId; reused across user messages AND across in-flight tool calls
2276
2289
  // (route_forward delivers via this same coord). If the roster mutated
2277
2290
  // we replace the cached coord so it points at the freshly-opened
2278
- // groupHandle.
2291
+ // sessionHandle.
2279
2292
  if (rosterMutated) {
2280
- groupContexts.delete(groupId);
2293
+ groupContexts.delete(sessionId);
2281
2294
  }
2282
- const groupCtx = getOrCreateGroupContext(groupId, groupHandle);
2283
- const coord = groupCtx.coord;
2295
+ const sessionCtx = getOrCreateSessionContext(sessionId, sessionHandle);
2296
+ const coord = sessionCtx.coord;
2284
2297
 
2285
2298
  // Multi-thread routing owns active-VP decisions. Do not abort an active
2286
2299
  // VP before classification: a new query may append to the running thread
@@ -2299,7 +2312,7 @@ export async function handleYeaftGroupChat(msg) {
2299
2312
  let attachmentBundle = { promptAttachments: [], promptSuffix: '', promptParts: [], failed: [] };
2300
2313
  if (inboundFiles.length > 0) {
2301
2314
  try {
2302
- attachmentBundle = persistYeaftAttachments(inboundFiles, { subdir: groupId });
2315
+ attachmentBundle = persistYeaftAttachments(inboundFiles, { subdir: sessionId });
2303
2316
  } catch (err) {
2304
2317
  console.warn('[Yeaft] yeaft_group_chat: attachment persist failed', err?.message || err);
2305
2318
  }
@@ -2314,7 +2327,7 @@ export async function handleYeaftGroupChat(msg) {
2314
2327
  sendYeaftOutput({
2315
2328
  type: 'assistant',
2316
2329
  message: { content: [{ type: 'text', text: `⚠️ ${attachmentBundle.failed.length} file(s) could not be attached:\n${detail}` }] },
2317
- }, { groupId });
2330
+ }, { sessionId });
2318
2331
  }
2319
2332
  const persistedAttachments = attachmentsForPersistence(attachmentBundle.promptAttachments);
2320
2333
 
@@ -2344,8 +2357,8 @@ export async function handleYeaftGroupChat(msg) {
2344
2357
  sendYeaftOutput({
2345
2358
  type: 'assistant',
2346
2359
  message: { content: [{ type: 'text', text: `⚠️ Group dispatch error: ${err?.message || err}` }] },
2347
- }, { groupId });
2348
- sendYeaftOutput({ type: 'result', result_text: '' }, { groupId });
2360
+ }, { sessionId });
2361
+ sendYeaftOutput({ type: 'result', result_text: '' }, { sessionId });
2349
2362
  return;
2350
2363
  }
2351
2364
 
@@ -2365,8 +2378,8 @@ export async function handleYeaftGroupChat(msg) {
2365
2378
  sendYeaftOutput({
2366
2379
  type: 'assistant',
2367
2380
  message: { content: [{ type: 'text', text: '⚠️ No VP available to respond — check the group roster.' }] },
2368
- }, { groupId });
2369
- sendYeaftOutput({ type: 'result', result_text: '' }, { groupId });
2381
+ }, { sessionId });
2382
+ sendYeaftOutput({ type: 'result', result_text: '' }, { sessionId });
2370
2383
  return;
2371
2384
  }
2372
2385
 
@@ -2404,33 +2417,33 @@ async function waitForVpDrivers(_groupId, driverKeys = []) {
2404
2417
  *
2405
2418
  * @param {object} args
2406
2419
  * @param {string} args.vpId
2407
- * @param {object} args.groupCoordinator — the persistent coordinator for the
2420
+ * @param {object} args.sessionCoordinator — the persistent coordinator for the
2408
2421
  * group; used here for `group.getMeta()` (defaultVpId, announcement) and
2409
2422
  * to bind the per-group router into toolCtx.
2410
- * @param {string} [args.groupId]
2423
+ * @param {string} [args.sessionId]
2411
2424
  * @param {object} [args.envelope] — the inbound coordinator envelope that
2412
2425
  * triggered this turn. Threaded into toolCtx as `inboundEnvelope` so
2413
2426
  * `route_forward` can extend `causedBy` chains correctly. Optional only
2414
2427
  * for pre-707 callers that no longer exist in production.
2415
2428
  */
2416
- export function buildVpQueryOpts({ vpId, groupCoordinator, groupId, envelope, threadId = 'main' }) {
2429
+ export function buildVpQueryOpts({ vpId, sessionCoordinator, sessionId, envelope, threadId = 'main' }) {
2417
2430
  // Read the group meta once and reuse for both defaultVpId fallback and
2418
2431
  // announcement injection. Each .getMeta() reload reads + parses the
2419
2432
  // group.json file, so calling it twice per turn is wasteful — and
2420
2433
  // (more importantly) opens a window where a concurrent group edit
2421
2434
  // could land between the two reads, giving the engine a defaultVpId
2422
2435
  // from one snapshot and an announcement from a newer one.
2423
- let groupMeta = null;
2436
+ let sessionMeta = null;
2424
2437
  try {
2425
- groupMeta = groupCoordinator && groupCoordinator.group
2426
- && typeof groupCoordinator.group.getMeta === 'function'
2427
- ? groupCoordinator.group.getMeta() : null;
2438
+ sessionMeta = sessionCoordinator && sessionCoordinator.group
2439
+ && typeof sessionCoordinator.group.getMeta === 'function'
2440
+ ? sessionCoordinator.group.getMeta() : null;
2428
2441
  } catch { /* coordinator inspection is best-effort */ }
2429
2442
 
2430
2443
  let resolvedVpId = vpId;
2431
2444
  if (!resolvedVpId) {
2432
- if (groupMeta && typeof groupMeta.defaultVpId === 'string' && groupMeta.defaultVpId) {
2433
- resolvedVpId = groupMeta.defaultVpId;
2445
+ if (sessionMeta && typeof sessionMeta.defaultVpId === 'string' && sessionMeta.defaultVpId) {
2446
+ resolvedVpId = sessionMeta.defaultVpId;
2434
2447
  }
2435
2448
  }
2436
2449
  if (!resolvedVpId) {
@@ -2450,27 +2463,27 @@ export function buildVpQueryOpts({ vpId, groupCoordinator, groupId, envelope, th
2450
2463
  if (!resolvedVpId) return undefined;
2451
2464
 
2452
2465
  const out = { senderVpId: resolvedVpId, threadId: threadId || 'main' };
2453
- if (typeof groupId === 'string' && groupId.trim()) {
2454
- out.groupId = groupId.trim();
2466
+ if (typeof sessionId === 'string' && sessionId.trim()) {
2467
+ out.sessionId = sessionId.trim();
2455
2468
  }
2456
2469
  // task-334-group-editor: surface the group announcement to the engine so
2457
2470
  // buildWorkerPrompt can inject it as a CLAUDE.md-style shared prefix.
2458
2471
  // Empty/missing reads as '' and prompts.js skips the section.
2459
- if (groupMeta && typeof groupMeta.announcement === 'string') {
2460
- out.groupAnnouncement = groupMeta.announcement;
2472
+ if (sessionMeta && typeof sessionMeta.announcement === 'string') {
2473
+ out.sessionAnnouncement = sessionMeta.announcement;
2461
2474
  }
2462
2475
  // Surface the group's configured working directory so the engine can
2463
2476
  // resolve CLAUDE.md / AGENTS.md at that path and inject it as a
2464
2477
  // [Project Doc] block above the announcement. Groups with no workDir
2465
2478
  // skip the block silently (matches the announcement contract).
2466
- if (groupMeta && typeof groupMeta.workDir === 'string' && groupMeta.workDir.trim()) {
2467
- out.workDir = groupMeta.workDir.trim();
2479
+ if (sessionMeta && typeof sessionMeta.workDir === 'string' && sessionMeta.workDir.trim()) {
2480
+ out.workDir = sessionMeta.workDir.trim();
2468
2481
  }
2469
2482
  const persona = buildVpPersona(resolvedVpId);
2470
2483
  if (persona) out.vpPersona = persona;
2471
- if (groupCoordinator && typeof groupCoordinator.ingest === 'function') {
2484
+ if (sessionCoordinator && typeof sessionCoordinator.ingest === 'function') {
2472
2485
  try {
2473
- out.router = createRouter({ coordinator: groupCoordinator });
2486
+ out.router = createRouter({ coordinator: sessionCoordinator });
2474
2487
  } catch {
2475
2488
  // Router build failure is non-fatal.
2476
2489
  }
@@ -2484,10 +2497,10 @@ export function buildVpQueryOpts({ vpId, groupCoordinator, groupId, envelope, th
2484
2497
  out.inboundEnvelope = envelope;
2485
2498
  }
2486
2499
  // TodoWrite per-thread isolation. Bind closures that read/write a slot
2487
- // keyed by `${groupId}::${vpId}::${threadId}` so concurrent threads for
2500
+ // keyed by `${sessionId}::${vpId}::${threadId}` so concurrent threads for
2488
2501
  // the same VP cannot overwrite each other's lists, and the TodoWrite tool
2489
2502
  // can stay ignorant of routing details (it just calls ctx.setCurrentTodos).
2490
- const todosKey = threadKey(out.groupId || '', resolvedVpId, out.threadId || 'main');
2503
+ const todosKey = threadKey(out.sessionId || '', resolvedVpId, out.threadId || 'main');
2491
2504
  out.getCurrentTodos = () => {
2492
2505
  const cached = vpCurrentTodos.get(todosKey);
2493
2506
  return Array.isArray(cached) ? cached.slice() : null;
@@ -2533,13 +2546,13 @@ async function ensureSessionLoaded() {
2533
2546
  // Bug 8: clean up legacy `.archived-*` group dirs at boot.
2534
2547
  try {
2535
2548
  if (yeaftDir) {
2536
- const removed = purgeArchivedGroups(yeaftDir);
2549
+ const removed = purgeArchivedSessions(yeaftDir);
2537
2550
  if (removed && removed.length > 0) {
2538
2551
  console.log(`[Yeaft] purged ${removed.length} legacy .archived group dir(s)`);
2539
2552
  }
2540
2553
  }
2541
2554
  } catch (err) {
2542
- console.warn('[Yeaft] purgeArchivedGroups failed:', err?.message || err);
2555
+ console.warn('[Yeaft] purgeArchivedSessions failed:', err?.message || err);
2543
2556
  }
2544
2557
 
2545
2558
  yeaftConversationId = `yeaft-${Date.now()}`;
@@ -2597,7 +2610,7 @@ async function ensureSessionLoaded() {
2597
2610
  * helpers) or for adapter implementations that ignore signal.
2598
2611
  */
2599
2612
  async function runVpTurnWithEscalation(args) {
2600
- const { groupId, vpId, turnId, threadId, thread } = args;
2613
+ const { sessionId, vpId, turnId, threadId, thread } = args;
2601
2614
  const deadlineMs = QUERY_TIMEOUT_MS + ESCALATE_AFTER_ABORT_MS;
2602
2615
  await raceWithEscalation(runVpTurn(args), {
2603
2616
  deadlineMs,
@@ -2608,7 +2621,7 @@ async function runVpTurnWithEscalation(args) {
2608
2621
  try {
2609
2622
  sendYeaftOutput(
2610
2623
  { type: 'result', result_text: '', stopped: true },
2611
- { groupId, vpId, turnId, threadId },
2624
+ { sessionId, vpId, turnId, threadId },
2612
2625
  );
2613
2626
  } catch { /* never crash WS pipeline */ }
2614
2627
  // vp-status: when the watchdog escalates, `runVpTurn`'s inner
@@ -2620,7 +2633,7 @@ async function runVpTurnWithEscalation(args) {
2620
2633
  // settling here would re-introduce the "stuck on streaming" bug
2621
2634
  // the whole PR is meant to fix.
2622
2635
  try {
2623
- getVpStatusBroker().settleIdle({ groupId, vpId, threadId: threadId || 'main', title: thread?.title || '' });
2636
+ getVpStatusBroker().settleIdle({ sessionId, vpId, threadId: threadId || 'main', title: thread?.title || '' });
2624
2637
  } catch (err) {
2625
2638
  console.warn('[Yeaft] vp-status settleIdle (escalation) failed:', err?.message || err);
2626
2639
  }
@@ -2683,18 +2696,18 @@ async function raceWithEscalation(inner, { deadlineMs, onEscalate }) {
2683
2696
  * appended to `conversationMessages`.
2684
2697
  *
2685
2698
  * task-707: takes a coordinator `envelope` rather than the coordinator
2686
- * itself; the persistent coord lives in `groupContexts[groupId]`. Uses
2687
- * `getOrCreateVpEngine(groupId, vpId)` so each VP runs against its own
2699
+ * itself; the persistent coord lives in `groupContexts[sessionId]`. Uses
2700
+ * `getOrCreateVpEngine(sessionId, vpId)` so each VP runs against its own
2688
2701
  * Engine instance — private state (`#currentAbortCtrl`, `#__queryCounter`,
2689
2702
  * `#pendingT2`, `#abortReason`, `#adjustRanByGroup`, `#execLog`) does not
2690
2703
  * collide when VP-A and VP-B run concurrent turns.
2691
2704
  *
2692
- * @param {{ prompt: string, groupId: string, vpId: string, turnId: string, envelope: object, vpAbort: AbortController, baseSnapshot: Array }} args
2705
+ * @param {{ prompt: string, sessionId: string, vpId: string, turnId: string, envelope: object, vpAbort: AbortController, baseSnapshot: Array }} args
2693
2706
  */
2694
- async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId = 'main', thread = null, turnId, envelope: inboundEnvelope, vpAbort, baseSnapshot }) {
2707
+ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId = 'main', thread = null, turnId, envelope: inboundEnvelope, vpAbort, baseSnapshot }) {
2695
2708
  if (!prompt?.trim()) return;
2696
2709
 
2697
- const envelope = { groupId, vpId, threadId, turnId };
2710
+ const envelope = { sessionId, vpId, threadId, turnId };
2698
2711
 
2699
2712
  try {
2700
2713
  if (session?.dreamScheduler) {
@@ -2714,10 +2727,10 @@ async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId =
2714
2727
  resetQueryTimer();
2715
2728
 
2716
2729
  // Emit turn_start so frontend can create the message block.
2717
- sendYeaftEvent({ type: 'vp_turn_start', vpId, threadId, turnId, groupId, title: thread?.title || '' }, envelope);
2730
+ sendYeaftEvent({ type: 'vp_turn_start', vpId, threadId, turnId, sessionId, title: thread?.title || '' }, envelope);
2718
2731
  // vp-status: LLM call about to start, no text/tool yet → 'thinking'.
2719
2732
  try {
2720
- getVpStatusBroker().transition({ groupId, vpId, threadId, title: thread?.title || '', state: 'thinking', turnId, messageCount: thread?.messageIds?.length || 0 });
2733
+ getVpStatusBroker().transition({ sessionId, vpId, threadId, title: thread?.title || '', state: 'thinking', turnId, messageCount: thread?.messageIds?.length || 0 });
2721
2734
  } catch (err) {
2722
2735
  console.warn('[Yeaft] vp-status thinking transition failed:', err?.message || err);
2723
2736
  }
@@ -2731,21 +2744,21 @@ async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId =
2731
2744
  let vpEngine = null;
2732
2745
 
2733
2746
  // task-707: per-VP engine + persistent group coord. The coord is
2734
- // created in handleYeaftGroupChat via getOrCreateGroupContext and
2747
+ // created in handleYeaftSessionSend via getOrCreateSessionContext and
2735
2748
  // cached on `groupContexts`; we pull it here so route_forward
2736
2749
  // (router built from this same coord) lands envelopes back on the
2737
2750
  // right inbox set.
2738
- const groupCtx = groupContexts.get(groupId);
2739
- const groupCoordinator = groupCtx?.coord || null;
2751
+ const sessionCtx = groupContexts.get(sessionId);
2752
+ const sessionCoordinator = sessionCtx?.coord || null;
2740
2753
  const queryOpts = buildVpQueryOpts({
2741
2754
  vpId,
2742
- groupCoordinator,
2743
- groupId,
2755
+ sessionCoordinator,
2756
+ sessionId,
2744
2757
  envelope: inboundEnvelope,
2745
2758
  threadId,
2746
2759
  });
2747
2760
 
2748
- vpEngine = getOrCreateVpEngine(groupId, vpId, threadId);
2761
+ vpEngine = getOrCreateVpEngine(sessionId, vpId, threadId);
2749
2762
  if (thread) thread.engine = vpEngine;
2750
2763
 
2751
2764
  const handlerCtx = {
@@ -2754,7 +2767,7 @@ async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId =
2754
2767
  toolResultsAccum,
2755
2768
  thinkingBlocksAccum,
2756
2769
  resetQueryTimer,
2757
- groupId,
2770
+ sessionId,
2758
2771
  vpId,
2759
2772
  turnId,
2760
2773
  threadId,
@@ -2774,7 +2787,7 @@ async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId =
2774
2787
  messages: trimmedMessages,
2775
2788
  signal: vpAbort.signal,
2776
2789
  // Multi-VP fan-out (history-dedup): the user row was persisted
2777
- // ONCE by handleYeaftGroupChat → persistUserMessageOnce before
2790
+ // ONCE by handleYeaftSessionSend → persistUserMessageOnce before
2778
2791
  // fan-out. Tell the engine's stop-hook to skip the user-row
2779
2792
  // append for THIS VP's turn (it still writes assistant + tool
2780
2793
  // rows for this VP). Without this the magnet of N engines would
@@ -2793,7 +2806,7 @@ async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId =
2793
2806
  }
2794
2807
 
2795
2808
  // Turn completed — atomically append this VP's output to shared history.
2796
- appendTurnToGroupHistory(groupId, threadId, [prompt, ...appendedUserPrompts], assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum);
2809
+ appendTurnToGroupHistory(sessionId, threadId, [prompt, ...appendedUserPrompts], assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum);
2797
2810
 
2798
2811
  sendYeaftOutput({
2799
2812
  type: 'assistant',
@@ -2825,7 +2838,7 @@ async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId =
2825
2838
  // identical to a normal turn end in the timeline — the user has
2826
2839
  // no way to tell from the row that something went wrong.
2827
2840
  try {
2828
- getVpStatusBroker().transition({ groupId, vpId, threadId, title: thread?.title || '', state: 'error', turnId, messageCount: thread?.messageIds?.length || 0 });
2841
+ getVpStatusBroker().transition({ sessionId, vpId, threadId, title: thread?.title || '', state: 'error', turnId, messageCount: thread?.messageIds?.length || 0 });
2829
2842
  } catch (brokerErr) {
2830
2843
  console.warn('[Yeaft] vp-status error transition failed:', brokerErr?.message || brokerErr);
2831
2844
  }
@@ -2864,7 +2877,7 @@ async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId =
2864
2877
  // the row must drop back to 'idle'. Wrapped in its own try so a
2865
2878
  // broker bug can't mask the original error.
2866
2879
  try {
2867
- getVpStatusBroker().settleIdle({ groupId, vpId, threadId: threadId || 'main', title: thread?.title || '' });
2880
+ getVpStatusBroker().settleIdle({ sessionId, vpId, threadId: threadId || 'main', title: thread?.title || '' });
2868
2881
  } catch (err) {
2869
2882
  console.warn('[Yeaft] vp-status settleIdle failed:', err?.message || err);
2870
2883
  }
@@ -2899,9 +2912,9 @@ async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId =
2899
2912
  * a session, this in-memory tape carries the un-collapsed form — which
2900
2913
  * is fine because each VP turn's `engine.query` re-collapses on the fly.
2901
2914
  */
2902
- function appendTurnToGroupHistory(groupId, threadId, prompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum) {
2903
- if (!groupId) return;
2904
- const history = getOrCreateGroupHistory(groupId);
2915
+ function appendTurnToGroupHistory(sessionId, threadId, prompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum) {
2916
+ if (!sessionId) return;
2917
+ const history = getOrCreateGroupHistory(sessionId);
2905
2918
  const promptList = Array.isArray(prompts) ? prompts : [prompts];
2906
2919
  for (const prompt of promptList) {
2907
2920
  if (typeof prompt === 'string' && prompt.trim()) {
@@ -2948,7 +2961,7 @@ function appendTurnToGroupHistory(groupId, threadId, prompts, assistantTextParts
2948
2961
  /**
2949
2962
  * Persist an inbound message row to disk EXACTLY ONCE per
2950
2963
  * coordinator-ingest call, keyed by the coordinator-assigned `msgId`.
2951
- * Both `handleYeaftGroupChat` (real user input, persists as
2964
+ * Both `handleYeaftSessionSend` (real user input, persists as
2952
2965
  * role='user') and `enqueueForVp`'s driver loop (route_forward
2953
2966
  * synthetic injections, persists as role='assistant' attributed via
2954
2967
  * `speakerVpId`) call this — the Set guard makes either path the
@@ -2965,16 +2978,16 @@ function appendTurnToGroupHistory(groupId, threadId, prompts, assistantTextParts
2965
2978
  * still run, and the next user message will trigger another append.
2966
2979
  *
2967
2980
  * Note: we mirror `engine.#persistMessages`'s core user-row fields
2968
- * (role/content/threadId/groupId) so existing parsers keep working.
2981
+ * (role/content/threadId/sessionId) so existing parsers keep working.
2969
2982
  * Attachment UI metadata is persisted separately (without base64) so
2970
2983
  * refresh replay can render chips without leaking image source data into
2971
2984
  * the message body.
2972
2985
  *
2973
- * @param {{ msgId:string, text:string, groupId:string, role?:string, speakerVpId?:string|null, attachments?:Array<object> }} args
2986
+ * @param {{ msgId:string, text:string, sessionId:string, role?:string, speakerVpId?:string|null, attachments?:Array<object> }} args
2974
2987
  * @returns {boolean} true if this call wrote the row, false if a prior
2975
2988
  * call already wrote it (dedup hit).
2976
2989
  */
2977
- function persistInboundMessageOnceByMsgId({ msgId, text, groupId, threadId = 'main', role, speakerVpId, attachments }) {
2990
+ function persistInboundMessageOnceByMsgId({ msgId, text, sessionId, threadId = 'main', role, speakerVpId, attachments }) {
2978
2991
  if (!session?.conversationStore) return false;
2979
2992
  // No msgId means no dedup key — caller is responsible for guarding.
2980
2993
  // Both call sites already do (`if (envMsgId && text)` and
@@ -3006,7 +3019,7 @@ function persistInboundMessageOnceByMsgId({ msgId, text, groupId, threadId = 'ma
3006
3019
  }
3007
3020
  }
3008
3021
  try {
3009
- // role defaults to 'user' for back-compat: handleYeaftGroupChat's
3022
+ // role defaults to 'user' for back-compat: handleYeaftSessionSend's
3010
3023
  // real-user call site passes no role and gets a user row. The driver
3011
3024
  // loop passes role='assistant' + speakerVpId for route_forward
3012
3025
  // injections so the on-disk record correctly attributes the text to
@@ -3017,7 +3030,7 @@ function persistInboundMessageOnceByMsgId({ msgId, text, groupId, threadId = 'ma
3017
3030
  content: text,
3018
3031
  threadId: threadId || 'main',
3019
3032
  };
3020
- if (groupId) record.groupId = groupId;
3033
+ if (sessionId) record.sessionId = sessionId;
3021
3034
  // Stamp speakerVpId so the UI's loadHistory replay can route the row
3022
3035
  // to the correct VP block. Only meaningful when role='assistant'; for
3023
3036
  // a real user message we leave it unset (the UI's user track is
@@ -3182,28 +3195,28 @@ export function abortYeaftSession(opts = {}) {
3182
3195
  return { aborted: [], all: false };
3183
3196
  }
3184
3197
 
3185
- function seedAbortController(threadId, ctrl, groupId = 'test', vpId = 'vp', turnId = null) {
3198
+ function seedAbortController(threadId, ctrl, sessionId = 'test', vpId = 'vp', turnId = null) {
3186
3199
  const tid = threadId || 'main';
3187
- const key = threadKey(groupId, vpId, tid);
3200
+ const key = threadKey(sessionId, vpId, tid);
3188
3201
  if (ctrl) vpAborts.set(key, ctrl);
3189
3202
  if (turnId && ctrl) {
3190
3203
  turnAbortCtrls.set(turnId, ctrl);
3191
- turnAbortMeta.set(turnId, { groupId, vpId, threadId: tid, key });
3204
+ turnAbortMeta.set(turnId, { sessionId, vpId, threadId: tid, key });
3192
3205
  }
3193
3206
  }
3194
3207
 
3195
3208
  /** Test-only: seed an in-flight VP runtime controller. */
3196
- export function __testSeedAbortController(threadId, ctrl, groupId = 'test', vpId = 'vp') {
3197
- seedAbortController(threadId, ctrl, groupId, vpId);
3209
+ export function __testSeedAbortController(threadId, ctrl, sessionId = 'test', vpId = 'vp') {
3210
+ seedAbortController(threadId, ctrl, sessionId, vpId);
3198
3211
  }
3199
3212
 
3200
3213
  /** Test-only: seed an in-flight VP turn controller. */
3201
- export function __testSeedTurnAbortController(turnId, threadId, ctrl, groupId = 'test', vpId = 'vp') {
3214
+ export function __testSeedTurnAbortController(turnId, threadId, ctrl, sessionId = 'test', vpId = 'vp') {
3202
3215
  if (!turnId || !ctrl) return;
3203
3216
  const tid = threadId || 'main';
3204
- const key = threadKey(groupId, vpId, tid);
3217
+ const key = threadKey(sessionId, vpId, tid);
3205
3218
  turnAbortCtrls.set(turnId, ctrl);
3206
- turnAbortMeta.set(turnId, { groupId, vpId, threadId: tid, key });
3219
+ turnAbortMeta.set(turnId, { sessionId, vpId, threadId: tid, key });
3207
3220
  }
3208
3221
 
3209
3222
  /** Test-only: returns registered VP runtime thread ids. */
@@ -3233,12 +3246,12 @@ export const __testRaceWithEscalation = raceWithEscalation;
3233
3246
  * VP-detail page button). Fires an unscoped dream pass; the result
3234
3247
  * event is tagged with `vpId` so the per-VP store row updates.
3235
3248
  *
3236
- * { type: 'yeaft_dream_trigger', groupId } — per-GROUP trigger (new
3249
+ * { type: 'yeaft_dream_trigger', sessionId } — per-GROUP trigger (new
3237
3250
  * in v0.1.754 — added so users can manually kick dream for a group
3238
3251
  * after seeing the Resident layer stuck on the bootstrap seed).
3239
3252
  * Fires a scope-filtered pass via `triggerDreamForScopes(['group/X'])`
3240
3253
  * so unrelated groups don't get processed; the result event is
3241
- * tagged with `groupId` for the per-group UI row.
3254
+ * tagged with `sessionId` for the per-group UI row.
3242
3255
  *
3243
3256
  * Backwards-compat: when neither field is set, defaults to `vpId='default'`
3244
3257
  * which matches the pre-v0.1.754 behavior.
@@ -3286,14 +3299,14 @@ export function normalizeDreamResult(result) {
3286
3299
 
3287
3300
  export async function handleYeaftDreamTrigger(msg = {}) {
3288
3301
  // Resolve tag up-front so EVERY outbound envelope (including the
3289
- // scheduler-uninitialised early-return below) carries `groupId` /
3302
+ // scheduler-uninitialised early-return below) carries `sessionId` /
3290
3303
  // `vpId`. Without this the frontend's `applyDreamResult` couldn't
3291
3304
  // route the error event back to the right row and the per-group
3292
3305
  // "Run dream now" button would stay stuck on "Running…" forever
3293
3306
  // (review feedback from PR #757).
3294
- const groupId = typeof msg.groupId === 'string' && msg.groupId ? msg.groupId : null;
3295
- const vpId = !groupId ? (msg.vpId || 'default') : null;
3296
- const tag = groupId ? { groupId } : { vpId };
3307
+ const sessionId = typeof msg.sessionId === 'string' && msg.sessionId ? msg.sessionId : null;
3308
+ const vpId = !sessionId ? (msg.vpId || 'default') : null;
3309
+ const tag = sessionId ? { sessionId } : { vpId };
3297
3310
 
3298
3311
  if (!session?.dreamScheduler) {
3299
3312
  const error = 'Dream scheduler not initialized — session not loaded.';
@@ -3307,7 +3320,7 @@ export async function handleYeaftDreamTrigger(msg = {}) {
3307
3320
 
3308
3321
  // Concurrent-trigger guard for scoped runs. Two scoped clicks (same
3309
3322
  // group or different) overlapping the same inflight pass used to set
3310
- // the module-level groupId slot, race the sink wrapping, and let the
3323
+ // the module-level sessionId slot, race the sink wrapping, and let the
3311
3324
  // second `finally` restore the original sink while the first run was
3312
3325
  // still emitting events. We now refuse scoped triggers while ANY dream
3313
3326
  // pass is already running: a scoped manual click during an unscoped
@@ -3317,7 +3330,7 @@ export async function handleYeaftDreamTrigger(msg = {}) {
3317
3330
  // and a different group's filter would have been silently dropped
3318
3331
  // anyway (see dream-v2/schedule.js inflight reuse), so the user-facing
3319
3332
  // semantics are unchanged ("you already asked").
3320
- if (groupId && (inflightScopedDreamGroups.size > 0 || session.dreamScheduler.isRunning)) {
3333
+ if (sessionId && (inflightScopedDreamGroups.size > 0 || session.dreamScheduler.isRunning)) {
3321
3334
  const skippedResult = {
3322
3335
  skipped: true,
3323
3336
  skippedReason: 'already-running',
@@ -3333,21 +3346,21 @@ export async function handleYeaftDreamTrigger(msg = {}) {
3333
3346
  }
3334
3347
 
3335
3348
  // Per-call sink wrapper. For scoped runs we install a closure that
3336
- // injects this trigger's groupId onto top-level events the runner
3349
+ // injects this trigger's sessionId onto top-level events the runner
3337
3350
  // emits without one (start/merge/done), then delegates to the
3338
3351
  // original passthrough sink. The wrapper lives only for the lifetime
3339
3352
  // of this trigger and is restored in `finally`; concurrent calls for
3340
- // OTHER groupIds chain (last-installed wins) but each restoration
3353
+ // OTHER sessionIds chain (last-installed wins) but each restoration
3341
3354
  // unwinds back to its predecessor.
3342
3355
  const originalSink = session?._dreamProgressSink;
3343
- if (groupId) session._dreamActiveGroupId = groupId;
3344
- if (groupId && typeof originalSink === 'function') {
3345
- inflightScopedDreamGroups.add(groupId);
3356
+ if (sessionId) session._dreamActiveGroupId = sessionId;
3357
+ if (sessionId && typeof originalSink === 'function') {
3358
+ inflightScopedDreamGroups.add(sessionId);
3346
3359
  session._dreamProgressSink = (evt) => {
3347
3360
  try {
3348
- const stamped = evt && evt.groupId
3361
+ const stamped = evt && evt.sessionId
3349
3362
  ? evt
3350
- : { ...evt, groupId };
3363
+ : { ...evt, sessionId };
3351
3364
  originalSink(stamped);
3352
3365
  } catch { /* never let event delivery throw */ }
3353
3366
  };
@@ -3360,8 +3373,8 @@ export async function handleYeaftDreamTrigger(msg = {}) {
3360
3373
  status: 'running',
3361
3374
  });
3362
3375
 
3363
- const result = groupId
3364
- ? await session.dreamScheduler.triggerDreamForScopes([`group/${groupId}`])
3376
+ const result = sessionId
3377
+ ? await session.dreamScheduler.triggerDreamForScopes([`group/${sessionId}`])
3365
3378
  : await session.dreamScheduler.triggerDreamNow();
3366
3379
 
3367
3380
  const normalized = normalizeDreamResult(result);
@@ -3397,10 +3410,10 @@ export async function handleYeaftDreamTrigger(msg = {}) {
3397
3410
  });
3398
3411
  } finally {
3399
3412
  // Restore the original sink and release the per-group inflight lock.
3400
- if (groupId && session?._dreamActiveGroupId === groupId) session._dreamActiveGroupId = null;
3401
- if (groupId && typeof originalSink === 'function') {
3413
+ if (sessionId && session?._dreamActiveGroupId === sessionId) session._dreamActiveGroupId = null;
3414
+ if (sessionId && typeof originalSink === 'function') {
3402
3415
  session._dreamProgressSink = originalSink;
3403
- inflightScopedDreamGroups.delete(groupId);
3416
+ inflightScopedDreamGroups.delete(sessionId);
3404
3417
  }
3405
3418
  }
3406
3419
  }
@@ -3467,7 +3480,7 @@ export async function handleYeaftFetchToolStats(_msg = {}) {
3467
3480
  *
3468
3481
  * Inputs (all optional):
3469
3482
  * - `limit` — max number of loops to return (1..500, default 100)
3470
- * - `groupId` — narrow by group
3483
+ * - `sessionId` — narrow by group
3471
3484
  * - `threadId` — narrow by thread
3472
3485
  *
3473
3486
  * Sends:
@@ -3479,14 +3492,14 @@ export async function handleYeaftFetchToolStats(_msg = {}) {
3479
3492
  export async function handleYeaftFetchDebugHistory(msg = {}) {
3480
3493
  const limit = Number.isFinite(msg?.limit) ? Number(msg.limit) : 100;
3481
3494
  const dreamLimit = Number.isFinite(msg?.dreamLimit) ? Number(msg.dreamLimit) : 5;
3482
- const groupId = typeof msg?.groupId === 'string' && msg.groupId ? msg.groupId : null;
3495
+ const sessionId = typeof msg?.sessionId === 'string' && msg.sessionId ? msg.sessionId : null;
3483
3496
  const threadId = typeof msg?.threadId === 'string' && msg.threadId ? msg.threadId : null;
3484
3497
  let loops = [];
3485
3498
  let turns = [];
3486
3499
  let dreamEvents = [];
3487
3500
  try {
3488
3501
  if (session?.trace && typeof session.trace.fetchRecentDebugHistory === 'function') {
3489
- const out = session.trace.fetchRecentDebugHistory({ limit, dreamLimit, groupId, threadId });
3502
+ const out = session.trace.fetchRecentDebugHistory({ limit, dreamLimit, sessionId, threadId });
3490
3503
  loops = Array.isArray(out?.loops) ? out.loops : [];
3491
3504
  turns = Array.isArray(out?.turns) ? out.turns : [];
3492
3505
  dreamEvents = Array.isArray(out?.dreamEvents) ? out.dreamEvents : [];
@@ -3506,7 +3519,7 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
3506
3519
  loops,
3507
3520
  turns,
3508
3521
  dreamEvents,
3509
- groupId,
3522
+ sessionId,
3510
3523
  threadId,
3511
3524
  });
3512
3525
  }
@@ -3540,20 +3553,20 @@ export function handleYeaftModelSwitch(msg) {
3540
3553
  * Handle history load request. Loads recent messages from ConversationStore
3541
3554
  * and replays them through the standard claude_output pipeline.
3542
3555
  *
3543
- * Group-history-isolation (Bug 7): when `msg.groupId` is provided the
3556
+ * Group-history-isolation (Bug 7): when `msg.sessionId` is provided the
3544
3557
  * replay AND the engine's bootstrap context are filtered to that group.
3545
- * Messages tagged with another groupId — and legacy messages with no
3546
- * groupId at all — are excluded so a stale `grp_default` (or any other
3558
+ * Messages tagged with another sessionId — and legacy messages with no
3559
+ * sessionId at all — are excluded so a stale `grp_default` (or any other
3547
3560
  * group) never bleeds into the active group's pane.
3548
3561
  */
3549
3562
  export async function handleYeaftLoadHistory(msg) {
3550
- const groupId = (msg && typeof msg.groupId === 'string' && msg.groupId) || null;
3563
+ const sessionId = (msg && typeof msg.sessionId === 'string' && msg.sessionId) || null;
3551
3564
  // `lim` is now expressed in TURNS, not raw messages. `loadRecent` and
3552
3565
  // `loadRecentByGroup` use turn-based slicing so the cut never lands
3553
3566
  // mid-tool-arc. Pass `undefined` to use the persistence-layer default
3554
3567
  // (DEFAULT_RECENT_TURNS = 20 turns).
3555
3568
  const pickRecent = (store, lim) =>
3556
- groupId ? store.loadRecentByGroup(groupId, lim) : store.loadRecent(lim);
3569
+ sessionId ? store.loadRecentByGroup(sessionId, lim) : store.loadRecent(lim);
3557
3570
 
3558
3571
  if (!session) {
3559
3572
  const yeaftDir = ctx.CONFIG?.yeaftDir;
@@ -3568,16 +3581,16 @@ export async function handleYeaftLoadHistory(msg) {
3568
3581
  yeaftConversationId = `yeaft-${Date.now()}`;
3569
3582
 
3570
3583
  // Per-group history hydrates lazily via getOrCreateGroupHistory.
3571
- // When the load-history call carries a groupId, force-refresh THAT
3584
+ // When the load-history call carries a sessionId, force-refresh THAT
3572
3585
  // group's tape so the next user message sees on-disk state. When
3573
3586
  // it doesn't (legacy callers), do nothing — the per-group lazy
3574
3587
  // hydration handles it.
3575
- if (groupId) setGroupHistory(groupId, hydrateGroupHistory(groupId));
3576
- } else if (groupId) {
3588
+ if (sessionId) setGroupHistory(sessionId, hydrateGroupHistory(sessionId));
3589
+ } else if (sessionId) {
3577
3590
  // Re-entering an existing session with a (possibly new) group filter:
3578
3591
  // re-seed THIS group's history from disk so it doesn't carry stale
3579
3592
  // in-memory state into the next turn's context.
3580
- setGroupHistory(groupId, hydrateGroupHistory(groupId));
3593
+ setGroupHistory(sessionId, hydrateGroupHistory(sessionId));
3581
3594
  }
3582
3595
 
3583
3596
  // Always replay session_ready so refresh / reconnect rebuilds UI state.
@@ -3606,14 +3619,14 @@ export async function handleYeaftLoadHistory(msg) {
3606
3619
  // opening a group can paint the latest messages quickly; older rows are
3607
3620
  // paged via `yeaft_load_more_history` when the user scrolls upward.
3608
3621
  const limit = (typeof msg.limit === 'number') ? msg.limit : 10;
3609
- const visiblePage = groupId
3610
- ? loadVisibleGroupHistoryPage(session.conversationStore, groupId, limit)
3622
+ const visiblePage = sessionId
3623
+ ? loadVisibleGroupHistoryPage(session.conversationStore, sessionId, limit)
3611
3624
  : { messages: limit > 0 ? pickRecent(session.conversationStore, limit) : [], oldestSeq: null, hasMore: false };
3612
3625
  // Legacy compact.md is a non-group fallback only. For group replay, reading
3613
3626
  // it makes every group show "has compact" once any legacy/non-scoped compact
3614
3627
  // exists, even when this group has no scoped summary.
3615
- const compactSummary = groupId ? '' : session.conversationStore.readCompactSummary();
3616
- const replayEntries = groupId
3628
+ const compactSummary = sessionId ? '' : session.conversationStore.readCompactSummary();
3629
+ const replayEntries = sessionId
3617
3630
  ? visiblePage.messages
3618
3631
  : visiblePage.messages
3619
3632
  .map(projectPersistedToVisibleHistoryEntry)
@@ -3629,14 +3642,14 @@ export async function handleYeaftLoadHistory(msg) {
3629
3642
  ...(Array.isArray(entry.attachments) && entry.attachments.length > 0 ? { attachments: hydrateHistoryAttachmentPreviews(entry.attachments) } : {}),
3630
3643
  },
3631
3644
  ts: entry.ts || null,
3632
- }, { groupId: entry.groupId || null, threadId: entry.threadId || 'main', turnId: entry.turnId || entry.threadId || 'main' });
3645
+ }, { sessionId: entry.sessionId || null, threadId: entry.threadId || 'main', turnId: entry.turnId || entry.threadId || 'main' });
3633
3646
  } else if (entry.role === 'assistant') {
3634
3647
  // speakerVpId rides on the envelope so the frontend can route this
3635
3648
  // replayed assistant text to the correct VP track. Without it, the
3636
3649
  // history replay would merge replies from different VPs onto one
3637
3650
  // anonymous assistant turn.
3638
3651
  const envelopeOpts = {
3639
- groupId: entry.groupId || null,
3652
+ sessionId: entry.sessionId || null,
3640
3653
  threadId: entry.threadId || 'main',
3641
3654
  turnId: entry.turnId || entry.threadId || 'main',
3642
3655
  };
@@ -3657,7 +3670,7 @@ export async function handleYeaftLoadHistory(msg) {
3657
3670
  // tail rows cannot consume the bootstrap window or create false hasMore.
3658
3671
  let hasMore = false;
3659
3672
  let oldestSeq = null;
3660
- if (groupId) {
3673
+ if (sessionId) {
3661
3674
  hasMore = visiblePage.hasMore;
3662
3675
  oldestSeq = visiblePage.oldestSeq;
3663
3676
  }
@@ -3667,8 +3680,8 @@ export async function handleYeaftLoadHistory(msg) {
3667
3680
  // replay, only scoped per-(group, vp) summaries count; legacy compact.md is
3668
3681
  // reserved for non-group / pre-scoped 1:1 callers.
3669
3682
  let hasCompactSummaryFlag = !!compactSummary;
3670
- if (groupId && typeof session.conversationStore.hasAnyCompactSummaryForGroup === 'function') {
3671
- hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForGroup(groupId);
3683
+ if (sessionId && typeof session.conversationStore.hasAnyCompactSummaryForGroup === 'function') {
3684
+ hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForGroup(sessionId);
3672
3685
  }
3673
3686
 
3674
3687
  sendYeaftEvent({
@@ -3677,7 +3690,7 @@ export async function handleYeaftLoadHistory(msg) {
3677
3690
  hasCompactSummary: hasCompactSummaryFlag,
3678
3691
  totalHot: session.conversationStore.countHot(),
3679
3692
  totalCold: session.conversationStore.countCold(),
3680
- groupId,
3693
+ sessionId,
3681
3694
  hasMore,
3682
3695
  oldestSeq,
3683
3696
  });
@@ -3685,7 +3698,7 @@ export async function handleYeaftLoadHistory(msg) {
3685
3698
 
3686
3699
  /**
3687
3700
  * Handle a "load older messages" pagination request. Reads `turns` more
3688
- * turns of history strictly older than `beforeSeq` for `groupId`, and
3701
+ * turns of history strictly older than `beforeSeq` for `sessionId`, and
3689
3702
  * emits them in a single `yeaft_history_chunk` envelope (NOT a
3690
3703
  * `yeaft_output` — that pipeline appends, but the frontend needs to
3691
3704
  * PREPEND these older messages above what it already has).
@@ -3694,18 +3707,18 @@ export async function handleYeaftLoadHistory(msg) {
3694
3707
  * `handleYeaftLoadHistory` (user / assistant text only). On any internal
3695
3708
  * failure we still emit an empty chunk so the spinner clears.
3696
3709
  *
3697
- * @param {object} msg — { groupId, beforeSeq, turns }
3710
+ * @param {object} msg — { sessionId, beforeSeq, turns }
3698
3711
  */
3699
3712
  export async function handleYeaftLoadMoreHistory(msg) {
3700
- const groupId = (msg && typeof msg.groupId === 'string' && msg.groupId) || null;
3713
+ const sessionId = (msg && typeof msg.sessionId === 'string' && msg.sessionId) || null;
3701
3714
  const emit = (payload) => sendToServer({
3702
3715
  type: 'yeaft_history_chunk',
3703
3716
  conversationId: yeaftConversationId,
3704
- groupId,
3717
+ sessionId,
3705
3718
  ...payload,
3706
3719
  });
3707
3720
 
3708
- if (!session || !groupId) {
3721
+ if (!session || !sessionId) {
3709
3722
  emit({ messages: [], oldestSeq: null, hasMore: false });
3710
3723
  return;
3711
3724
  }
@@ -3715,7 +3728,7 @@ export async function handleYeaftLoadMoreHistory(msg) {
3715
3728
 
3716
3729
  let result;
3717
3730
  try {
3718
- result = loadVisibleGroupHistoryPage(session.conversationStore, groupId, turns, beforeSeq);
3731
+ result = loadVisibleGroupHistoryPage(session.conversationStore, sessionId, turns, beforeSeq);
3719
3732
  } catch (err) {
3720
3733
  console.error('[Yeaft] loadOlderByGroup failed:', err.message);
3721
3734
  result = { messages: [], oldestSeq: null, hasMore: false };
@@ -3731,7 +3744,7 @@ export async function handleYeaftLoadMoreHistory(msg) {
3731
3744
  role: m.role,
3732
3745
  content: m.content,
3733
3746
  ts: m.ts || m.time || null,
3734
- groupId: m.groupId || null,
3747
+ sessionId: m.sessionId || null,
3735
3748
  threadId: m.threadId || m.turnId || 'main',
3736
3749
  turnId: m.turnId || m.threadId || 'main',
3737
3750
  ...(Array.isArray(m.attachments) && m.attachments.length > 0 ? { attachments: hydrateHistoryAttachmentPreviews(m.attachments) } : {}),