@yeaft/webchat-agent 0.1.873 → 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 (48) hide show
  1. package/connection/message-router.js +20 -13
  2. package/package.json +1 -1
  3. package/providers/copilot-models.js +105 -89
  4. package/providers/copilot.js +3 -1
  5. package/yeaft/attachments.js +2 -2
  6. package/yeaft/cli.js +13 -13
  7. package/yeaft/compact/compactor.js +20 -20
  8. package/yeaft/conversation/persist.js +95 -95
  9. package/yeaft/debug-trace.js +12 -12
  10. package/yeaft/dream-v2/apply.js +15 -15
  11. package/yeaft/dream-v2/merge.js +12 -12
  12. package/yeaft/dream-v2/prompts/{extract-group.md → extract-session.md} +12 -12
  13. package/yeaft/dream-v2/prompts/index.js +3 -3
  14. package/yeaft/dream-v2/prompts/triage-pass1.md +1 -1
  15. package/yeaft/dream-v2/runner.js +37 -37
  16. package/yeaft/dream-v2/segment.js +3 -3
  17. package/yeaft/dream-v2/session-wiring.js +22 -22
  18. package/yeaft/dream-v2/state.js +7 -7
  19. package/yeaft/dream-v2/triage.js +22 -22
  20. package/yeaft/engine.js +65 -65
  21. package/yeaft/memory/ams-registry.js +22 -22
  22. package/yeaft/memory/seed-backfill.js +9 -9
  23. package/yeaft/memory/store-v2.js +27 -27
  24. package/yeaft/prompts.js +9 -9
  25. package/yeaft/routing/loop-guard.js +14 -14
  26. package/yeaft/routing/router.js +5 -5
  27. package/yeaft/session.js +5 -5
  28. package/yeaft/sessions/coordinator.js +96 -20
  29. package/yeaft/{groups → sessions}/ids.js +3 -3
  30. package/yeaft/{groups → sessions}/index.js +28 -28
  31. package/yeaft/sessions/pre-flow.js +178 -42
  32. package/yeaft/{groups → sessions}/seed-default.js +19 -19
  33. package/yeaft/{groups/group-config.js → sessions/session-config.js} +29 -29
  34. package/yeaft/{groups/group-crud.js → sessions/session-crud.js} +113 -113
  35. package/yeaft/sessions/session-store.js +85 -154
  36. package/yeaft/stop-hooks.js +4 -4
  37. package/yeaft/tools/todo-write.js +1 -1
  38. package/yeaft/tools/types.js +2 -2
  39. package/yeaft/vp/registry.js +1 -1
  40. package/yeaft/vp/vp-crud.js +1 -1
  41. package/yeaft/vp-status-broker.js +28 -28
  42. package/yeaft/web-bridge.js +411 -398
  43. package/yeaft/groups/coordinator.js +0 -221
  44. package/yeaft/groups/group-store.js +0 -212
  45. package/yeaft/groups/pre-flow.js +0 -329
  46. /package/yeaft/{groups → sessions}/feature-flag.js +0 -0
  47. /package/yeaft/{groups → sessions}/project-doc.js +0 -0
  48. /package/yeaft/{groups → sessions}/roster.js +0 -0
@@ -2,7 +2,7 @@
2
2
  * persist.js — Conversation message persistence
3
3
  *
4
4
  * Each message is stored as a .md file with YAML frontmatter in
5
- * ~/.yeaft/chat/messages/ or ~/.yeaft/groups/<groupId>/conversation/messages/. Design: zero JSON, all Markdown.
5
+ * ~/.yeaft/chat/messages/ or ~/.yeaft/groups/<sessionId>/conversation/messages/. Design: zero JSON, all Markdown.
6
6
  *
7
7
  * Message format:
8
8
  * ---
@@ -104,11 +104,11 @@ function serializeMessage(msg) {
104
104
  // their original thread id in `sourceThreadId` so the UI can still
105
105
  // render a small "#source" pill next to each bubble.
106
106
  if (msg.sourceThreadId) fm.push(`sourceThreadId: ${msg.sourceThreadId}`);
107
- // Bug 6: persist groupId so history replay can stamp messages with the
107
+ // Bug 6: persist sessionId so history replay can stamp messages with the
108
108
  // group they originated in. Without this, every replayed message lands
109
109
  // in the default group and switching back to the originating group
110
110
  // shows an empty pane.
111
- if (msg.groupId) fm.push(`groupId: ${msg.groupId}`);
111
+ if (msg.sessionId) fm.push(`sessionId: ${msg.sessionId}`);
112
112
  if (msg.chatId) fm.push(`chatId: ${msg.chatId}`);
113
113
  // Group-chat attribution: when a VP authors an assistant turn (either
114
114
  // its own reply or a route_forward injection from another VP), stamp
@@ -225,7 +225,7 @@ export function parseMessage(raw) {
225
225
  case 'tokens_est': msg.tokens_est = parseInt(value, 10); break;
226
226
  case 'threadId': msg.threadId = value; break;
227
227
  case 'sourceThreadId': msg.sourceThreadId = value; break;
228
- case 'groupId': msg.groupId = value; break;
228
+ case 'sessionId': msg.sessionId = value; break;
229
229
  case 'chatId': msg.chatId = value; break;
230
230
  case 'speakerVpId': msg.speakerVpId = value; break;
231
231
  case 'attachmentsB64':
@@ -330,15 +330,15 @@ export function parseMessage(raw) {
330
330
  * messages/
331
331
  * cold/
332
332
  * blobs/
333
- * groups/<groupId>/conversation/
333
+ * groups/<sessionId>/conversation/
334
334
  * compact/
335
335
  * messages/
336
336
  * cold/
337
337
  * blobs/
338
338
  *
339
339
  * Legacy compatibility: ~/.yeaft/conversation is read as an old mixed store.
340
- * New writes are split by mode: records with groupId go to
341
- * groups/<groupId>/conversation/, all others go to chat/.
340
+ * New writes are split by mode: records with sessionId go to
341
+ * groups/<sessionId>/conversation/, all others go to chat/.
342
342
  */
343
343
  export class ConversationStore {
344
344
  #dir; // root dir (e.g. ~/.yeaft)
@@ -380,7 +380,7 @@ export class ConversationStore {
380
380
  this.#legacyMsgDir = join(this.#legacyConvDir, 'messages');
381
381
  this.#legacyColdDir = join(this.#legacyConvDir, 'cold');
382
382
 
383
- // Per-(groupId, vpId) compact summary files live under that group's
383
+ // Per-(sessionId, vpId) compact summary files live under that group's
384
384
  // conversation directory. The legacy ~/.yeaft/conversation/compact directory
385
385
  // is read for compatibility.
386
386
  this.#legacyCompactScopedDir = join(this.#legacyConvDir, 'compact');
@@ -389,7 +389,7 @@ export class ConversationStore {
389
389
 
390
390
  // Ensure new chat and group-root directories exist (graceful on permission
391
391
  // errors). Per-group conversation directories are created lazily once a
392
- // groupId is known. The legacy conversation directory is never created by
392
+ // sessionId is known. The legacy conversation directory is never created by
393
393
  // new versions.
394
394
  for (const d of [
395
395
  this.#chatDir, join(this.#chatDir, 'blobs'), this.#chatMsgDir, this.#chatColdDir,
@@ -535,7 +535,7 @@ export class ConversationStore {
535
535
  }
536
536
 
537
537
  /**
538
- * Sanitize one id (groupId or vpId) into a safe filename component.
538
+ * Sanitize one id (sessionId or vpId) into a safe filename component.
539
539
  * Anything outside `[A-Za-z0-9._-]` collapses to `_`; max 120 chars.
540
540
  * For directory path components, use `#safeDirComponent` instead; this
541
541
  * helper intentionally preserves historical compact-summary filenames.
@@ -553,27 +553,27 @@ export class ConversationStore {
553
553
  }
554
554
 
555
555
  /**
556
- * Sanitize a (groupId, vpId) pair into a safe filename. We accept
557
- * arbitrary user strings here (groupIds and vpIds are user-set), so
556
+ * Sanitize a (sessionId, vpId) pair into a safe filename. We accept
557
+ * arbitrary user strings here (sessionIds and vpIds are user-set), so
558
558
  * the result is purely a basename — never parsed back.
559
559
  *
560
- * @param {string} groupId
560
+ * @param {string} sessionId
561
561
  * @param {string} vpId
562
562
  * @returns {string|null} — full path, or null if either id missing
563
563
  */
564
- #scopedCompactPath(groupId, vpId) {
565
- if (!groupId || !vpId) return null;
566
- const compactDir = join(this.#groupConversationDir(groupId, { create: true }), 'compact');
564
+ #scopedCompactPath(sessionId, vpId) {
565
+ if (!sessionId || !vpId) return null;
566
+ const compactDir = join(this.#groupConversationDir(sessionId, { create: true }), 'compact');
567
567
  return join(compactDir, `${this.#safeIdComponent(vpId)}.md`);
568
568
  }
569
569
 
570
- #legacyScopedCompactPath(groupId, vpId) {
571
- if (!groupId || !vpId) return null;
572
- return join(this.#legacyCompactScopedDir, `${this.#safeIdComponent(groupId)}__${this.#safeIdComponent(vpId)}.md`);
570
+ #legacyScopedCompactPath(sessionId, vpId) {
571
+ if (!sessionId || !vpId) return null;
572
+ return join(this.#legacyCompactScopedDir, `${this.#safeIdComponent(sessionId)}__${this.#safeIdComponent(vpId)}.md`);
573
573
  }
574
574
 
575
575
  /**
576
- * Read a per-(groupId, vpId) compact summary. Returns '' if no summary
576
+ * Read a per-(sessionId, vpId) compact summary. Returns '' if no summary
577
577
  * has been written yet. Falls back to nothing — callers that need the
578
578
  * legacy global file should call `readCompactSummary()` explicitly.
579
579
  *
@@ -583,13 +583,13 @@ export class ConversationStore {
583
583
  * unrelated content and every VP read the same merged blob. See
584
584
  * `engine.#runOrchestratorCompact`.
585
585
  *
586
- * @param {string} groupId
586
+ * @param {string} sessionId
587
587
  * @param {string} vpId
588
588
  * @returns {string}
589
589
  */
590
- readCompactSummaryFor(groupId, vpId) {
591
- const path = this.#scopedCompactPath(groupId, vpId);
592
- const legacyPath = this.#legacyScopedCompactPath(groupId, vpId);
590
+ readCompactSummaryFor(sessionId, vpId) {
591
+ const path = this.#scopedCompactPath(sessionId, vpId);
592
+ const legacyPath = this.#legacyScopedCompactPath(sessionId, vpId);
593
593
  for (const candidate of [path, legacyPath]) {
594
594
  if (!candidate || !existsSync(candidate)) continue;
595
595
  try { return readFileSync(candidate, 'utf8'); }
@@ -599,16 +599,16 @@ export class ConversationStore {
599
599
  }
600
600
 
601
601
  /**
602
- * Rewrite a per-(groupId, vpId) compact summary in place. See
602
+ * Rewrite a per-(sessionId, vpId) compact summary in place. See
603
603
  * `replaceCompactSummary` for the rationale — same reason, scoped file.
604
604
  *
605
- * @param {string} groupId
605
+ * @param {string} sessionId
606
606
  * @param {string} vpId
607
607
  * @param {string} summary
608
608
  */
609
- replaceCompactSummaryFor(groupId, vpId, summary) {
609
+ replaceCompactSummaryFor(sessionId, vpId, summary) {
610
610
  if (typeof summary !== 'string' || !summary) return;
611
- const path = this.#scopedCompactPath(groupId, vpId);
611
+ const path = this.#scopedCompactPath(sessionId, vpId);
612
612
  if (!path) return;
613
613
  try {
614
614
  writeFileSync(path, summary, { encoding: 'utf8', mode: 0o644 });
@@ -625,22 +625,22 @@ export class ConversationStore {
625
625
  }
626
626
 
627
627
  /**
628
- * Check whether ANY per-(group, vp) compact summary exists for `groupId`.
628
+ * Check whether ANY per-(group, vp) compact summary exists for `sessionId`.
629
629
  * Used by the history-replay path to decide whether to flag
630
630
  * `hasCompactSummary` for the UI without committing to one VP's view.
631
631
  *
632
- * @param {string} groupId
632
+ * @param {string} sessionId
633
633
  * @returns {boolean}
634
634
  */
635
- hasAnyCompactSummaryForGroup(groupId) {
636
- if (!groupId) return false;
637
- const compactDir = join(this.#groupConversationDir(groupId), 'compact');
635
+ hasAnyCompactSummaryForGroup(sessionId) {
636
+ if (!sessionId) return false;
637
+ const compactDir = join(this.#groupConversationDir(sessionId), 'compact');
638
638
  for (const dir of [compactDir, this.#legacyCompactScopedDir]) {
639
639
  if (!existsSync(dir)) continue;
640
640
  try {
641
641
  for (const f of readdirSync(dir)) {
642
642
  if (dir === compactDir && f.endsWith('.md')) return true;
643
- if (dir === this.#legacyCompactScopedDir && f.startsWith(`${this.#safeIdComponent(groupId)}__`) && f.endsWith('.md')) return true;
643
+ if (dir === this.#legacyCompactScopedDir && f.startsWith(`${this.#safeIdComponent(sessionId)}__`) && f.endsWith('.md')) return true;
644
644
  }
645
645
  } catch { /* best-effort */ }
646
646
  }
@@ -755,11 +755,11 @@ export class ConversationStore {
755
755
  }
756
756
 
757
757
  /**
758
- * Load recent hot messages stamped with `groupId`, sliced to the last
758
+ * Load recent hot messages stamped with `sessionId`, sliced to the last
759
759
  * `turnsLimit` TURNS and sorted chronologically.
760
760
  *
761
761
  * Group-history-isolation (Bug 7): a message lives in exactly one
762
- * group. Messages without a `groupId` frontmatter (legacy / pre-
762
+ * group. Messages without a `sessionId` frontmatter (legacy / pre-
763
763
  * grouping) are NOT returned — they would otherwise leak into every
764
764
  * group's stream.
765
765
  *
@@ -782,26 +782,26 @@ export class ConversationStore {
782
782
  * typical inboxes (≤ a few thousand hot messages) this is cheap; if
783
783
  * it ever becomes a hot path we add a per-group on-disk index.
784
784
  *
785
- * @param {string} groupId — required; null/empty returns []
785
+ * @param {string} sessionId — required; null/empty returns []
786
786
  * @param {number} [turnsLimit=DEFAULT_RECENT_TURNS]
787
787
  * @returns {object[]}
788
788
  */
789
- loadRecentByGroup(groupId, turnsLimit = DEFAULT_RECENT_TURNS) {
790
- if (!groupId) return [];
791
- const all = this.#loadGroupMessages(groupId)
792
- const filtered = all.filter(m => m && m.groupId === groupId);
789
+ loadRecentByGroup(sessionId, turnsLimit = DEFAULT_RECENT_TURNS) {
790
+ if (!sessionId) return [];
791
+ const all = this.#loadGroupMessages(sessionId)
792
+ const filtered = all.filter(m => m && m.sessionId === sessionId);
793
793
  if (turnsLimit === Infinity || turnsLimit < 0) return pairSanitize(filtered);
794
794
  return pairSanitize(sliceLastNTurns(filtered, turnsLimit));
795
795
  }
796
796
 
797
797
  /**
798
- * Load every hot message stamped with `groupId`.
798
+ * Load every hot message stamped with `sessionId`.
799
799
  *
800
- * @param {string} groupId
800
+ * @param {string} sessionId
801
801
  * @returns {object[]}
802
802
  */
803
- loadAllByGroup(groupId) {
804
- return this.loadRecentByGroup(groupId, Infinity);
803
+ loadAllByGroup(sessionId) {
804
+ return this.loadRecentByGroup(sessionId, Infinity);
805
805
  }
806
806
 
807
807
  /**
@@ -825,16 +825,16 @@ export class ConversationStore {
825
825
  * The output is pair-safe by construction for THIS VP's tool arcs and
826
826
  * carries only summary-relevant text for the other VPs.
827
827
  *
828
- * @param {string} groupId
828
+ * @param {string} sessionId
829
829
  * @param {string} vpId
830
830
  * @returns {object[]}
831
831
  */
832
- loadGroupHistoryForVp(groupId, vpId) {
833
- if (!groupId || !vpId) return [];
834
- const all = this.#loadGroupMessages(groupId)
832
+ loadGroupHistoryForVp(sessionId, vpId) {
833
+ if (!sessionId || !vpId) return [];
834
+ const all = this.#loadGroupMessages(sessionId)
835
835
  const out = [];
836
836
  for (const m of all) {
837
- if (!m || m.groupId !== groupId) continue;
837
+ if (!m || m.sessionId !== sessionId) continue;
838
838
  if (m._reflection || m.internal || m.systemOnly || m.systemOnlyMessage) continue;
839
839
  if (m.role === 'user') {
840
840
  out.push(m);
@@ -871,7 +871,7 @@ export class ConversationStore {
871
871
 
872
872
  /**
873
873
  * Pagination-cursor read: load the page of `turnsLimit` TURNS that ends
874
- * just before `beforeSeq` (exclusive) for the given `groupId`. Used by
874
+ * just before `beforeSeq` (exclusive) for the given `sessionId`. Used by
875
875
  * the Yeaft "Load older messages" UI to walk backwards through history
876
876
  * one click at a time.
877
877
  *
@@ -889,7 +889,7 @@ export class ConversationStore {
889
889
  * are already pair-safe, but historical / hand-edited stores may
890
890
  * contain orphan tool_use/tool_result pairs.
891
891
  *
892
- * @param {string} groupId — required; null/empty returns empty result
892
+ * @param {string} sessionId — required; null/empty returns empty result
893
893
  * @param {number|null} beforeSeq — exclusive upper bound on message
894
894
  * sequence id. Special cases:
895
895
  * - `null` / `undefined` / non-finite (e.g. `Infinity`, `NaN`) → start
@@ -901,14 +901,14 @@ export class ConversationStore {
901
901
  * @param {number} [turnsLimit=DEFAULT_RECENT_TURNS] — max turns per page
902
902
  * @returns {{ messages: object[], oldestSeq: number|null, hasMore: boolean }}
903
903
  */
904
- loadOlderByGroup(groupId, beforeSeq, turnsLimit = DEFAULT_RECENT_TURNS) {
905
- if (!groupId) return { messages: [], oldestSeq: null, hasMore: false };
906
- const hot = this.#loadGroupHotMessages(groupId);
907
- const cold = this.#loadGroupColdMessages(groupId);
904
+ loadOlderByGroup(sessionId, beforeSeq, turnsLimit = DEFAULT_RECENT_TURNS) {
905
+ if (!sessionId) return { messages: [], oldestSeq: null, hasMore: false };
906
+ const hot = this.#loadGroupHotMessages(sessionId);
907
+ const cold = this.#loadGroupColdMessages(sessionId);
908
908
  // Cold ids strictly < hot ids by construction → chronological concat.
909
909
  const all = [...cold, ...hot];
910
910
  const cutoff = Number.isFinite(beforeSeq) ? beforeSeq : Infinity;
911
- const prefix = all.filter(m => m && m.groupId === groupId
911
+ const prefix = all.filter(m => m && m.sessionId === sessionId
912
912
  && parseSeqFromId(m.id) < cutoff);
913
913
  if (prefix.length === 0) return { messages: [], oldestSeq: null, hasMore: false };
914
914
  const sliced = pairSanitize(sliceLastNTurns(prefix, turnsLimit));
@@ -935,17 +935,17 @@ export class ConversationStore {
935
935
  * window, so a dense run of hidden metadata cannot force the first screen to
936
936
  * scan and materialize the group's entire history in the web bridge.
937
937
  *
938
- * @param {string} groupId
938
+ * @param {string} sessionId
939
939
  * @param {number|null} beforeSeq — exclusive upper bound, or null for newest
940
940
  * @param {number} [turnsLimit=DEFAULT_RECENT_TURNS]
941
941
  * @returns {{ messages: object[], oldestSeq: number|null, hasMore: boolean }}
942
942
  */
943
- loadVisibleByGroup(groupId, beforeSeq, turnsLimit = DEFAULT_RECENT_TURNS) {
944
- if (!groupId || !(turnsLimit > 0)) return { messages: [], oldestSeq: null, hasMore: false };
943
+ loadVisibleByGroup(sessionId, beforeSeq, turnsLimit = DEFAULT_RECENT_TURNS) {
944
+ if (!sessionId || !(turnsLimit > 0)) return { messages: [], oldestSeq: null, hasMore: false };
945
945
 
946
946
  const cutoff = Number.isFinite(beforeSeq) ? beforeSeq : Infinity;
947
- const hot = this.#loadVisibleFromDirsByGroup([...this.#groupMessageDirs('messages', groupId), this.#legacyMsgDir], groupId, cutoff);
948
- const cold = this.#loadVisibleFromDirsByGroup([...this.#groupMessageDirs('cold', groupId), this.#legacyColdDir], groupId, cutoff);
947
+ const hot = this.#loadVisibleFromDirsByGroup([...this.#groupMessageDirs('messages', sessionId), this.#legacyMsgDir], sessionId, cutoff);
948
+ const cold = this.#loadVisibleFromDirsByGroup([...this.#groupMessageDirs('cold', sessionId), this.#legacyColdDir], sessionId, cutoff);
949
949
  const visible = [...cold, ...hot];
950
950
  if (visible.length === 0) return { messages: [], oldestSeq: null, hasMore: false };
951
951
 
@@ -1018,9 +1018,9 @@ export class ConversationStore {
1018
1018
  // ─── Internal ───────────────────────────────────────────
1019
1019
 
1020
1020
  /**
1021
- * Delete every persisted message stamped with `groupId`. Scans both hot
1021
+ * Delete every persisted message stamped with `sessionId`. Scans both hot
1022
1022
  * (`messages/`) and cold (`cold/`) directories and `unlink`s matching
1023
- * files. Messages without a `groupId` frontmatter are NOT touched —
1023
+ * files. Messages without a `sessionId` frontmatter are NOT touched —
1024
1024
  * they may be legitimate pre-grouping legacy messages and are handled
1025
1025
  * by `compactOrphans` instead.
1026
1026
  *
@@ -1031,11 +1031,11 @@ export class ConversationStore {
1031
1031
  * Idempotent and safe: missing dirs / unparseable files are skipped.
1032
1032
  * Returns the number of message files removed.
1033
1033
  *
1034
- * @param {string} groupId
1034
+ * @param {string} sessionId
1035
1035
  * @returns {number}
1036
1036
  */
1037
- deleteByGroup(groupId) {
1038
- if (!groupId) return 0;
1037
+ deleteByGroup(sessionId) {
1038
+ if (!sessionId) return 0;
1039
1039
  let removed = 0;
1040
1040
  for (const dir of [this.#chatMsgDir, this.#chatColdDir, ...this.#groupMessageDirs('messages'), ...this.#groupMessageDirs('cold'), this.#legacyMsgDir, this.#legacyColdDir]) {
1041
1041
  if (!existsSync(dir)) continue;
@@ -1056,7 +1056,7 @@ export class ConversationStore {
1056
1056
  throw err;
1057
1057
  }
1058
1058
  const msg = parseMessage(raw);
1059
- if (!msg || msg.groupId !== groupId) continue;
1059
+ if (!msg || msg.sessionId !== sessionId) continue;
1060
1060
  try {
1061
1061
  unlinkSync(path);
1062
1062
  removed += 1;
@@ -1073,7 +1073,7 @@ export class ConversationStore {
1073
1073
 
1074
1074
  /**
1075
1075
  * Sweep messages that don't belong to any live group. A message is
1076
- * considered an orphan when its frontmatter `groupId`:
1076
+ * considered an orphan when its frontmatter `sessionId`:
1077
1077
  * - is missing entirely (legacy / pre-grouping); OR
1078
1078
  * - is set to a value not in `keepGroupIds`.
1079
1079
  *
@@ -1116,7 +1116,7 @@ export class ConversationStore {
1116
1116
  const msg = parseMessage(raw);
1117
1117
  if (!msg) continue;
1118
1118
  scanned += 1;
1119
- const isOrphan = !msg.groupId || !keep.has(msg.groupId);
1119
+ const isOrphan = !msg.sessionId || !keep.has(msg.sessionId);
1120
1120
  if (!isOrphan) continue;
1121
1121
  orphans.push(path);
1122
1122
  if (dryRun) continue;
@@ -1357,8 +1357,8 @@ export class ConversationStore {
1357
1357
 
1358
1358
  #messageDirFor(msg) {
1359
1359
  if (msg?.chatId) return join(this.#chatConversationDir(msg.chatId, { create: true }), 'messages');
1360
- if (!msg?.groupId) return this.#chatMsgDir;
1361
- return join(this.#groupConversationDir(msg.groupId, { create: true }), 'messages');
1360
+ if (!msg?.sessionId) return this.#chatMsgDir;
1361
+ return join(this.#groupConversationDir(msg.sessionId, { create: true }), 'messages');
1362
1362
  }
1363
1363
 
1364
1364
  #chatConversationDir(chatId, { create = false } = {}) {
@@ -1456,8 +1456,8 @@ export class ConversationStore {
1456
1456
  return pairSanitize(out);
1457
1457
  }
1458
1458
 
1459
- #groupConversationDir(groupId, { create = false } = {}) {
1460
- const dir = join(this.#groupsDir, this.#safeDirComponent(groupId), 'conversation');
1459
+ #groupConversationDir(sessionId, { create = false } = {}) {
1460
+ const dir = join(this.#groupsDir, this.#safeDirComponent(sessionId), 'conversation');
1461
1461
  if (create) this.#ensureConversationDirs(dir);
1462
1462
  return dir;
1463
1463
  }
@@ -1472,22 +1472,22 @@ export class ConversationStore {
1472
1472
  if (!existsSync(this.#groupsDir)) return [];
1473
1473
  const dirs = [];
1474
1474
  for (const name of readdirSync(this.#groupsDir)) {
1475
- const groupDir = join(this.#groupsDir, name);
1475
+ const sessionDir = join(this.#groupsDir, name);
1476
1476
  try {
1477
- if (!statSync(groupDir).isDirectory()) continue;
1477
+ if (!statSync(sessionDir).isDirectory()) continue;
1478
1478
  } catch (err) {
1479
1479
  if (isPermissionError(err)) continue;
1480
1480
  throw err;
1481
1481
  }
1482
- const conversationDir = join(groupDir, 'conversation');
1482
+ const conversationDir = join(sessionDir, 'conversation');
1483
1483
  if (existsSync(conversationDir)) dirs.push(conversationDir);
1484
1484
  }
1485
1485
  return dirs;
1486
1486
  }
1487
1487
 
1488
- #groupMessageDirs(kind, groupId = null) {
1489
- if (groupId) {
1490
- const dir = join(this.#groupConversationDir(groupId), kind);
1488
+ #groupMessageDirs(kind, sessionId = null) {
1489
+ if (sessionId) {
1490
+ const dir = join(this.#groupConversationDir(sessionId), kind);
1491
1491
  return existsSync(dir) ? [dir] : [];
1492
1492
  }
1493
1493
  return this.#groupConversationDirs()
@@ -1507,29 +1507,29 @@ export class ConversationStore {
1507
1507
  #loadChatMessages() {
1508
1508
  // Legacy ~/.yeaft/conversation held both chat and group records. For chat
1509
1509
  // mode compatibility, only import legacy records that are not stamped with
1510
- // a groupId, so group mode cannot bleed into chat.
1510
+ // a sessionId, so group mode cannot bleed into chat.
1511
1511
  return [
1512
- ...this.#loadFromDir(this.#legacyMsgDir, Infinity).filter(m => !m?.groupId),
1512
+ ...this.#loadFromDir(this.#legacyMsgDir, Infinity).filter(m => !m?.sessionId),
1513
1513
  ...this.#loadFromDir(this.#chatMsgDir, Infinity),
1514
1514
  ].sort(compareMessagesBySeq);
1515
1515
  }
1516
1516
 
1517
- #loadGroupHotMessages(groupId = null) {
1517
+ #loadGroupHotMessages(sessionId = null) {
1518
1518
  return [
1519
- ...this.#loadFromDir(this.#legacyMsgDir, Infinity).filter(m => m?.groupId),
1520
- ...this.#groupMessageDirs('messages', groupId).flatMap(dir => this.#loadFromDir(dir, Infinity)),
1519
+ ...this.#loadFromDir(this.#legacyMsgDir, Infinity).filter(m => m?.sessionId),
1520
+ ...this.#groupMessageDirs('messages', sessionId).flatMap(dir => this.#loadFromDir(dir, Infinity)),
1521
1521
  ].sort(compareMessagesBySeq);
1522
1522
  }
1523
1523
 
1524
- #loadGroupColdMessages(groupId = null) {
1524
+ #loadGroupColdMessages(sessionId = null) {
1525
1525
  return [
1526
- ...this.#loadFromDir(this.#legacyColdDir, Infinity).filter(m => m?.groupId),
1527
- ...this.#groupMessageDirs('cold', groupId).flatMap(dir => this.#loadFromDir(dir, Infinity)),
1526
+ ...this.#loadFromDir(this.#legacyColdDir, Infinity).filter(m => m?.sessionId),
1527
+ ...this.#groupMessageDirs('cold', sessionId).flatMap(dir => this.#loadFromDir(dir, Infinity)),
1528
1528
  ].sort(compareMessagesBySeq);
1529
1529
  }
1530
1530
 
1531
- #loadGroupMessages(groupId = null) {
1532
- return [...this.#loadGroupColdMessages(groupId), ...this.#loadGroupHotMessages(groupId)].sort(compareMessagesBySeq);
1531
+ #loadGroupMessages(sessionId = null) {
1532
+ return [...this.#loadGroupColdMessages(sessionId), ...this.#loadGroupHotMessages(sessionId)].sort(compareMessagesBySeq);
1533
1533
  }
1534
1534
 
1535
1535
  #loadAllMessages() {
@@ -1556,8 +1556,8 @@ export class ConversationStore {
1556
1556
  return total;
1557
1557
  }
1558
1558
 
1559
- #loadVisibleFromDirsByGroup(dirs, groupId, beforeSeq) {
1560
- return dirs.flatMap(dir => this.#loadVisibleFromDirByGroup(dir, groupId, beforeSeq))
1559
+ #loadVisibleFromDirsByGroup(dirs, sessionId, beforeSeq) {
1560
+ return dirs.flatMap(dir => this.#loadVisibleFromDirByGroup(dir, sessionId, beforeSeq))
1561
1561
  .sort(compareMessagesBySeq);
1562
1562
  }
1563
1563
 
@@ -1619,7 +1619,7 @@ export class ConversationStore {
1619
1619
  return messages;
1620
1620
  }
1621
1621
 
1622
- #loadVisibleFromDirByGroup(dir, groupId, beforeSeq) {
1622
+ #loadVisibleFromDirByGroup(dir, sessionId, beforeSeq) {
1623
1623
  if (!existsSync(dir)) return [];
1624
1624
 
1625
1625
  const files = readdirSync(dir)
@@ -1632,11 +1632,11 @@ export class ConversationStore {
1632
1632
  if (!Number.isFinite(seq) || seq >= beforeSeq) continue;
1633
1633
 
1634
1634
  const raw = readFileSync(join(dir, file), 'utf8');
1635
- if (!raw.includes(`groupId: ${groupId}`)) continue;
1635
+ if (!raw.includes(`sessionId: ${sessionId}`)) continue;
1636
1636
  if (!raw.includes('role: user') && !raw.includes('role: assistant')) continue;
1637
1637
 
1638
1638
  const parsed = parseMessage(raw);
1639
- if (!parsed || parsed.groupId !== groupId) continue;
1639
+ if (!parsed || parsed.sessionId !== sessionId) continue;
1640
1640
  if (parsed._reflection || parsed.internal || parsed.systemOnly || parsed.systemOnlyMessage) continue;
1641
1641
  if (parsed.role !== 'user' && parsed.role !== 'assistant') continue;
1642
1642
  out.push(parsed);
@@ -209,16 +209,16 @@ export class DebugTrace {
209
209
 
210
210
  /**
211
211
  * Start a new turn.
212
- * @param {{ traceId: string, messageId?: string, mode?: string, turnNumber?: number, groupId?: string, vpId?: string, threadId?: string, userPrompt?: string }} opts
212
+ * @param {{ traceId: string, messageId?: string, mode?: string, turnNumber?: number, sessionId?: string, vpId?: string, threadId?: string, userPrompt?: string }} opts
213
213
  * @returns {string} — turnId
214
214
  */
215
- startTurn({ traceId, messageId = null, mode = null, turnNumber = null, groupId = null, vpId = null, threadId = null, userPrompt = null }) {
215
+ startTurn({ traceId, messageId = null, mode = null, turnNumber = null, sessionId = null, vpId = null, threadId = null, userPrompt = null }) {
216
216
  const id = randomUUID();
217
217
  const now = Date.now();
218
218
  this.#prepare('insertTurn', `
219
219
  INSERT INTO trace_turns (id, trace_id, message_id, mode, turn_number, started_at, group_id, vp_id, thread_id, user_prompt)
220
220
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
221
- `).run(id, traceId, messageId, mode, turnNumber, now, groupId, vpId, threadId, truncate(userPrompt, MAX_LOOP_PAYLOAD));
221
+ `).run(id, traceId, messageId, mode, turnNumber, now, sessionId, vpId, threadId, truncate(userPrompt, MAX_LOOP_PAYLOAD));
222
222
  return id;
223
223
  }
224
224
 
@@ -406,17 +406,17 @@ export class DebugTrace {
406
406
  * fields the panel expects. JSON columns are parsed; truncated /
407
407
  * malformed payloads degrade to null instead of failing the call.
408
408
  *
409
- * @param {{ limit?: number, dreamLimit?: number, groupId?: string|null, threadId?: string|null }} [opts]
409
+ * @param {{ limit?: number, dreamLimit?: number, sessionId?: string|null, threadId?: string|null }} [opts]
410
410
  * @returns {{ loops: object[], turns: object[], dreamEvents: object[] }}
411
411
  */
412
- fetchRecentDebugHistory({ limit = 100, dreamLimit = 5, groupId = null, threadId = null } = {}) {
412
+ fetchRecentDebugHistory({ limit = 100, dreamLimit = 5, sessionId = null, threadId = null } = {}) {
413
413
  const lim = Math.max(1, Math.min(500, Number(limit) || 100));
414
414
  const dreamLim = Number.isFinite(Number(dreamLimit))
415
415
  ? Math.max(0, Math.min(50, Number(dreamLimit)))
416
416
  : 5;
417
417
  const where = [];
418
418
  const args = [];
419
- if (groupId) { where.push('group_id = ?'); args.push(groupId); }
419
+ if (sessionId) { where.push('group_id = ?'); args.push(sessionId); }
420
420
  if (threadId) { where.push('thread_id = ?'); args.push(threadId); }
421
421
  const sql = `
422
422
  SELECT * FROM trace_turns
@@ -437,7 +437,7 @@ export class DebugTrace {
437
437
  try { return JSON.parse(s); }
438
438
  catch { return null; }
439
439
  };
440
- // Group rows by (turnId, threadId, groupId, vpId) → frontend Turn
440
+ // Group rows by (turnId, threadId, sessionId, vpId) → frontend Turn
441
441
  // record. Each row is also surfaced as a Loop.
442
442
  const turnsById = new Map();
443
443
  const loops = rows.map((r) => {
@@ -461,7 +461,7 @@ export class DebugTrace {
461
461
  stopReason: r.stop_reason || null,
462
462
  rawRequest: r.raw_request || null,
463
463
  rawResponse: r.raw_response || null,
464
- groupId: r.group_id || null,
464
+ sessionId: r.group_id || null,
465
465
  vpId: r.vp_id || null,
466
466
  threadId: r.thread_id || null,
467
467
  };
@@ -474,7 +474,7 @@ export class DebugTrace {
474
474
  // cumulative conversation snapshot, so `messages[0].content`
475
475
  // would be turn-1's prompt for every subsequent turn header.
476
476
  userPrompt: r.user_prompt || '',
477
- groupId: r.group_id || null,
477
+ sessionId: r.group_id || null,
478
478
  vpId: r.vp_id || null,
479
479
  threadId: r.thread_id || null,
480
480
  openedAt: r.started_at || 0,
@@ -525,11 +525,11 @@ export class DebugTrace {
525
525
  `).all(Math.max(dreamLim * 5, dreamLim));
526
526
  for (const er of eventRows) {
527
527
  const data = parseJsonSafe(er.event_data) || {};
528
- const evtGroupId = typeof data.groupId === 'string' && data.groupId ? data.groupId : null;
528
+ const evtGroupId = typeof data.sessionId === 'string' && data.sessionId ? data.sessionId : null;
529
529
  const target = typeof data.target === 'string' ? data.target : '';
530
- if (groupId) {
530
+ if (sessionId) {
531
531
  const isBroadcast = !evtGroupId && !target;
532
- const isThisGroup = evtGroupId === groupId || target === `group/${groupId}`;
532
+ const isThisGroup = evtGroupId === sessionId || target === `group/${sessionId}`;
533
533
  if (!isBroadcast && !isThisGroup) continue;
534
534
  }
535
535
  dreamEvents.push({
@@ -36,13 +36,13 @@ function applySystem(language) {
36
36
 
37
37
  /**
38
38
  * Build the UPDATE prompt body. Accepts the current scope state +
39
- * one or more `(groupId, diff)` source blocks.
39
+ * one or more `(sessionId, diff)` source blocks.
40
40
  *
41
41
  * @param {{
42
42
  * target: string,
43
43
  * memoryMd: string,
44
44
  * summaryMd: string,
45
- * sources: Array<{ groupId: string, diff: Array<object> }>,
45
+ * sources: Array<{ sessionId: string, diff: Array<object> }>,
46
46
  * batchInfo?: { index: number, total: number },
47
47
  * }} ctx
48
48
  */
@@ -68,7 +68,7 @@ export function buildUpdatePrompt(ctx) {
68
68
  *
69
69
  * @param {{
70
70
  * target: string,
71
- * sources: Array<{ groupId: string, diff: Array<object> }>,
71
+ * sources: Array<{ sessionId: string, diff: Array<object> }>,
72
72
  * siblingTopics?: Array<{ path: string, summary: string }>,
73
73
  * }} ctx
74
74
  */
@@ -89,17 +89,17 @@ export function buildCreatePrompt(ctx) {
89
89
  }
90
90
 
91
91
  /**
92
- * Render a list of `(groupId, diff)` source blocks for inclusion in the
92
+ * Render a list of `(sessionId, diff)` source blocks for inclusion in the
93
93
  * update / create prompts. Single-source aware: omits leading blank line
94
94
  * if there's only one source, to keep small prompts compact.
95
95
  *
96
- * @param {Array<{ groupId: string, diff: Array<object> }>} sources
96
+ * @param {Array<{ sessionId: string, diff: Array<object> }>} sources
97
97
  */
98
98
  function renderSourceBlocks(sources, language) {
99
99
  const out = [];
100
100
  for (const src of (sources || [])) {
101
101
  out.push('');
102
- out.push(`[group/${src.groupId}]`);
102
+ out.push(`[group/${src.sessionId}]`);
103
103
  for (const m of (src.diff || [])) {
104
104
  const head = `[${m.role || 'message'}${m.kind === 'overlap' ? (String(language || '').toLowerCase().startsWith('zh') ? '(已处理)' : ' (already processed)') : ''}]`;
105
105
  out.push(head);
@@ -128,19 +128,19 @@ export function targetToScope(target) {
128
128
  if (segs.length === 2) return { kind: 'group', id: segs[1] };
129
129
  // group/<g>/user
130
130
  if (segs.length === 3 && segs[2] === 'user') {
131
- return { kind: 'group-user', groupId: segs[1] };
131
+ return { kind: 'group-user', sessionId: segs[1] };
132
132
  }
133
133
  // group/<g>/vp/<v>
134
134
  if (segs.length === 4 && segs[2] === 'vp') {
135
- return { kind: 'group-vp', groupId: segs[1], id: segs[3] };
135
+ return { kind: 'group-vp', sessionId: segs[1], id: segs[3] };
136
136
  }
137
137
  // group/<g>/feature/<f>
138
138
  if (segs.length === 4 && segs[2] === 'feature') {
139
- return { kind: 'group-feature', groupId: segs[1], id: segs[3] };
139
+ return { kind: 'group-feature', sessionId: segs[1], id: segs[3] };
140
140
  }
141
141
  // group/<g>/topic/<l1>[/<l2>]
142
142
  if (segs[2] === 'topic' && (segs.length === 4 || segs.length === 5)) {
143
- return { kind: 'group-topic', groupId: segs[1], path: segs.slice(3) };
143
+ return { kind: 'group-topic', sessionId: segs[1], path: segs.slice(3) };
144
144
  }
145
145
  }
146
146
  if (segs[0] === 'chat') {
@@ -159,7 +159,7 @@ export function targetToScope(target) {
159
159
  * @param {{
160
160
  * target: string,
161
161
  * kind: 'update'|'create',
162
- * sources: Array<{ groupId: string, diff: any }>,
162
+ * sources: Array<{ sessionId: string, diff: any }>,
163
163
  * }} merged
164
164
  * @param {{
165
165
  * root: string,
@@ -276,10 +276,10 @@ function scopeRelDir(scope) {
276
276
  switch (scope.kind) {
277
277
  case 'user': return 'user';
278
278
  case 'group': return `group/${scope.id}`;
279
- case 'group-user': return `group/${scope.groupId}/user`;
280
- case 'group-vp': return `group/${scope.groupId}/vp/${scope.id}`;
281
- case 'group-feature': return `group/${scope.groupId}/feature/${scope.id}`;
282
- case 'group-topic': return `group/${scope.groupId}/topic/${scope.path.join('/')}`;
279
+ case 'group-user': return `group/${scope.sessionId}/user`;
280
+ case 'group-vp': return `group/${scope.sessionId}/vp/${scope.id}`;
281
+ case 'group-feature': return `group/${scope.sessionId}/feature/${scope.id}`;
282
+ case 'group-topic': return `group/${scope.sessionId}/topic/${scope.path.join('/')}`;
283
283
  case 'chat': return `chat/${scope.id}`;
284
284
  case 'chat-vp': return `chat/${scope.chatId}/vp/${scope.id}`;
285
285
  default: throw new Error(`apply.scopeRelDir: unknown kind ${scope.kind}`);