@yeaft/webchat-agent 0.1.874 → 0.1.876

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
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * [
14
14
  * {
15
- * groupId: 'g-eng',
15
+ * sessionId: 'g-eng',
16
16
  * diff: [<message>, ...], // the per-group source diff
17
17
  * // (already truncated/segmented if needed)
18
18
  * actions: [
@@ -32,36 +32,36 @@
32
32
  * { target: 'user',
33
33
  * kind: 'update', // 'update' wins over 'create' if any group says update
34
34
  * sources: [
35
- * { groupId: 'g-eng', diff: [...] },
36
- * { groupId: 'g-life', diff: [...] },
35
+ * { sessionId: 'g-eng', diff: [...] },
36
+ * { sessionId: 'g-life', diff: [...] },
37
37
  * ],
38
38
  * },
39
39
  * { target: 'topic/life/parenting',
40
40
  * kind: 'create', // create only if every contributing group said create
41
- * sources: [{ groupId: 'g-life', diff: [...] }],
41
+ * sources: [{ sessionId: 'g-life', diff: [...] }],
42
42
  * },
43
43
  * ...
44
44
  * ]
45
45
  *
46
46
  * Determinism contract:
47
47
  * - Targets are returned sorted alphabetically by target path.
48
- * - Within a target, sources are sorted by groupId.
48
+ * - Within a target, sources are sorted by sessionId.
49
49
  * This makes the debug-panel output predictable across runs.
50
50
  */
51
51
 
52
52
  /**
53
53
  * Merge per-group triage outputs into per-target apply units.
54
54
  *
55
- * @param {Array<{ groupId: string, diff: any, actions: Array<{ kind: 'update'|'create', scope: string }> }>} groupTriages
56
- * @returns {Array<{ target: string, kind: 'update'|'create', sources: Array<{ groupId: string, diff: any }> }>}
55
+ * @param {Array<{ sessionId: string, diff: any, actions: Array<{ kind: 'update'|'create', scope: string }> }>} groupTriages
56
+ * @returns {Array<{ target: string, kind: 'update'|'create', sources: Array<{ sessionId: string, diff: any }> }>}
57
57
  */
58
58
  export function mergeByTarget(groupTriages) {
59
59
  const byTarget = new Map();
60
60
  for (const g of (groupTriages || [])) {
61
- const groupId = g && g.groupId;
61
+ const sessionId = g && g.sessionId;
62
62
  const diff = g && g.diff;
63
63
  const actions = Array.isArray(g && g.actions) ? g.actions : [];
64
- if (!groupId) continue;
64
+ if (!sessionId) continue;
65
65
  for (const a of actions) {
66
66
  if (!a || !a.scope) continue;
67
67
  const k = a.kind === 'create' ? 'create' : 'update';
@@ -75,13 +75,13 @@ export function mergeByTarget(groupTriages) {
75
75
  if (k === 'update') entry.kind = 'update';
76
76
  // Avoid duplicate (target, group) pairs — should never happen
77
77
  // in normal triage but be defensive.
78
- if (!entry.sources.some(s => s.groupId === groupId)) {
79
- entry.sources.push({ groupId, diff });
78
+ if (!entry.sources.some(s => s.sessionId === sessionId)) {
79
+ entry.sources.push({ sessionId, diff });
80
80
  }
81
81
  }
82
82
  }
83
83
  const out = Array.from(byTarget.values());
84
84
  out.sort((a, b) => a.target.localeCompare(b.target));
85
- for (const e of out) e.sources.sort((a, b) => a.groupId.localeCompare(b.groupId));
85
+ for (const e of out) e.sources.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
86
86
  return out;
87
87
  }
@@ -1,23 +1,23 @@
1
- # Dream Extract — Group Scope
1
+ # Dream Extract — Session Scope
2
2
 
3
3
  You are extracting **memory segments** from a conversation between the
4
4
  user and a Yeaft AI companion. This pass focuses on a specific
5
- **`group/<id>` scope**: long-lived facts about one collaboration group
5
+ **`session/<id>` scope**: long-lived facts about one collaboration session
6
6
  (a project team, a study cohort, a working set of people/agents).
7
7
 
8
- The target group id is provided as `{{groupId}}`.
8
+ The target session id is provided as `{{sessionId}}`.
9
9
 
10
- ## What to extract for `group/<id>` scope
10
+ ## What to extract for `session/<id>` scope
11
11
 
12
- - **purpose** — what this group exists to do, its charter / mission
13
- - **members** — people, VPs, and roles in the group, and what each is
12
+ - **purpose** — what this session exists to do, its charter / mission
13
+ - **members** — people, VPs, and roles in the session, and what each is
14
14
  responsible for
15
- - **conventions** — how the group works (rituals, cadences, naming,
15
+ - **conventions** — how the session works (rituals, cadences, naming,
16
16
  channels, languages used)
17
17
  - **shared decisions** — durable agreements ("we ship on Fridays",
18
18
  "all PRs need two reviewers")
19
- - **shared context** — domain knowledge the whole group relies on
20
- - **relations** — other groups, features, or topics this group owns or
19
+ - **shared context** — domain knowledge the whole session relies on
20
+ - **relations** — other sessions, features, or topics this session owns or
21
21
  depends on
22
22
  - **lessons** — collective takeaways ("we tried X in Q1, it didn't
23
23
  scale, switched to Y")
@@ -28,7 +28,7 @@ The target group id is provided as `{{groupId}}`.
28
28
  - Single-VP traits — those go to that VP's `vp/<id>` scope.
29
29
  - Feature-specific implementation detail — those go to
30
30
  `feature/<id>` scope.
31
- - Transient status updates — only durable group facts.
31
+ - Transient status updates — only durable session facts.
32
32
 
33
33
  ## Segment shape
34
34
 
@@ -45,7 +45,7 @@ Reply with a JSON array of segment objects:
45
45
  "kind": "decision",
46
46
  "tags": ["process", "review"],
47
47
  "sourceMessages": ["m_201"],
48
- "body": "Group {{groupId}} decided every PR touching the payments
48
+ "body": "Session {{sessionId}} decided every PR touching the payments
49
49
  module needs sign-off from both the security VP and the payments
50
50
  feature owner before merge. Rationale: a near-miss in March."
51
51
  }
@@ -54,4 +54,4 @@ Reply with a JSON array of segment objects:
54
54
 
55
55
  `kind` ∈ {`fact`, `preference`, `decision`, `lesson`, `relation`,
56
56
  `goal`, `context`}. `scope` is filled in by the runner — do not include
57
- it. If nothing group-scope is in this batch, return `[]`.
57
+ it. If nothing session-scope is in this batch, return `[]`.
@@ -20,7 +20,7 @@ const FILES = {
20
20
  // H2.e — per-scope segment extraction prompts (one per scope family)
21
21
  extractUser: 'extract-user.md',
22
22
  extractVp: 'extract-vp.md',
23
- extractGroup: 'extract-group.md',
23
+ extractSession: 'extract-session.md',
24
24
  extractTopic: 'extract-topic.md',
25
25
  // H2.e — per-scope summary compression
26
26
  summarizeScope: 'summarize-scope.md',
@@ -46,10 +46,10 @@ export function extractTemplateForScope(scope) {
46
46
  if (/^group\/[^/]+\/vp\//.test(scope)) return 'extractVp';
47
47
  if (/^group\/[^/]+\/topic\//.test(scope)) return 'extractTopic';
48
48
  if (/^group\/[^/]+\/user(?:\/|$)/.test(scope)) return 'extractUser';
49
- if (scope.startsWith('group/')) return 'extractGroup';
49
+ if (scope.startsWith('group/')) return 'extractSession';
50
50
  // Chat-isolated scopes: same template family as groups.
51
51
  if (/^chat\/[^/]+\/vp\//.test(scope)) return 'extractVp';
52
- if (scope.startsWith('chat/')) return 'extractGroup';
52
+ if (scope.startsWith('chat/')) return 'extractSession';
53
53
  // Legacy top-level vp/topic scopes (archived to .legacy/ on boot — kept
54
54
  // here defensively in case something still constructs the old strings).
55
55
  if (scope.startsWith('vp/')) return 'extractVp';
@@ -4,7 +4,7 @@ You are deciding whether a recent group conversation carries:
4
4
 
5
5
  Do NOT mention vp/, group/, or feature/ scopes — those are handled by hard rules.
6
6
 
7
- Group: {{groupId}}
7
+ Session: {{sessionId}}
8
8
 
9
9
  Existing topic scopes (path — summary):
10
10
  {{topicSummaries}}
@@ -5,12 +5,12 @@
5
5
  *
6
6
  * trigger
7
7
  * ↓
8
- * enumerateGroups() via opts.listGroups()
8
+ * enumerateGroups() via opts.listSessions()
9
9
  * ↓
10
10
  * for each group with newCount ≥ MIN_NEW_PER_GROUP (auto)
11
11
  * or > 0 (manual)
12
12
  * or prior messages in a scoped manual group rerun:
13
- * loadDiff() via opts.loadGroupDiff(groupId, sinceId)
13
+ * loadDiff() via opts.loadGroupDiff(sessionId, sinceId)
14
14
  * applyOverlap() via opts.loadOverlapPreamble(...)
15
15
  * segment() segmentDiff(...)
16
16
  * triageGroupSegments() → group-local actions[]
@@ -58,10 +58,10 @@ import { tsForBackup, pruneOldSnapshots } from './snapshot.js';
58
58
  * @property {boolean} [manual=false] — manual trigger overrides newCount<20 skip
59
59
  * @property {string[]} [scopeFilter] — optional: only dream these targets; scoped manual group triggers rerun the current group when there are prior messages but no new cursor delta ('*' allowed)
60
60
  * @property {(req: {pass:string, prompt:string, system:string}) => Promise<string>} llm
61
- * @property {() => Promise<Array<string>>} listGroups — return all group ids (incl. '_no-group')
62
- * @property {(groupId: string) => Promise<number>} countMessages — total message count for a group
63
- * @property {(groupId: string, sinceMessageId: string|null) => Promise<Array<object>>} loadGroupDiff
64
- * @property {(groupId: string, beforeMessageId: string|null, count: number) => Promise<Array<object>>} loadOverlapPreamble
61
+ * @property {() => Promise<Array<string>>} listSessions — return all group ids (incl. '_no-group')
62
+ * @property {(sessionId: string) => Promise<number>} countMessages — total message count for a group
63
+ * @property {(sessionId: string, sinceMessageId: string|null) => Promise<Array<object>>} loadGroupDiff
64
+ * @property {(sessionId: string, beforeMessageId: string|null, count: number) => Promise<Array<object>>} loadOverlapPreamble
65
65
  * @property {() => Promise<Array<{path:string, summary:string}>>} [listTopicSummaries]
66
66
  * @property {(target: string) => Promise<Array<{path:string, summary:string}>>} [siblingTopicsFor]
67
67
  * @property {(event: object) => void} [onProgress]
@@ -87,7 +87,7 @@ export async function runDream(opts) {
87
87
  onProgress({ phase: 'start', manual: !!opts.manual, ts });
88
88
 
89
89
  // 1. enumerate groups
90
- const groupIds = await safeCall(opts.listGroups, []);
90
+ const sessionIds = await safeCall(opts.listSessions, []);
91
91
  const filter = Array.isArray(opts.scopeFilter) ? new Set(opts.scopeFilter) : null;
92
92
  const groupFilter = deriveGroupFilter(filter);
93
93
  const groupsReport = [];
@@ -97,53 +97,53 @@ export async function runDream(opts) {
97
97
  // 2. per-group: skip / segment / triage
98
98
  // Topic summaries are now per-group (group/<g>/topic/...), so resolve
99
99
  // them inside the per-group loop instead of once up front.
100
- const resolveTopicSummaries = async (groupId) => {
100
+ const resolveTopicSummaries = async (sessionId) => {
101
101
  if (opts.listTopicSummaries) {
102
- return await safeCall(() => opts.listTopicSummaries(groupId), []);
102
+ return await safeCall(() => opts.listTopicSummaries(sessionId), []);
103
103
  }
104
- return await defaultListTopicSummaries(opts.root, groupId, opts.language).catch(() => []);
104
+ return await defaultListTopicSummaries(opts.root, sessionId, opts.language).catch(() => []);
105
105
  };
106
106
 
107
- for (const groupId of groupIds) {
107
+ for (const sessionId of sessionIds) {
108
108
  // Current-group manual dream passes are the one case where scopeFilter
109
109
  // must constrain enumeration too: clicking the conversation header means
110
110
  // "dream this group now", not "triage every group and then only apply
111
111
  // group/<id>". Pure target filters such as ['user'] still triage every
112
112
  // group so their hard-rule actions can contribute to the requested scope.
113
- if (groupFilter && !groupFilter.has(groupId)) {
114
- groupsReport.push({ groupId, new: 0, status: 'skipped', reason: 'scope-filtered' });
113
+ if (groupFilter && !groupFilter.has(sessionId)) {
114
+ groupsReport.push({ sessionId, new: 0, status: 'skipped', reason: 'scope-filtered' });
115
115
  continue;
116
116
  }
117
- const state = await readGroupState(opts.root, groupId);
118
- const beforeCount = await safeCall(() => opts.countMessages(groupId), 0);
117
+ const state = await readGroupState(opts.root, sessionId);
118
+ const beforeCount = await safeCall(() => opts.countMessages(sessionId), 0);
119
119
  const newCount = Math.max(0, beforeCount - (state.messageCount || 0));
120
120
 
121
121
  const rerunScopedManual = !!opts.manual
122
122
  && groupFilter
123
- && groupFilter.has(groupId)
123
+ && groupFilter.has(sessionId)
124
124
  && newCount === 0
125
125
  && beforeCount > 0;
126
126
 
127
127
  if (newCount === 0 && !rerunScopedManual) {
128
- groupsReport.push({ groupId, new: 0, status: 'skipped', reason: 'no-new-messages' });
128
+ groupsReport.push({ sessionId, new: 0, status: 'skipped', reason: 'no-new-messages' });
129
129
  continue;
130
130
  }
131
131
  if (!opts.manual && newCount < limits.MIN_NEW_PER_GROUP) {
132
- groupsReport.push({ groupId, new: newCount, status: 'skipped', reason: 'below-threshold' });
132
+ groupsReport.push({ sessionId, new: newCount, status: 'skipped', reason: 'below-threshold' });
133
133
  continue;
134
134
  }
135
135
 
136
- onProgress({ phase: 'load-diff', groupId });
136
+ onProgress({ phase: 'load-diff', sessionId });
137
137
  const diffCursor = rerunScopedManual ? null : state.lastDreamMessageId;
138
- const diffNew = await safeCall(() => opts.loadGroupDiff(groupId, diffCursor), []);
138
+ const diffNew = await safeCall(() => opts.loadGroupDiff(sessionId, diffCursor), []);
139
139
  if (!diffNew || diffNew.length === 0) {
140
- groupsReport.push({ groupId, new: newCount, status: 'skipped', reason: 'empty-diff' });
140
+ groupsReport.push({ sessionId, new: newCount, status: 'skipped', reason: 'empty-diff' });
141
141
  continue;
142
142
  }
143
143
  const overlapMessages = state.lastDreamMessageId && !rerunScopedManual
144
144
  ? await safeCall(
145
145
  () => opts.loadOverlapPreamble
146
- ? opts.loadOverlapPreamble(groupId, state.lastDreamMessageId, limits.DREAM_OVERLAP)
146
+ ? opts.loadOverlapPreamble(sessionId, state.lastDreamMessageId, limits.DREAM_OVERLAP)
147
147
  : [],
148
148
  [],
149
149
  )
@@ -153,13 +153,13 @@ export async function runDream(opts) {
153
153
  const fullDiff = [...taggedOverlap, ...taggedNew];
154
154
 
155
155
  const segments = segmentDiff(fullDiff, limits.MAX_DIFF_TOKENS_PER_TRIAGE, limits.DREAM_OVERLAP);
156
- onProgress({ phase: 'triage', groupId, status: 'running', segments: segments.length });
156
+ onProgress({ phase: 'triage', sessionId, status: 'running', segments: segments.length });
157
157
 
158
158
  let actions;
159
159
  try {
160
- const topicSummaries = await resolveTopicSummaries(groupId);
160
+ const topicSummaries = await resolveTopicSummaries(sessionId);
161
161
  actions = await triageGroupSegments({
162
- groupId,
162
+ sessionId,
163
163
  segments,
164
164
  topicSummaries,
165
165
  llm: opts.llm,
@@ -167,12 +167,12 @@ export async function runDream(opts) {
167
167
  language: opts.language,
168
168
  });
169
169
  } catch (err) {
170
- groupsReport.push({ groupId, new: newCount, status: 'error', error: err.message });
171
- onProgress({ phase: 'triage', groupId, status: 'error', error: err.message });
170
+ groupsReport.push({ sessionId, new: newCount, status: 'error', error: err.message });
171
+ onProgress({ phase: 'triage', sessionId, status: 'error', error: err.message });
172
172
  // Journal the failure on disk so operators can see WHY dream is
173
173
  // not advancing without having to enable `config.debug`. Best-
174
174
  // effort — `writeDreamError` swallows its own I/O errors.
175
- await writeDreamError(opts.root, `group/${groupId}`, {
175
+ await writeDreamError(opts.root, `group/${sessionId}`, {
176
176
  phase: 'triage',
177
177
  message: err.message,
178
178
  stack: err.stack,
@@ -180,12 +180,12 @@ export async function runDream(opts) {
180
180
  continue;
181
181
  }
182
182
 
183
- onProgress({ phase: 'triage', groupId, status: 'done', actions: actions.length });
184
- groupTriages.push({ groupId, diff: fullDiff, actions });
183
+ onProgress({ phase: 'triage', sessionId, status: 'done', actions: actions.length });
184
+ groupTriages.push({ sessionId, diff: fullDiff, actions });
185
185
 
186
186
  const tailId = lastMessageId(diffNew);
187
- processedGroups.push({ groupId, tailId, beforeCount, newCount, segments: segments.length, actions: actions.length });
188
- groupsReport.push({ groupId, new: newCount, segments: segments.length, actions: actions.length, status: 'triaged', rerun: rerunScopedManual || undefined });
187
+ processedGroups.push({ sessionId, tailId, beforeCount, newCount, segments: segments.length, actions: actions.length });
188
+ groupsReport.push({ sessionId, new: newCount, segments: segments.length, actions: actions.length, status: 'triaged', rerun: rerunScopedManual || undefined });
189
189
  }
190
190
 
191
191
  // 3. merge
@@ -238,12 +238,12 @@ export async function runDream(opts) {
238
238
  // retries.)
239
239
  const successfulTargets = new Set(targetsReport.filter(r => r.status === 'done').map(r => r.target));
240
240
  for (const pg of processedGroups) {
241
- const contributed = (groupTriages.find(g => g.groupId === pg.groupId) || { actions: [] })
241
+ const contributed = (groupTriages.find(g => g.sessionId === pg.sessionId) || { actions: [] })
242
242
  .actions.map(a => a.scope);
243
243
  const anySuccess = contributed.some(t => successfulTargets.has(t));
244
244
  if (!anySuccess) continue;
245
245
  if (pg.tailId) {
246
- await writeGroupState(opts.root, pg.groupId, {
246
+ await writeGroupState(opts.root, pg.sessionId, {
247
247
  lastDreamMessageId: pg.tailId,
248
248
  lastDreamAt: nowIso,
249
249
  messageCount: pg.beforeCount,
@@ -297,7 +297,7 @@ function deriveGroupFilter(filter) {
297
297
 
298
298
  function sourceGroupId(mergedTarget) {
299
299
  const src = Array.isArray(mergedTarget?.sources) ? mergedTarget.sources[0] : null;
300
- return src && typeof src.groupId === 'string' ? src.groupId : '';
300
+ return src && typeof src.sessionId === 'string' ? src.sessionId : '';
301
301
  }
302
302
 
303
303
  async function safeCall(fn, fallback) {
@@ -310,12 +310,12 @@ async function safeCall(fn, fallback) {
310
310
  }
311
311
  }
312
312
 
313
- async function defaultListTopicSummaries(root, groupId, language) {
313
+ async function defaultListTopicSummaries(root, sessionId, language) {
314
314
  const all = await listScopes({ root });
315
315
  const out = [];
316
316
  for (const sc of all) {
317
317
  if (sc.kind !== 'group-topic') continue;
318
- if (sc.groupId !== groupId) continue;
318
+ if (sc.sessionId !== sessionId) continue;
319
319
  const summary = await readSummary(sc, { root, language });
320
320
  out.push({ path: sc.path.join('/'), summary });
321
321
  }
@@ -141,7 +141,7 @@ export function segmentDiff(diff, maxTokens = MAX_DIFF_TOKENS_PER_TRIAGE, overla
141
141
  /**
142
142
  * Decide whether a merged apply target needs to be split into batches.
143
143
  *
144
- * @param {{ memoryMd?: string, summaryMd?: string, sources: Array<{ groupId: string, diff: any }> }} merged
144
+ * @param {{ memoryMd?: string, summaryMd?: string, sources: Array<{ sessionId: string, diff: any }> }} merged
145
145
  * @param {number} [maxTokens=MAX_APPLY_TOKENS]
146
146
  */
147
147
  export function needsBatchedApply(merged, maxTokens = MAX_APPLY_TOKENS) {
@@ -165,9 +165,9 @@ function totalApplyTokens(merged) {
165
165
  * goes into its own batch — we never split a source diff here (segment
166
166
  * happens earlier, in triage).
167
167
  *
168
- * @param {{ memoryMd?: string, summaryMd?: string, sources: Array<{ groupId: string, diff: any }> }} merged
168
+ * @param {{ memoryMd?: string, summaryMd?: string, sources: Array<{ sessionId: string, diff: any }> }} merged
169
169
  * @param {number} [maxTokens=MAX_APPLY_TOKENS]
170
- * @returns {Array<{ groupId: string, diff: any }[]>}
170
+ * @returns {Array<{ sessionId: string, diff: any }[]>}
171
171
  */
172
172
  export function batchSourcesForApply(merged, maxTokens = MAX_APPLY_TOKENS) {
173
173
  const sources = Array.isArray(merged.sources) ? merged.sources : [];
@@ -15,7 +15,7 @@
15
15
  * load-bearing for the debug panel. Renaming a field on this side
16
16
  * silently degrades that UI to a generic JSON dump.
17
17
  *
18
- * dream_turn_open: { type:'turn_open', turnId, userPrompt, vpId, groupId, at }
18
+ * dream_turn_open: { type:'turn_open', turnId, userPrompt, vpId, sessionId, at }
19
19
  * dream_loop: { type:'loop', turnId, loopNumber, pass, model,
20
20
  * systemPrompt: string,
21
21
  * messages: [{ role:'user', content:string }],
@@ -35,19 +35,19 @@
35
35
  * `kind, memoryMdPreview, summaryMdPreview, memoryMdLength,
36
36
  * summaryMdLength` (see apply.js).
37
37
  *
38
- * `groupId` may be inherited via `stampDreamScope()` when a scope is active.
38
+ * `sessionId` may be inherited via `stampDreamScope()` when a scope is active.
39
39
  */
40
40
 
41
41
  import { join } from 'path';
42
42
  import { runDream } from './runner.js';
43
43
  import { createDreamScheduler } from './schedule.js';
44
- import { listGroups, openGroup } from '../groups/group-store.js';
44
+ import { listSessions, openSession } from '../sessions/session-store.js';
45
45
  import { readGroupState } from './state.js';
46
46
  import { DREAM_NUDGE_AFTER_MESSAGES, DREAM_INTERVAL_HOURS } from './limits.js';
47
47
 
48
48
  /**
49
49
  * Build the per-call options for runDream. Pure: takes a session and returns
50
- * the closures runDream needs (listGroups, countMessages, loadGroupDiff, etc.).
50
+ * the closures runDream needs (listSessions, countMessages, loadGroupDiff, etc.).
51
51
  *
52
52
  * @param {Object} session — the live Session object from loadSession()
53
53
  * @param {(event: object) => void} [onProgress]
@@ -56,19 +56,19 @@ import { DREAM_NUDGE_AFTER_MESSAGES, DREAM_INTERVAL_HOURS } from './limits.js';
56
56
  export function buildRunDreamOpts(session, onProgress) {
57
57
  const yeaftDir = session.yeaftDir;
58
58
  const memoryRoot = join(yeaftDir, 'memory');
59
- const groupsRoot = join(yeaftDir, 'groups');
59
+ const sessionsRoot = join(yeaftDir, 'sessions');
60
60
 
61
61
  return {
62
62
  root: memoryRoot,
63
63
  language: session.config?.language || 'en',
64
64
  llm: makeLlm(session),
65
- listGroups: async () => {
66
- try { return listGroups(groupsRoot).map(g => g.id); }
65
+ listSessions: async () => {
66
+ try { return listSessions(sessionsRoot).map(g => g.id); }
67
67
  catch { return []; }
68
68
  },
69
69
  countMessages: async (gid) => {
70
70
  try {
71
- const h = openGroup(groupsRoot, gid);
71
+ const h = openSession(sessionsRoot, gid);
72
72
  let n = 0;
73
73
  for (const _m of h.streamMessages()) n += 1;
74
74
  return n;
@@ -76,7 +76,7 @@ export function buildRunDreamOpts(session, onProgress) {
76
76
  },
77
77
  loadGroupDiff: async (gid, sinceId) => {
78
78
  try {
79
- const h = openGroup(groupsRoot, gid);
79
+ const h = openSession(sessionsRoot, gid);
80
80
  const out = [];
81
81
  let started = !sinceId;
82
82
  for (const m of h.streamMessages()) {
@@ -91,7 +91,7 @@ export function buildRunDreamOpts(session, onProgress) {
91
91
  },
92
92
  loadOverlapPreamble: async (gid, beforeId, n) => {
93
93
  try {
94
- const h = openGroup(groupsRoot, gid);
94
+ const h = openSession(sessionsRoot, gid);
95
95
  const buf = [];
96
96
  for (const m of h.streamMessages()) {
97
97
  if (m.id === beforeId) break;
@@ -199,8 +199,8 @@ function makeLlm(session) {
199
199
 
200
200
  function stampDreamScope(session, evt) {
201
201
  if (!evt || typeof evt !== 'object') return evt;
202
- if (evt.groupId || !session?._dreamActiveGroupId) return evt;
203
- return { ...evt, groupId: session._dreamActiveGroupId };
202
+ if (evt.sessionId || !session?._dreamActiveGroupId) return evt;
203
+ return { ...evt, sessionId: session._dreamActiveGroupId };
204
204
  }
205
205
 
206
206
  function finiteNumber(v) {
@@ -326,7 +326,7 @@ export function createV2DreamScheduler(session) {
326
326
  turnId,
327
327
  userPrompt: '[dream] automatic memory consolidation',
328
328
  vpId: null,
329
- groupId: null,
329
+ sessionId: null,
330
330
  at: startedAt,
331
331
  });
332
332
  persistDreamTrace(session, 'dream_turn_open', turnOpen);
@@ -341,7 +341,7 @@ export function createV2DreamScheduler(session) {
341
341
  }).then((result) => {
342
342
  // Bug 2: emit turn_close when the dream pass completes.
343
343
  result.trigger = opts.manual ? 'manual' : 'auto';
344
- if (session._dreamActiveGroupId && !result.groupId) result.groupId = session._dreamActiveGroupId;
344
+ if (session._dreamActiveGroupId && !result.sessionId) result.sessionId = session._dreamActiveGroupId;
345
345
  const metrics = finalizeDreamMetrics(session._dreamMetrics, Date.now() - startedAt);
346
346
  result.metrics = metrics;
347
347
  result.durationMs = metrics.durationMs;
@@ -449,9 +449,9 @@ export function createV2DreamScheduler(session) {
449
449
  export async function bootInitEmptyGroups(args) {
450
450
  const out = { triggered: [] };
451
451
  if (!args || !args.memoryIndex || !args.dreamScheduler) return out;
452
- const groupsRoot = join(args.yeaftDir, 'groups');
452
+ const sessionsRoot = join(args.yeaftDir, 'sessions');
453
453
  let ids;
454
- try { ids = listGroups(groupsRoot).map(g => g.id); }
454
+ try { ids = listSessions(sessionsRoot).map(g => g.id); }
455
455
  catch { return out; }
456
456
  const empty = [];
457
457
  for (const gid of ids) {
@@ -461,7 +461,7 @@ export async function bootInitEmptyGroups(args) {
461
461
  if (segCount > 0) continue;
462
462
  let hasMessages = false;
463
463
  try {
464
- const h = openGroup(groupsRoot, gid);
464
+ const h = openSession(sessionsRoot, gid);
465
465
  // Any message at all is enough — pull the first record off the
466
466
  // iterator and stop.
467
467
  const first = h.streamMessages().next();
@@ -516,18 +516,18 @@ export async function bootCatchUpStaleDream(args) {
516
516
  if (!args || !args.dreamScheduler) return out;
517
517
 
518
518
  const memoryRoot = join(args.yeaftDir, 'memory');
519
- const groupsRoot = join(args.yeaftDir, 'groups');
519
+ const sessionsRoot = join(args.yeaftDir, 'sessions');
520
520
  const intervalMs = (args.intervalHours ?? DREAM_INTERVAL_HOURS) * 60 * 60 * 1000;
521
521
  const now = args.now ?? Date.now();
522
522
 
523
- let groupIds;
524
- try { groupIds = listGroups(groupsRoot).map(g => g.id); }
523
+ let sessionIds;
524
+ try { sessionIds = listSessions(sessionsRoot).map(g => g.id); }
525
525
  catch { return out; }
526
526
 
527
527
  // Find the newest lastDreamAt across all groups.
528
528
  let newestAt = null;
529
529
  let anyTraffic = false;
530
- for (const gid of groupIds) {
530
+ for (const gid of sessionIds) {
531
531
  let st;
532
532
  try { st = await readGroupState(memoryRoot, gid); }
533
533
  catch { continue; }
@@ -537,7 +537,7 @@ export async function bootCatchUpStaleDream(args) {
537
537
  }
538
538
  if (!anyTraffic) {
539
539
  try {
540
- const h = openGroup(groupsRoot, gid);
540
+ const h = openSession(sessionsRoot, gid);
541
541
  const first = h.streamMessages().next();
542
542
  if (!first.done) anyTraffic = true;
543
543
  } catch { /* keep going */ }
@@ -59,11 +59,11 @@ const DREAM_BLOCK_CLOSE = '<!-- /dream-state -->';
59
59
  * Read a group's .dream-state. Missing file → defaults.
60
60
  *
61
61
  * @param {string} root — memory root, e.g. ~/.yeaft/memory
62
- * @param {string} groupId
62
+ * @param {string} sessionId
63
63
  * @returns {Promise<{ lastDreamMessageId: string|null, lastDreamAt: string|null, messageCount: number }>}
64
64
  */
65
- export async function readGroupState(root, groupId) {
66
- const abs = join(root, 'group', groupId, STATE_FILE);
65
+ export async function readGroupState(root, sessionId) {
66
+ const abs = join(root, 'group', sessionId, STATE_FILE);
67
67
  const empty = { lastDreamMessageId: null, lastDreamAt: null, messageCount: 0 };
68
68
  let raw;
69
69
  try { raw = await fsp.readFile(abs, 'utf8'); }
@@ -76,11 +76,11 @@ export async function readGroupState(root, groupId) {
76
76
  * absent. Unknown fields are ignored.
77
77
  *
78
78
  * @param {string} root
79
- * @param {string} groupId
79
+ * @param {string} sessionId
80
80
  * @param {{ lastDreamMessageId?: string|null, lastDreamAt?: string|null, messageCount?: number }} state
81
81
  */
82
- export async function writeGroupState(root, groupId, state) {
83
- const dir = join(root, 'group', groupId);
82
+ export async function writeGroupState(root, sessionId, state) {
83
+ const dir = join(root, 'group', sessionId);
84
84
  await fsp.mkdir(dir, { recursive: true });
85
85
  const abs = join(dir, STATE_FILE);
86
86
  const body =
@@ -131,7 +131,7 @@ function parseGroupState(raw) {
131
131
  /**
132
132
  * Resolve a memoryRoot + scope-string to the scope directory.
133
133
  * The scope string is the same shape dream-v2 already uses internally:
134
- * `'user'`, `'vp/<vpId>'`, `'group/<groupId>'`, `'feature/<id>'`, etc.
134
+ * `'user'`, `'vp/<vpId>'`, `'group/<sessionId>'`, `'feature/<id>'`, etc.
135
135
  *
136
136
  * Pure path-join; does NOT create the directory. The writer creates it.
137
137
  *