@yeaft/webchat-agent 0.1.903 → 0.1.905

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.
@@ -55,6 +55,8 @@ import {
55
55
  import { persistYeaftAttachments, attachmentsForPersistence, persistedAttachmentPreviewPayload } from './attachments.js';
56
56
  import { parseSeqFromId } from './conversation/persist.js';
57
57
  import { sliceLastNTurns } from './turn-utils.js';
58
+ import { pairSanitize } from './pair-sanitize.js';
59
+ import { filterSnapshotForVp } from './snapshot-filter.js';
58
60
  import { createVpStatusBroker } from './vp-status-broker.js';
59
61
  import { classifyThread as defaultClassifyThread, fallbackTitle } from './vp/thread-classifier.js';
60
62
 
@@ -221,7 +223,7 @@ const vpCurrentTodos = new Map();
221
223
  * router: ReturnType<typeof createRouter>,
222
224
  * sessionHandle: object }>}
223
225
  */
224
- const groupContexts = new Map();
226
+ const sessionContexts = new Map();
225
227
 
226
228
  function vpKey(sessionId, vpId) {
227
229
  return `${sessionId}::${vpId}`;
@@ -356,7 +358,7 @@ async function waitForRoutePromises(msgId) {
356
358
  */
357
359
  function invalidateGroupContext(sessionId) {
358
360
  if (!sessionId) return;
359
- groupContexts.delete(sessionId);
361
+ sessionContexts.delete(sessionId);
360
362
  const prefix = `${sessionId}::`;
361
363
  for (const [k, ctrl] of vpAborts) {
362
364
  if (!k.startsWith(prefix)) continue;
@@ -458,7 +460,7 @@ let _vpUnsubscribe = null;
458
460
 
459
461
  /**
460
462
  * Per-group conversation history lives on the GroupContext entry
461
- * (`groupContexts.get(sessionId).history`). The pre-refactor module-level
463
+ * (`sessionContexts.get(sessionId).history`). The pre-refactor module-level
462
464
  * `conversationMessages` was a single array shared across every group —
463
465
  * a user prompt in group-A would leak into group-B's next-turn snapshot
464
466
  * because the bridge appended every turn to the same array regardless
@@ -466,7 +468,7 @@ let _vpUnsubscribe = null;
466
468
  * the in-memory tape was unified.
467
469
  *
468
470
  * Post-refactor: each GroupContext owns its own `history`, lazily
469
- * hydrated from `conversationStore.loadRecentByGroup(sessionId)` on first
471
+ * hydrated from `conversationStore.loadRecentBySession(sessionId)` on first
470
472
  * access. Group-A and group-B are isolated.
471
473
  *
472
474
  * @typedef {Array<{role:'user'|'assistant'|'tool', content:string|Array, toolCalls?:Array, toolCallId?:string, isError?:boolean}>} GroupHistory
@@ -482,7 +484,7 @@ let _vpUnsubscribe = null;
482
484
  * from disk (or explicitly assigned). The flag is required because an
483
485
  * empty array is legitimate post-consolidate / post-clear state and
484
486
  * MUST NOT trigger a re-hydrate. Without the flag, a partial entry
485
- * would short-circuit `getOrCreateGroupHistory` on truthy `[]` and skip
487
+ * would short-circuit `getOrCreateSessionHistory` on truthy `[]` and skip
486
488
  * the disk load.
487
489
  */
488
490
 
@@ -602,26 +604,26 @@ function loadVisibleGroupHistoryPage(store, sessionId, limit, beforeSeq = null)
602
604
 
603
605
  let rows = [];
604
606
  try {
605
- if (typeof store.loadVisibleByGroup === 'function') {
606
- const page = store.loadVisibleByGroup(sessionId, beforeSeq, limit);
607
+ if (typeof store.loadVisibleBySession === 'function') {
608
+ const page = store.loadVisibleBySession(sessionId, beforeSeq, limit);
607
609
  return {
608
610
  messages: (page.messages || []).map(projectPersistedToVisibleHistoryEntry).filter(Boolean),
609
611
  oldestSeq: (typeof page.oldestSeq === 'number') ? page.oldestSeq : null,
610
612
  hasMore: !!page.hasMore,
611
613
  };
612
- } else if (typeof store.loadOlderByGroup === 'function') {
614
+ } else if (typeof store.loadOlderBySession === 'function') {
613
615
  // Compatibility fallback for older test doubles: use an unbounded raw
614
616
  // prefix, then project/slice visible rows below.
615
- rows = store.loadOlderByGroup(sessionId, beforeSeq, Infinity).messages || [];
617
+ rows = store.loadOlderBySession(sessionId, beforeSeq, Infinity).messages || [];
616
618
  } else if (Number.isFinite(beforeSeq)) {
617
- const all = typeof store.loadAllByGroup === 'function'
618
- ? store.loadAllByGroup(sessionId)
619
- : store.loadRecentByGroup(sessionId, Infinity);
619
+ const all = typeof store.loadAllBySession === 'function'
620
+ ? store.loadAllBySession(sessionId)
621
+ : store.loadRecentBySession(sessionId, Infinity);
620
622
  rows = all.filter(m => parseSeqFromId(m?.id) < beforeSeq);
621
- } else if (typeof store.loadAllByGroup === 'function') {
622
- rows = store.loadAllByGroup(sessionId);
623
+ } else if (typeof store.loadAllBySession === 'function') {
624
+ rows = store.loadAllBySession(sessionId);
623
625
  } else {
624
- rows = store.loadRecentByGroup(sessionId, Infinity);
626
+ rows = store.loadRecentBySession(sessionId, Infinity);
625
627
  }
626
628
  } catch (err) {
627
629
  console.error('[Yeaft] visible history page load failed:', err?.message || err);
@@ -658,7 +660,7 @@ function hydrateGroupHistory(sessionId) {
658
660
  if (!session?.conversationStore || !sessionId) return [];
659
661
  let recent;
660
662
  try {
661
- recent = session.conversationStore.loadRecentByGroup(sessionId);
663
+ recent = session.conversationStore.loadRecentBySession(sessionId);
662
664
  } catch (err) {
663
665
  console.warn('[Yeaft] hydrateGroupHistory failed (sessionId=%s):', sessionId, err?.message || err);
664
666
  return [];
@@ -674,7 +676,7 @@ function hydrateGroupHistory(sessionId) {
674
676
  /**
675
677
  * Get-or-create the per-group history array. Used everywhere the bridge
676
678
  * needs to read/append/snapshot a group's conversation tape. Lazily
677
- * inserts an entry into `groupContexts` on first access — no
679
+ * inserts an entry into `sessionContexts` on first access — no
678
680
  * `sessionHandle` required (history is independent of coord/router
679
681
  * lifecycle, so a sub-agent / route_forward path that hasn't yet
680
682
  * opened the group can still read history).
@@ -687,9 +689,9 @@ function hydrateGroupHistory(sessionId) {
687
689
  * @param {string} sessionId
688
690
  * @returns {GroupHistory}
689
691
  */
690
- function getOrCreateGroupHistory(sessionId) {
692
+ function getOrCreateSessionHistory(sessionId) {
691
693
  if (!sessionId) return [];
692
- let entry = groupContexts.get(sessionId);
694
+ let entry = sessionContexts.get(sessionId);
693
695
  // Use `historyHydrated` rather than truthiness on `history` itself —
694
696
  // an empty array (post-consolidate, post-clear, or a partial entry
695
697
  // seeded by an early `getOrCreateSessionContext` call before data was
@@ -699,7 +701,7 @@ function getOrCreateGroupHistory(sessionId) {
699
701
  if (entry && entry.historyHydrated) return entry.history;
700
702
  if (!entry) {
701
703
  entry = makeGroupContextStub();
702
- groupContexts.set(sessionId, entry);
704
+ sessionContexts.set(sessionId, entry);
703
705
  }
704
706
  entry.history = hydrateGroupHistory(sessionId);
705
707
  entry.historyHydrated = true;
@@ -720,10 +722,10 @@ function getOrCreateGroupHistory(sessionId) {
720
722
  */
721
723
  function setGroupHistory(sessionId, next) {
722
724
  if (!sessionId) return;
723
- let entry = groupContexts.get(sessionId);
725
+ let entry = sessionContexts.get(sessionId);
724
726
  if (!entry) {
725
727
  entry = makeGroupContextStub();
726
- groupContexts.set(sessionId, entry);
728
+ sessionContexts.set(sessionId, entry);
727
729
  }
728
730
  entry.history = next;
729
731
  entry.historyHydrated = true;
@@ -737,7 +739,7 @@ function setGroupHistory(sessionId, next) {
737
739
  * @param {string} sessionId
738
740
  */
739
741
  export function __testGroupHistory(sessionId) {
740
- return getOrCreateGroupHistory(sessionId);
742
+ return getOrCreateSessionHistory(sessionId);
741
743
  }
742
744
 
743
745
  /**
@@ -758,12 +760,12 @@ export function __testSetSession(sessionLike) {
758
760
  /**
759
761
  * Test-only: peek at the GroupContext entry for a group (or undefined
760
762
  * if never seeded). Lets tests assert the `historyHydrated` flag without
761
- * exporting the entire `groupContexts` Map.
763
+ * exporting the entire `sessionContexts` Map.
762
764
  *
763
765
  * @param {string} sessionId
764
766
  */
765
767
  export function __testGroupContextEntry(sessionId) {
766
- return groupContexts.get(sessionId);
768
+ return sessionContexts.get(sessionId);
767
769
  }
768
770
 
769
771
  /**
@@ -887,9 +889,9 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
887
889
  * @returns {{ coord: object, router: object, sessionHandle: object }}
888
890
  */
889
891
  function getOrCreateSessionContext(sessionId, sessionHandle) {
890
- let entry = groupContexts.get(sessionId);
892
+ let entry = sessionContexts.get(sessionId);
891
893
  if (entry && entry.coord && entry.router) return entry;
892
- // Either no entry, or a partial entry seeded by `getOrCreateGroupHistory`
894
+ // Either no entry, or a partial entry seeded by `getOrCreateSessionHistory`
893
895
  // (no coord/router yet). Build the coord/router and merge into the
894
896
  // existing record so the per-group history reference and hydration
895
897
  // flag are preserved.
@@ -899,13 +901,13 @@ function getOrCreateSessionContext(sessionId, sessionHandle) {
899
901
  const router = createRouter({ coordinator: coord });
900
902
  if (!entry) {
901
903
  entry = makeGroupContextStub();
902
- groupContexts.set(sessionId, entry);
904
+ sessionContexts.set(sessionId, entry);
903
905
  }
904
906
  entry.coord = coord;
905
907
  entry.router = router;
906
908
  entry.sessionHandle = sessionHandle;
907
909
  // Defend against a future caller that builds a coord/router without
908
- // having gone through `getOrCreateGroupHistory` first: a partial entry
910
+ // having gone through `getOrCreateSessionHistory` first: a partial entry
909
911
  // could exist with `historyHydrated:false`, so do the load now.
910
912
  if (!entry.historyHydrated) {
911
913
  entry.history = hydrateGroupHistory(sessionId);
@@ -1048,8 +1050,13 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
1048
1050
  vpAborts.set(key, vpAbort);
1049
1051
  turnAbortCtrls.set(turnId, vpAbort);
1050
1052
  turnAbortMeta.set(turnId, { sessionId, vpId, threadId: thread.threadId, key });
1051
- const baseSnapshot = getOrCreateGroupHistory(sessionId)
1052
- .filter((m) => !m.threadId || m.threadId === 'main' || m.threadId === thread.threadId);
1053
+ const baseSnapshot = pairSanitize(
1054
+ filterSnapshotForVp(
1055
+ getOrCreateSessionHistory(sessionId)
1056
+ .filter((m) => !m.threadId || m.threadId === 'main' || m.threadId === thread.threadId),
1057
+ vpId,
1058
+ ),
1059
+ );
1053
1060
  const trigger = envelope?.trigger || 'fallback';
1054
1061
  const { text, prompt, promptParts } = buildVpPromptPayload(vpId, envelope);
1055
1062
 
@@ -1104,7 +1111,7 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
1104
1111
  try {
1105
1112
  if (text && envelope?.msg) {
1106
1113
  sendYeaftEvent({
1107
- type: 'group_message',
1114
+ type: 'session_message',
1108
1115
  sessionId,
1109
1116
  vpId,
1110
1117
  threadId: thread.threadId,
@@ -1226,7 +1233,7 @@ export async function __testResetVpState() {
1226
1233
  vpDrivers.clear();
1227
1234
  vpEngines.clear();
1228
1235
  vpAborts.clear();
1229
- groupContexts.clear();
1236
+ sessionContexts.clear();
1230
1237
  vpCurrentTodos.clear();
1231
1238
  threadClassifier = defaultClassifyThread;
1232
1239
  // Per-group compact in-flight + pending state lives on the session's
@@ -1413,31 +1420,20 @@ export function handleYeaftVpRead(msg) {
1413
1420
  }
1414
1421
 
1415
1422
  /**
1416
- * Group CRUD wired to WS events.
1423
+ * Session CRUD wired to WS events.
1417
1424
  */
1418
- function sendGroupCrudResult(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 });
1427
- }
1428
-
1429
- function sendGroupSnapshotBroadcast() {
1425
+ function sendSessionCrudResult(payload) {
1426
+ sendYeaftEvent({ type: 'session_crud_result', ...payload });
1427
+ }
1428
+
1429
+ function sendSessionSnapshotBroadcast() {
1430
1430
  try {
1431
1431
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1432
1432
  if (!yeaftDir) return;
1433
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
1434
  sendYeaftEvent({ type: 'session_list_updated', sessions });
1439
1435
  } catch (err) {
1440
- console.warn('[Yeaft] sendGroupSnapshotBroadcast failed:', err?.message || err);
1436
+ console.warn('[Yeaft] sendSessionSnapshotBroadcast failed:', err?.message || err);
1441
1437
  }
1442
1438
  }
1443
1439
 
@@ -1451,28 +1447,26 @@ function sendGroupSnapshotBroadcast() {
1451
1447
  *
1452
1448
  * fix-yeaft-session-per-agent: previously, Agent B's sessions were
1453
1449
  * invisible in the unified sidebar until the user clicked into B's
1454
- * Yeaft view, because `sendGroupSnapshotBroadcast` only fired from
1450
+ * Yeaft view, because `sendSessionSnapshotBroadcast` only fired from
1455
1451
  * `ensureSessionLoaded`. That made the cross-agent list look broken
1456
1452
  * ("I see A but not B even though B is online") and was a major
1457
1453
  * contributor to the "session list disappears on switch" symptom.
1458
1454
  */
1459
- export { sendGroupSnapshotBroadcast as broadcastYeaftSessionSnapshotEager };
1455
+ export { sendSessionSnapshotBroadcast as broadcastYeaftSessionSnapshotEager };
1460
1456
 
1461
- function sendGroupRosterChanged(session) {
1457
+ function sendSessionRosterChanged(session) {
1462
1458
  if (!session) return;
1463
1459
  const payload = {
1464
1460
  sessionId: session.id,
1465
- groupId: session.id, // wire-compat for old web bundles
1466
1461
  name: session.name,
1467
1462
  roster: session.roster,
1468
1463
  defaultVpId: session.defaultVpId,
1469
1464
  workDir: session.workDir || '',
1470
1465
  };
1471
- sendYeaftEvent({ type: 'group_roster_changed', ...payload });
1472
1466
  sendYeaftEvent({ type: 'session_roster_changed', ...payload });
1473
1467
  }
1474
1468
 
1475
- function groupErrorPayload(err) {
1469
+ function sessionErrorPayload(err) {
1476
1470
  let code = 'unknown';
1477
1471
  if (err instanceof SessionCrudError) code = err.code;
1478
1472
  else if (err instanceof SessionConfigError) code = err.code;
@@ -1488,9 +1482,9 @@ export function handleYeaftListSessions(msg) {
1488
1482
  try {
1489
1483
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1490
1484
  const groups = snapshotSessions(yeaftDir);
1491
- sendGroupCrudResult({ op: 'list', requestId, ok: true, groups });
1485
+ sendSessionCrudResult({ op: 'list', requestId, ok: true, sessions: groups });
1492
1486
  } catch (err) {
1493
- sendGroupCrudResult({ op: 'list', requestId, ok: false, error: groupErrorPayload(err) });
1487
+ sendSessionCrudResult({ op: 'list', requestId, ok: false, error: sessionErrorPayload(err) });
1494
1488
  }
1495
1489
  }
1496
1490
 
@@ -1500,25 +1494,34 @@ export function handleYeaftCreateSession(msg) {
1500
1494
  try {
1501
1495
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1502
1496
  const group = createSessionFromSpec(yeaftDir, payload);
1503
- sendGroupCrudResult({ op: 'create', requestId, ok: true, group });
1504
- sendGroupSnapshotBroadcast();
1497
+ sendSessionCrudResult({ op: 'create', requestId, ok: true, session: group });
1498
+ sendSessionSnapshotBroadcast();
1505
1499
  } catch (err) {
1506
- sendGroupCrudResult({ op: 'create', requestId, ok: false, error: groupErrorPayload(err) });
1500
+ sendSessionCrudResult({ op: 'create', requestId, ok: false, error: sessionErrorPayload(err) });
1507
1501
  }
1508
1502
  }
1509
1503
 
1510
1504
  export function handleYeaftRenameSession(msg) {
1511
1505
  const requestId = msg && msg.requestId;
1512
- const sessionId = msg && msg.sessionId;
1506
+ // fix-yeaft-delete-and-agent-revert: accept legacy `groupId` in
1507
+ // addition to `sessionId`. The contract documented in
1508
+ // `web/stores/sessions.js` header ("Inbound payloads may carry
1509
+ // either sessionId (new) or groupId (legacy); both are accepted,
1510
+ // prefer sessionId") was only honored on web-side reads; the agent
1511
+ // handlers were silently rejecting the older wire shape that today's
1512
+ // SessionSettingsModal still sends. That's what made delete/rename/
1513
+ // archive/update_config/add_member/remove_member/set_default_vp
1514
+ // all throw `not_found` on undefined ids.
1515
+ const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
1513
1516
  const name = msg && msg.name;
1514
1517
  try {
1515
1518
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1516
1519
  const group = renameSession(yeaftDir, sessionId, name);
1517
1520
  invalidateGroupContext(sessionId);
1518
- sendGroupCrudResult({ op: 'rename', requestId, ok: true, group });
1519
- sendGroupSnapshotBroadcast();
1521
+ sendSessionCrudResult({ op: 'rename', requestId, ok: true, session: group });
1522
+ sendSessionSnapshotBroadcast();
1520
1523
  } catch (err) {
1521
- sendGroupCrudResult({ op: 'rename', requestId, ok: false, error: groupErrorPayload(err) });
1524
+ sendSessionCrudResult({ op: 'rename', requestId, ok: false, error: sessionErrorPayload(err) });
1522
1525
  }
1523
1526
  }
1524
1527
 
@@ -1538,7 +1541,8 @@ export function handleYeaftRenameSession(msg) {
1538
1541
  */
1539
1542
  export function handleYeaftUpdateSession(msg) {
1540
1543
  const requestId = msg && msg.requestId;
1541
- const sessionId = msg && msg.sessionId;
1544
+ // wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
1545
+ const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
1542
1546
  const patch = (msg && msg.patch && typeof msg.patch === 'object') ? msg.patch : null;
1543
1547
  try {
1544
1548
  const hasName = patch && typeof patch.name === 'string' && patch.name.trim().length > 0;
@@ -1555,10 +1559,10 @@ export function handleYeaftUpdateSession(msg) {
1555
1559
  group = updateSessionAnnouncement(yeaftDir, sessionId, patch.announcement);
1556
1560
  }
1557
1561
  invalidateGroupContext(sessionId);
1558
- sendGroupCrudResult({ op: 'update', requestId, ok: true, group });
1559
- sendGroupSnapshotBroadcast();
1562
+ sendSessionCrudResult({ op: 'update', requestId, ok: true, session: group });
1563
+ sendSessionSnapshotBroadcast();
1560
1564
  } catch (err) {
1561
- sendGroupCrudResult({ op: 'update', requestId, ok: false, error: groupErrorPayload(err) });
1565
+ sendSessionCrudResult({ op: 'update', requestId, ok: false, error: sessionErrorPayload(err) });
1562
1566
  }
1563
1567
  }
1564
1568
 
@@ -1572,7 +1576,8 @@ export function handleYeaftUpdateSession(msg) {
1572
1576
  */
1573
1577
  export function handleYeaftUpdateSessionConfig(msg) {
1574
1578
  const requestId = msg && msg.requestId;
1575
- const sessionId = msg && msg.sessionId;
1579
+ // wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
1580
+ const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
1576
1581
  const partial = (msg && msg.config && typeof msg.config === 'object') ? msg.config : null;
1577
1582
  try {
1578
1583
  if (!sessionId) throw new SessionConfigError('missing_group_id', 'sessionId required');
@@ -1585,30 +1590,32 @@ export function handleYeaftUpdateSessionConfig(msg) {
1585
1590
  if (k.startsWith(prefix)) vpEngines.delete(k);
1586
1591
  }
1587
1592
  invalidateGroupContext(sessionId);
1588
- sendGroupCrudResult({ op: 'update_config', requestId, ok: true, sessionId, config: savedConfig });
1589
- sendGroupSnapshotBroadcast();
1593
+ sendSessionCrudResult({ op: 'update_config', requestId, ok: true, sessionId, config: savedConfig });
1594
+ sendSessionSnapshotBroadcast();
1590
1595
  } catch (err) {
1591
- sendGroupCrudResult({ op: 'update_config', requestId, ok: false, error: groupErrorPayload(err) });
1596
+ sendSessionCrudResult({ op: 'update_config', requestId, ok: false, error: sessionErrorPayload(err) });
1592
1597
  }
1593
1598
  }
1594
1599
 
1595
1600
  export function handleYeaftArchiveSession(msg) {
1596
1601
  const requestId = msg && msg.requestId;
1597
- const sessionId = msg && msg.sessionId;
1602
+ // wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
1603
+ const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
1598
1604
  try {
1599
1605
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1600
1606
  const result = archiveSession(yeaftDir, sessionId);
1601
1607
  invalidateGroupContext(sessionId);
1602
- sendGroupCrudResult({ op: 'archive', requestId, ok: true, sessionId: result.sessionId });
1603
- sendGroupSnapshotBroadcast();
1608
+ sendSessionCrudResult({ op: 'archive', requestId, ok: true, sessionId: result.sessionId });
1609
+ sendSessionSnapshotBroadcast();
1604
1610
  } catch (err) {
1605
- sendGroupCrudResult({ op: 'archive', requestId, ok: false, error: groupErrorPayload(err) });
1611
+ sendSessionCrudResult({ op: 'archive', requestId, ok: false, error: sessionErrorPayload(err) });
1606
1612
  }
1607
1613
  }
1608
1614
 
1609
1615
  export function handleYeaftDeleteSession(msg) {
1610
1616
  const requestId = msg && msg.requestId;
1611
- const sessionId = msg && msg.sessionId;
1617
+ // wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
1618
+ const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
1612
1619
  try {
1613
1620
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1614
1621
  const result = deleteSession(yeaftDir, sessionId);
@@ -1633,37 +1640,39 @@ export function handleYeaftDeleteSession(msg) {
1633
1640
  for (const k of Array.from(vpEngines.keys())) {
1634
1641
  if (k.startsWith(prefix)) vpEngines.delete(k);
1635
1642
  }
1636
- sendGroupCrudResult({
1643
+ sendSessionCrudResult({
1637
1644
  op: 'delete',
1638
1645
  requestId,
1639
1646
  ok: true,
1640
1647
  sessionId: result.sessionId,
1641
1648
  messagesRemoved,
1642
1649
  });
1643
- sendGroupSnapshotBroadcast();
1650
+ sendSessionSnapshotBroadcast();
1644
1651
  } catch (err) {
1645
- sendGroupCrudResult({ op: 'delete', requestId, ok: false, error: groupErrorPayload(err) });
1652
+ sendSessionCrudResult({ op: 'delete', requestId, ok: false, error: sessionErrorPayload(err) });
1646
1653
  }
1647
1654
  }
1648
1655
 
1649
1656
  export function handleYeaftSessionAddMember(msg) {
1650
1657
  const requestId = msg && msg.requestId;
1651
- const sessionId = msg && msg.sessionId;
1658
+ // wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
1659
+ const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
1652
1660
  const vpId = msg && msg.vpId;
1653
1661
  try {
1654
1662
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1655
1663
  const group = addMember(yeaftDir, sessionId, vpId);
1656
1664
  invalidateGroupContext(sessionId);
1657
- sendGroupCrudResult({ op: 'add_member', requestId, ok: true, group });
1658
- sendGroupRosterChanged(group);
1665
+ sendSessionCrudResult({ op: 'add_member', requestId, ok: true, session: group });
1666
+ sendSessionRosterChanged(group);
1659
1667
  } catch (err) {
1660
- sendGroupCrudResult({ op: 'add_member', requestId, ok: false, error: groupErrorPayload(err) });
1668
+ sendSessionCrudResult({ op: 'add_member', requestId, ok: false, error: sessionErrorPayload(err) });
1661
1669
  }
1662
1670
  }
1663
1671
 
1664
1672
  export function handleYeaftSessionRemoveMember(msg) {
1665
1673
  const requestId = msg && msg.requestId;
1666
- const sessionId = msg && msg.sessionId;
1674
+ // wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
1675
+ const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
1667
1676
  const vpId = msg && msg.vpId;
1668
1677
  try {
1669
1678
  const yeaftDir = ctx.CONFIG?.yeaftDir;
@@ -1675,25 +1684,26 @@ export function handleYeaftSessionRemoveMember(msg) {
1675
1684
  for (const key of Array.from(vpEngines.keys())) {
1676
1685
  if (key.startsWith(removedPrefix)) vpEngines.delete(key);
1677
1686
  }
1678
- sendGroupCrudResult({ op: 'remove_member', requestId, ok: true, group });
1679
- sendGroupRosterChanged(group);
1687
+ sendSessionCrudResult({ op: 'remove_member', requestId, ok: true, session: group });
1688
+ sendSessionRosterChanged(group);
1680
1689
  } catch (err) {
1681
- sendGroupCrudResult({ op: 'remove_member', requestId, ok: false, error: groupErrorPayload(err) });
1690
+ sendSessionCrudResult({ op: 'remove_member', requestId, ok: false, error: sessionErrorPayload(err) });
1682
1691
  }
1683
1692
  }
1684
1693
 
1685
1694
  export function handleYeaftSessionSetDefaultVp(msg) {
1686
1695
  const requestId = msg && msg.requestId;
1687
- const sessionId = msg && msg.sessionId;
1696
+ // wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
1697
+ const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
1688
1698
  const vpId = msg && msg.vpId;
1689
1699
  try {
1690
1700
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1691
1701
  const group = setSessionDefaultVp(yeaftDir, sessionId, vpId);
1692
1702
  invalidateGroupContext(sessionId);
1693
- sendGroupCrudResult({ op: 'set_default_vp', requestId, ok: true, group });
1694
- sendGroupRosterChanged(group);
1703
+ sendSessionCrudResult({ op: 'set_default_vp', requestId, ok: true, session: group });
1704
+ sendSessionRosterChanged(group);
1695
1705
  } catch (err) {
1696
- sendGroupCrudResult({ op: 'set_default_vp', requestId, ok: false, error: groupErrorPayload(err) });
1706
+ sendSessionCrudResult({ op: 'set_default_vp', requestId, ok: false, error: sessionErrorPayload(err) });
1697
1707
  }
1698
1708
  }
1699
1709
 
@@ -2170,20 +2180,21 @@ function handleEngineEvent(event, hctx) {
2170
2180
  }
2171
2181
 
2172
2182
  /**
2173
- * Handle a yeaft_group_chat message from the web UI — the SOLE Yeaft
2183
+ * Handle a yeaft_session_chat message from the web UI — the SOLE Yeaft
2174
2184
  * conversation entry point.
2175
2185
  *
2176
2186
  * Contract (post-consolidation, was previously split between handleYeaftChat
2177
2187
  * and handleYeaftSessionSend):
2178
- * - Frontend ALWAYS sends `yeaft_group_chat`. There is no `yeaft_chat`.
2188
+ * - Frontend ALWAYS sends `yeaft_session_chat`. There is no `yeaft_chat`.
2179
2189
  * - `sessionId` defaults to `'grp_default'` if missing — Yeaft is a single
2180
- * conversation backed by the default group; the user is never "outside"
2181
- * a group.
2182
- * - If the group dir doesn't exist and the resolved id is `'grp_default'`,
2183
- * it is seeded on the fly. Any other unknown sessionId surfaces an error.
2190
+ * conversation backed by the default session; the user is never "outside"
2191
+ * a session.
2192
+ * - Sessions are created up-front via `handleYeaftCreateSession`; this
2193
+ * handler does NOT seed missing sessions on the fly. An unknown
2194
+ * sessionId surfaces a clear "session not found" error to the UI.
2184
2195
  * - Coordinator is MANDATORY (this is what guarantees ctx.router is wired
2185
2196
  * so the `route_forward` tool can never trip `router_unavailable`).
2186
- * - No legacy "no-group" fallback paths — they were the source of the
2197
+ * - No legacy "no-session" fallback paths — they were the source of the
2187
2198
  * router_unavailable bug fixed in v0.1.671.
2188
2199
  */
2189
2200
  export async function handleYeaftSessionSend(msg) {
@@ -2228,12 +2239,11 @@ export async function handleYeaftSessionSend(msg) {
2228
2239
 
2229
2240
  await ensureSessionLoaded();
2230
2241
 
2231
- // Open the group; seed grp_default on the fly if absent. Track
2232
- // seedFailed separately so a seed crash surfaces a different message
2233
- // than a genuinely-missing group.
2242
+ // Open the session. The default `grp_default` no longer self-seeds
2243
+ // here session creation happens up-front via `handleYeaftCreateSession`,
2244
+ // so a missing dir surfaces a clear error rather than masking it.
2234
2245
  let sessionHandle = null;
2235
2246
  let sessionRoot = null;
2236
- let seedFailed = false;
2237
2247
  try {
2238
2248
  const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
2239
2249
  sessionRoot = sessionsRoot(groupYeaftDir);
@@ -2247,19 +2257,16 @@ export async function handleYeaftSessionSend(msg) {
2247
2257
  // default-group row across agents. Now we surface the not-found
2248
2258
  // case so the web can show an "agent offline / session missing"
2249
2259
  // hint instead of silently creating a different session.
2250
- console.warn('[Yeaft] yeaft_group_chat: sessionId %s not found', sessionId);
2260
+ console.warn('[Yeaft] yeaft_session_chat: sessionId %s not found', sessionId);
2251
2261
  }
2252
2262
  } catch (err) {
2253
- console.warn('[Yeaft] yeaft_group_chat: group open failed', err?.message || err);
2263
+ console.warn('[Yeaft] yeaft_session_chat: session open failed', err?.message || err);
2254
2264
  }
2255
2265
 
2256
2266
  if (!sessionHandle) {
2257
- const errText = seedFailed
2258
- ? `⚠️ Failed to seed default group ${sessionId} — check group .yeaft permissions.`
2259
- : `⚠️ Group ${sessionId} not found.`;
2260
2267
  sendYeaftOutput({
2261
2268
  type: 'assistant',
2262
- message: { content: [{ type: 'text', text: errText }] },
2269
+ message: { content: [{ type: 'text', text: `⚠️ Session ${sessionId} not found.` }] },
2263
2270
  }, { sessionId });
2264
2271
  sendYeaftOutput({ type: 'result', result_text: '' }, { sessionId });
2265
2272
  return;
@@ -2284,7 +2291,7 @@ export async function handleYeaftSessionSend(msg) {
2284
2291
  if (rosterMutated) {
2285
2292
  try { sessionHandle.close && sessionHandle.close(); } catch { /* best-effort */ }
2286
2293
  sessionHandle = openSession(sessionRoot, sessionId);
2287
- sendGroupRosterChanged(sessionHandle.getMeta());
2294
+ sendSessionRosterChanged(sessionHandle.getMeta());
2288
2295
  }
2289
2296
  }
2290
2297
  const meta2 = sessionHandle.getMeta();
@@ -2293,12 +2300,12 @@ export async function handleYeaftSessionSend(msg) {
2293
2300
  setSessionDefaultVp(yeaftDir, sessionId, meta2.roster[0]);
2294
2301
  try { sessionHandle.close && sessionHandle.close(); } catch { /* best-effort */ }
2295
2302
  sessionHandle = openSession(sessionRoot, sessionId);
2296
- sendGroupRosterChanged(sessionHandle.getMeta());
2303
+ sendSessionRosterChanged(sessionHandle.getMeta());
2297
2304
  rosterMutated = true;
2298
2305
  } catch { /* best-effort */ }
2299
2306
  }
2300
2307
  } catch (err) {
2301
- console.warn('[Yeaft] yeaft_group_chat: auto-roster heal failed', err?.message || err);
2308
+ console.warn('[Yeaft] yeaft_session_chat: auto-roster heal failed', err?.message || err);
2302
2309
  }
2303
2310
 
2304
2311
  // task-707: per-group persistent coordinator/router. Created once per
@@ -2307,7 +2314,7 @@ export async function handleYeaftSessionSend(msg) {
2307
2314
  // we replace the cached coord so it points at the freshly-opened
2308
2315
  // sessionHandle.
2309
2316
  if (rosterMutated) {
2310
- groupContexts.delete(sessionId);
2317
+ sessionContexts.delete(sessionId);
2311
2318
  }
2312
2319
  const sessionCtx = getOrCreateSessionContext(sessionId, sessionHandle);
2313
2320
  const coord = sessionCtx.coord;
@@ -2331,7 +2338,7 @@ export async function handleYeaftSessionSend(msg) {
2331
2338
  try {
2332
2339
  attachmentBundle = persistYeaftAttachments(inboundFiles, { subdir: sessionId });
2333
2340
  } catch (err) {
2334
- console.warn('[Yeaft] yeaft_group_chat: attachment persist failed', err?.message || err);
2341
+ console.warn('[Yeaft] yeaft_session_chat: attachment persist failed', err?.message || err);
2335
2342
  }
2336
2343
  }
2337
2344
  // Surface partial / total upload failures to the user. We don't abort
@@ -2370,10 +2377,10 @@ export async function handleYeaftSessionSend(msg) {
2370
2377
  _promptSuffix: attachmentBundle.promptSuffix,
2371
2378
  });
2372
2379
  } catch (err) {
2373
- console.warn('[Yeaft] yeaft_group_chat: coord.ingest failed', err?.message || err);
2380
+ console.warn('[Yeaft] yeaft_session_chat: coord.ingest failed', err?.message || err);
2374
2381
  sendYeaftOutput({
2375
2382
  type: 'assistant',
2376
- message: { content: [{ type: 'text', text: `⚠️ Group dispatch error: ${err?.message || err}` }] },
2383
+ message: { content: [{ type: 'text', text: `⚠️ Session dispatch error: ${err?.message || err}` }] },
2377
2384
  }, { sessionId });
2378
2385
  sendYeaftOutput({ type: 'result', result_text: '' }, { sessionId });
2379
2386
  return;
@@ -2574,7 +2581,7 @@ async function ensureSessionLoaded() {
2574
2581
 
2575
2582
  yeaftConversationId = `yeaft-${Date.now()}`;
2576
2583
 
2577
- // Per-group history is hydrated lazily on first `getOrCreateGroupHistory`
2584
+ // Per-group history is hydrated lazily on first `getOrCreateSessionHistory`
2578
2585
  // — there's no global "all conversations" tape any more.
2579
2586
 
2580
2587
  sendYeaftEvent({
@@ -2587,7 +2594,7 @@ async function ensureSessionLoaded() {
2587
2594
  tools: session.status.tools,
2588
2595
  yeaftDir: ctx.CONFIG?.yeaftDir || null,
2589
2596
  });
2590
- sendGroupSnapshotBroadcast();
2597
+ sendSessionSnapshotBroadcast();
2591
2598
  // vp-status: rebuild frontend status table from authoritative agent
2592
2599
  // memory. Sent unconditionally so reconnect/refresh paths get the same
2593
2600
  // bootstrap as first-load (the broker dedup logic makes a redundant
@@ -2713,7 +2720,7 @@ async function raceWithEscalation(inner, { deadlineMs, onEscalate }) {
2713
2720
  * appended to `conversationMessages`.
2714
2721
  *
2715
2722
  * task-707: takes a coordinator `envelope` rather than the coordinator
2716
- * itself; the persistent coord lives in `groupContexts[sessionId]`. Uses
2723
+ * itself; the persistent coord lives in `sessionContexts[sessionId]`. Uses
2717
2724
  * `getOrCreateVpEngine(sessionId, vpId)` so each VP runs against its own
2718
2725
  * Engine instance — private state (`#currentAbortCtrl`, `#__queryCounter`,
2719
2726
  * `#pendingT2`, `#abortReason`, `#adjustRanByGroup`, `#execLog`) does not
@@ -2794,10 +2801,10 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
2794
2801
 
2795
2802
  // task-707: per-VP engine + persistent group coord. The coord is
2796
2803
  // created in handleYeaftSessionSend via getOrCreateSessionContext and
2797
- // cached on `groupContexts`; we pull it here so route_forward
2804
+ // cached on `sessionContexts`; we pull it here so route_forward
2798
2805
  // (router built from this same coord) lands envelopes back on the
2799
2806
  // right inbox set.
2800
- const sessionCtx = groupContexts.get(sessionId);
2807
+ const sessionCtx = sessionContexts.get(sessionId);
2801
2808
  const sessionCoordinator = sessionCtx?.coord || null;
2802
2809
  const queryOpts = buildVpQueryOpts({
2803
2810
  vpId,
@@ -2856,7 +2863,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
2856
2863
  }
2857
2864
 
2858
2865
  // Turn completed — atomically append this VP's output to shared history.
2859
- appendTurnToGroupHistory(sessionId, threadId, [prompt, ...appendedUserPrompts], assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum);
2866
+ appendTurnToSessionHistory(sessionId, threadId, vpId, [prompt, ...appendedUserPrompts], assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum);
2860
2867
 
2861
2868
  sendYeaftOutput({
2862
2869
  type: 'assistant',
@@ -2974,23 +2981,31 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
2974
2981
  * exactly once, with the same threadId as the running thread. The
2975
2982
  * engine's own `conversationMessages` (with T1/T2 collapse applied)
2976
2983
  * is persisted to disk via stop-hooks, so the next turn's history is
2977
- * read from disk via `loadRecentByGroup` on next session boot. Within
2984
+ * read from disk via `loadRecentBySession` on next session boot. Within
2978
2985
  * a session, this in-memory tape carries the un-collapsed form — which
2979
2986
  * is fine because each VP turn's `engine.query` re-collapses on the fly.
2980
2987
  */
2981
- function appendTurnToGroupHistory(sessionId, threadId, prompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum) {
2988
+ function appendTurnToSessionHistory(sessionId, threadId, vpId, prompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum) {
2982
2989
  if (!sessionId) return;
2983
- const history = getOrCreateGroupHistory(sessionId);
2990
+ const history = getOrCreateSessionHistory(sessionId);
2984
2991
  const promptList = Array.isArray(prompts) ? prompts : [prompts];
2985
2992
  for (const prompt of promptList) {
2986
2993
  if (typeof prompt === 'string' && prompt.trim()) {
2994
+ // user rows intentionally carry NO speakerVpId — every VP in the
2995
+ // session should see the prompt in their history.
2987
2996
  history.push({ role: 'user', content: prompt, threadId: threadId || 'main' });
2988
2997
  }
2989
2998
  }
2990
2999
 
2991
3000
  const fullText = assistantTextParts.join('');
2992
3001
  if (fullText || toolCallsAccum.length > 0) {
3002
+ // Stamp speakerVpId on assistant + tool rows so the in-memory
3003
+ // baseSnapshot filter (filterSnapshotForVp) can mirror the disk
3004
+ // replay's per-VP isolation rules. Without this stamp, the next
3005
+ // VP turn would inherit the previous VP's tool_use ids without
3006
+ // matching tool_result rows → Anthropic API 422.
2993
3007
  const assistantMsg = { role: 'assistant', content: fullText, threadId: threadId || 'main' };
3008
+ if (vpId) assistantMsg.speakerVpId = vpId;
2994
3009
  if (toolCallsAccum.length > 0) {
2995
3010
  assistantMsg.toolCalls = toolCallsAccum.map(tc => ({
2996
3011
  id: tc.id,
@@ -3002,7 +3017,9 @@ function appendTurnToGroupHistory(sessionId, threadId, prompts, assistantTextPar
3002
3017
  // requires us to echo them back on the next request or the API
3003
3018
  // returns "content[].thinking in the thinking mode must be passed
3004
3019
  // back to the API". The signature is server-private — it stays in
3005
- // this in-memory history and in agent-side persistence only.
3020
+ // this in-memory history and in agent-side persistence only. The
3021
+ // signature is also VP-private; filterSnapshotForVp drops it from
3022
+ // OTHER VPs' rows before each turn's payload is built.
3006
3023
  if (Array.isArray(thinkingBlocksAccum) && thinkingBlocksAccum.length > 0) {
3007
3024
  assistantMsg.thinkingBlocks = thinkingBlocksAccum.map(tb => (
3008
3025
  tb.redacted
@@ -3013,13 +3030,15 @@ function appendTurnToGroupHistory(sessionId, threadId, prompts, assistantTextPar
3013
3030
  history.push(assistantMsg);
3014
3031
 
3015
3032
  for (const tr of toolResultsAccum) {
3016
- history.push({
3033
+ const toolMsg = {
3017
3034
  role: 'tool',
3018
3035
  toolCallId: tr.toolCallId,
3019
3036
  content: tr.content,
3020
3037
  isError: tr.isError,
3021
3038
  threadId: threadId || 'main',
3022
- });
3039
+ };
3040
+ if (vpId) toolMsg.speakerVpId = vpId;
3041
+ history.push(toolMsg);
3023
3042
  }
3024
3043
  }
3025
3044
  }
@@ -3303,6 +3322,16 @@ export function __testGetRegisteredThreadIds() {
3303
3322
  */
3304
3323
  export const __testRaceWithEscalation = raceWithEscalation;
3305
3324
 
3325
+ /**
3326
+ * Test-only: invoke `appendTurnToSessionHistory` directly. Lets the VP
3327
+ * stamp contract (`speakerVpId` on assistant + tool rows, none on user
3328
+ * rows) be pinned with a table-driven test instead of booting a full
3329
+ * session. See `test/agent/yeaft/web-bridge-append-turn-vp-stamp.test.js`.
3330
+ */
3331
+ export function __testAppendTurnToSessionHistory(...args) {
3332
+ return appendTurnToSessionHistory(...args);
3333
+ }
3334
+
3306
3335
  /**
3307
3336
  * Manual dream trigger.
3308
3337
  *
@@ -3628,11 +3657,11 @@ export function handleYeaftModelSwitch(msg) {
3628
3657
  export async function handleYeaftLoadHistory(msg) {
3629
3658
  const sessionId = (msg && typeof msg.sessionId === 'string' && msg.sessionId) || null;
3630
3659
  // `lim` is now expressed in TURNS, not raw messages. `loadRecent` and
3631
- // `loadRecentByGroup` use turn-based slicing so the cut never lands
3660
+ // `loadRecentBySession` use turn-based slicing so the cut never lands
3632
3661
  // mid-tool-arc. Pass `undefined` to use the persistence-layer default
3633
3662
  // (DEFAULT_RECENT_TURNS = 20 turns).
3634
3663
  const pickRecent = (store, lim) =>
3635
- sessionId ? store.loadRecentByGroup(sessionId, lim) : store.loadRecent(lim);
3664
+ sessionId ? store.loadRecentBySession(sessionId, lim) : store.loadRecent(lim);
3636
3665
 
3637
3666
  if (!session) {
3638
3667
  const yeaftDir = ctx.CONFIG?.yeaftDir;
@@ -3646,7 +3675,7 @@ export async function handleYeaftLoadHistory(msg) {
3646
3675
 
3647
3676
  yeaftConversationId = `yeaft-${Date.now()}`;
3648
3677
 
3649
- // Per-group history hydrates lazily via getOrCreateGroupHistory.
3678
+ // Per-group history hydrates lazily via getOrCreateSessionHistory.
3650
3679
  // When the load-history call carries a sessionId, force-refresh THAT
3651
3680
  // group's tape so the next user message sees on-disk state. When
3652
3681
  // it doesn't (legacy callers), do nothing — the per-group lazy
@@ -3670,7 +3699,7 @@ export async function handleYeaftLoadHistory(msg) {
3670
3699
  tools: session.status.tools,
3671
3700
  yeaftDir: ctx.CONFIG?.yeaftDir || null,
3672
3701
  });
3673
- sendGroupSnapshotBroadcast();
3702
+ sendSessionSnapshotBroadcast();
3674
3703
  // vp-status: replay the authoritative table on reconnect so a refreshed
3675
3704
  // frontend doesn't have to wait for the next transition to learn each
3676
3705
  // VP's current state.
@@ -3795,8 +3824,8 @@ export async function handleYeaftLoadHistory(msg) {
3795
3824
  // replay, only scoped per-(group, vp) summaries count; legacy compact.md is
3796
3825
  // reserved for non-group / pre-scoped 1:1 callers.
3797
3826
  let hasCompactSummaryFlag = !!compactSummary;
3798
- if (sessionId && typeof session.conversationStore.hasAnyCompactSummaryForGroup === 'function') {
3799
- hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForGroup(sessionId);
3827
+ if (sessionId && typeof session.conversationStore.hasAnyCompactSummaryForSession === 'function') {
3828
+ hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForSession(sessionId);
3800
3829
  }
3801
3830
 
3802
3831
  // Latest seq cursor in the recent-mode reply lets the frontend stamp its
@@ -3856,7 +3885,7 @@ export async function handleYeaftLoadMoreHistory(msg) {
3856
3885
  try {
3857
3886
  result = loadVisibleGroupHistoryPage(session.conversationStore, sessionId, turns, beforeSeq);
3858
3887
  } catch (err) {
3859
- console.error('[Yeaft] loadOlderByGroup failed:', err.message);
3888
+ console.error('[Yeaft] loadOlderBySession failed:', err.message);
3860
3889
  result = { messages: [], oldestSeq: null, hasMore: false };
3861
3890
  }
3862
3891
 
@@ -3902,7 +3931,7 @@ export async function resetYeaftSession() {
3902
3931
  session = null;
3903
3932
  }
3904
3933
  yeaftConversationId = null;
3905
- // Per-group histories live on groupContexts entries — clearing the
3934
+ // Per-group histories live on sessionContexts entries — clearing the
3906
3935
  // map (a few lines below) drops every group's history with it. No
3907
3936
  // separate global tape to clear.
3908
3937
  // Re-arm the permission warning. The user might have fixed the
@@ -3922,7 +3951,7 @@ export async function resetYeaftSession() {
3922
3951
  vpInboxes.clear();
3923
3952
  vpDrivers.clear();
3924
3953
  vpEngines.clear();
3925
- groupContexts.clear();
3954
+ sessionContexts.clear();
3926
3955
  vpCurrentTodos.clear();
3927
3956
  threadClassifier = defaultClassifyThread;
3928
3957
  // History-dedup cache is keyed by per-session coordinator msg ids;
@@ -3951,7 +3980,7 @@ export async function resetYeaftSession() {
3951
3980
 
3952
3981
  yeaftConversationId = `yeaft-${Date.now()}`;
3953
3982
 
3954
- // Per-group history hydrates lazily via getOrCreateGroupHistory on
3983
+ // Per-group history hydrates lazily via getOrCreateSessionHistory on
3955
3984
  // first read. Nothing to seed here.
3956
3985
 
3957
3986
  sendYeaftEvent({