@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
@@ -3,29 +3,29 @@
3
3
  *
4
4
  * Wraps the primitives from group-store.js + roster.js into the 5 operations
5
5
  * wired to WS events (§Δ10 334m + R6 §Δ31.2):
6
- * createGroupFromSpec — wizard "create new group" (empty or user-picked roster)
7
- * renameGroup — update meta.name; preserves roster / defaultVpId
8
- * archiveGroup — rename dir to `.archived-<ts>-<id>` (soft delete)
6
+ * createSessionFromSpec — wizard "create new group" (empty or user-picked roster)
7
+ * renameSession — update meta.name; preserves roster / defaultVpId
8
+ * archiveSession — rename dir to `.archived-<ts>-<id>` (soft delete)
9
9
  * addMember — roster.addVp + save; sets defaultVpId if first
10
10
  * removeMember — roster.removeVp + save; clears/rotates defaultVpId
11
11
  *
12
12
  * Plus the D1 bootstrap helper:
13
- * ensureDefaultGroupIfEmpty(yeaftDir, {libDir}) — if NO group exists on
13
+ * ensureDefaultSessionIfEmpty(yeaftDir, {libDir}) — if NO group exists on
14
14
  * disk, seed `grp_default` with roster = every VP in the library, and
15
15
  * defaultVpId = alphabetically first vpId. No-op when ≥1 group present.
16
16
  *
17
17
  * Hard constraints (PM):
18
18
  * (a) We don't touch 334o storage primitives (storage/index.js) — we call
19
- * group-store.openGroup / saveMeta which already go through openLog.
19
+ * group-store.openSession / saveMeta which already go through openLog.
20
20
  * (b) We don't touch VP entity (vp-store.js / vp-loader.js) — only read
21
21
  * via scanVpLibrary to know which VPs exist at seed time.
22
22
  * (c) When `addMember` is called with an empty roster and no defaultVpId
23
- * resolvable, callers surface `no_default_vp` via `createGroupFromSpec`;
23
+ * resolvable, callers surface `no_default_vp` via `createSessionFromSpec`;
24
24
  * on `removeMember` we permit the empty state (UI nudges the user).
25
25
  *
26
- * Error shape — every throw is a `GroupCrudError` with a stable `.code`:
26
+ * Error shape — every throw is a `SessionCrudError` with a stable `.code`:
27
27
  * 'not_found' — group id has no dir / meta
28
- * 'duplicate' — createGroup collided with an existing id
28
+ * 'duplicate' — createSession collided with an existing id
29
29
  * 'invalid_name' — display name empty after trim
30
30
  * 'no_default_vp' — seed with empty VP library OR roster empties to []
31
31
  * and the caller asked for a defaultVpId. (D1 spec)
@@ -46,14 +46,14 @@ import { randomBytes } from 'crypto';
46
46
  import { homedir } from 'os';
47
47
  import { isAbsolute, join, resolve } from 'path';
48
48
  import {
49
- openGroup, createGroup, listGroups, loadGroupMeta,
50
- } from './group-store.js';
49
+ openSession, createSession, listSessions, loadSessionMeta,
50
+ } from './session-store.js';
51
51
  import { addVp as rosterAdd, removeVp as rosterRemove, setDefaultVp } from './roster.js';
52
- import { seedDefaultGroup, DEFAULT_GROUP_ID } from './seed-default.js';
53
- import { nextGroupId, validateVpId, isReservedVpId } from './ids.js';
52
+ import { seedDefaultSession, DEFAULT_SESSION_ID } from './seed-default.js';
53
+ import { nextSessionId, validateVpId, isReservedVpId } from './ids.js';
54
54
  import { scanVpLibrary, DEFAULT_VP_LIB_DIR } from '../vp/vp-store.js';
55
55
  import { seedSummaryIfMissingSync, removeScopeDirSync } from '../memory/store-v2.js';
56
- import { ensureGroupConfigFile, saveGroupConfig, loadGroupConfig } from './group-config.js';
56
+ import { ensureSessionConfigFile, saveSessionConfig, loadSessionConfig } from './session-config.js';
57
57
 
58
58
  /**
59
59
  * Default memory root used when callers don't pass `options.memoryRoot`.
@@ -73,7 +73,7 @@ const DEFAULT_MEMORY_ROOT = join(homedir(), '.yeaft', 'memory');
73
73
  * @param {{name:string, roster?:string[], defaultVpId?:string|null}} spec
74
74
  * @returns {string}
75
75
  */
76
- export function buildGroupSeedSummary(spec) {
76
+ export function buildSessionSeedSummary(spec) {
77
77
  const name = String(spec?.name || '').trim();
78
78
  const roster = Array.isArray(spec?.roster) ? spec.roster : [];
79
79
  const lines = [];
@@ -88,19 +88,19 @@ export function buildGroupSeedSummary(spec) {
88
88
  return lines.join('\n').trim();
89
89
  }
90
90
 
91
- export class GroupCrudError extends Error {
92
- constructor(code, groupId, message) {
93
- super(message || `${code}: ${groupId}`);
94
- this.name = 'GroupCrudError';
91
+ export class SessionCrudError extends Error {
92
+ constructor(code, sessionId, message) {
93
+ super(message || `${code}: ${sessionId}`);
94
+ this.name = 'SessionCrudError';
95
95
  this.code = code;
96
- this.groupId = groupId;
96
+ this.sessionId = sessionId;
97
97
  }
98
98
  }
99
99
 
100
100
  const GROUP_WORKDIR_REGISTRY = 'group-workdirs.json';
101
101
 
102
- export function groupsRoot(yeaftDir) {
103
- return join(yeaftDir, 'groups');
102
+ export function sessionsRoot(yeaftDir) {
103
+ return join(yeaftDir, 'sessions');
104
104
  }
105
105
 
106
106
  export function normalizeWorkDir(workDir) {
@@ -136,46 +136,46 @@ function writeWorkDirRegistry(yeaftDir, registry) {
136
136
  writeFileSync(registryPath(yeaftDir), `${JSON.stringify(registry, null, 2)}\n`);
137
137
  }
138
138
 
139
- export function registerGroupWorkDir(defaultYeaftDir, groupId, workDir) {
139
+ export function registerSessionWorkDir(defaultYeaftDir, sessionId, workDir) {
140
140
  const normalized = normalizeWorkDir(workDir);
141
- if (!defaultYeaftDir || !groupId || !normalized) return;
141
+ if (!defaultYeaftDir || !sessionId || !normalized) return;
142
142
  const registry = readWorkDirRegistry(defaultYeaftDir);
143
- registry[groupId] = normalized;
143
+ registry[sessionId] = normalized;
144
144
  writeWorkDirRegistry(defaultYeaftDir, registry);
145
145
  }
146
146
 
147
- export function unregisterGroupWorkDir(defaultYeaftDir, groupId) {
148
- if (!defaultYeaftDir || !groupId) return;
147
+ export function unregisterSessionWorkDir(defaultYeaftDir, sessionId) {
148
+ if (!defaultYeaftDir || !sessionId) return;
149
149
  const registry = readWorkDirRegistry(defaultYeaftDir);
150
- if (!Object.prototype.hasOwnProperty.call(registry, groupId)) return;
151
- delete registry[groupId];
150
+ if (!Object.prototype.hasOwnProperty.call(registry, sessionId)) return;
151
+ delete registry[sessionId];
152
152
  writeWorkDirRegistry(defaultYeaftDir, registry);
153
153
  }
154
154
 
155
- export function resolveGroupYeaftDir(defaultYeaftDir, groupId) {
156
- if (!defaultYeaftDir || !groupId) return defaultYeaftDir;
157
- const defaultGroupDir = join(groupsRoot(defaultYeaftDir), groupId);
158
- if (existsSync(defaultGroupDir) && loadGroupMeta(defaultGroupDir)) return defaultYeaftDir;
155
+ export function resolveSessionYeaftDir(defaultYeaftDir, sessionId) {
156
+ if (!defaultYeaftDir || !sessionId) return defaultYeaftDir;
157
+ const defaultGroupDir = join(sessionsRoot(defaultYeaftDir), sessionId);
158
+ if (existsSync(defaultGroupDir) && loadSessionMeta(defaultGroupDir)) return defaultYeaftDir;
159
159
 
160
160
  const registry = readWorkDirRegistry(defaultYeaftDir);
161
- const workDir = normalizeWorkDir(registry[groupId]);
161
+ const workDir = normalizeWorkDir(registry[sessionId]);
162
162
  if (workDir) {
163
163
  const candidate = yeaftDirForWorkDir(workDir);
164
- const candidateDir = join(groupsRoot(candidate), groupId);
165
- if (existsSync(candidateDir) && loadGroupMeta(candidateDir)) return candidate;
164
+ const candidateDir = join(sessionsRoot(candidate), sessionId);
165
+ if (existsSync(candidateDir) && loadSessionMeta(candidateDir)) return candidate;
166
166
  }
167
167
 
168
168
  return defaultYeaftDir;
169
169
  }
170
170
 
171
171
  /** Build a safe group id from a display name (slug + ulid-lite suffix). */
172
- export function makeGroupId(name) {
172
+ export function makeSessionId(name) {
173
173
  const slug = String(name || 'group')
174
174
  .toLowerCase()
175
175
  .replace(/[^a-z0-9]+/g, '-')
176
176
  .replace(/^-+|-+$/g, '')
177
177
  .slice(0, 24) || 'group';
178
- return nextGroupId(slug);
178
+ return nextSessionId(slug);
179
179
  }
180
180
 
181
181
  /**
@@ -188,12 +188,12 @@ export function makeGroupId(name) {
188
188
  * the UI has somewhere to land — but defaultVpId is null and downstream
189
189
  * message send will return `no_default_vp` until the user adds a VP.
190
190
  */
191
- export function ensureDefaultGroupIfEmpty(yeaftDir, options = {}) {
191
+ export function ensureDefaultSessionIfEmpty(yeaftDir, options = {}) {
192
192
  const libDir = options.libDir || DEFAULT_VP_LIB_DIR;
193
193
  const memoryRoot = options.memoryRoot || DEFAULT_MEMORY_ROOT;
194
- const existing = listGroups(groupsRoot(yeaftDir));
194
+ const existing = listSessions(sessionsRoot(yeaftDir));
195
195
  if (existing.length > 0) {
196
- return { seeded: false, groupId: existing[0].id };
196
+ return { seeded: false, sessionId: existing[0].id };
197
197
  }
198
198
 
199
199
  // Sort VP ids alphabetically (stable for tests / deterministic UI).
@@ -204,7 +204,7 @@ export function ensureDefaultGroupIfEmpty(yeaftDir, options = {}) {
204
204
  vps.sort((a, b) => a.localeCompare(b));
205
205
 
206
206
  const defaultVpId = vps[0] || null;
207
- const { group, created } = seedDefaultGroup(yeaftDir, {
207
+ const { group, created } = seedDefaultSession(yeaftDir, {
208
208
  name: options.name || 'Default',
209
209
  roster: vps,
210
210
  defaultVpId,
@@ -212,7 +212,7 @@ export function ensureDefaultGroupIfEmpty(yeaftDir, options = {}) {
212
212
  });
213
213
  return {
214
214
  seeded: created,
215
- groupId: group.id,
215
+ sessionId: group.id,
216
216
  defaultVpId,
217
217
  rosterSize: vps.length,
218
218
  };
@@ -226,21 +226,21 @@ export function ensureDefaultGroupIfEmpty(yeaftDir, options = {}) {
226
226
  * @param {{name:string, roster?:string[], defaultVpId?:string|null, workDir?:string}} spec
227
227
  * @returns {{id:string, name:string, roster:string[], defaultVpId:string|null, workDir?:string}}
228
228
  */
229
- export function createGroupFromSpec(yeaftDir, spec, options = {}) {
229
+ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
230
230
  const normalizedWorkDir = normalizeWorkDir(spec && spec.workDir);
231
231
  const groupYeaftDir = normalizedWorkDir ? yeaftDirForWorkDir(normalizedWorkDir) : yeaftDir;
232
232
  const memoryRoot = options.memoryRoot || (groupYeaftDir ? join(groupYeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
233
233
  const name = String(spec && spec.name || '').trim();
234
- if (!name) throw new GroupCrudError('invalid_name', null, 'group name required');
234
+ if (!name) throw new SessionCrudError('invalid_name', null, 'group name required');
235
235
 
236
236
  const roster = Array.isArray(spec.roster) ? spec.roster.slice() : [];
237
237
  // Validate every member up-front so we fail before touching fs.
238
238
  for (const vpId of roster) {
239
239
  if (isReservedVpId(vpId)) {
240
- throw new GroupCrudError('reserved', null, `reserved vpId: ${vpId}`);
240
+ throw new SessionCrudError('reserved', null, `reserved vpId: ${vpId}`);
241
241
  }
242
242
  const v = validateVpId(vpId);
243
- if (!v.ok) throw new GroupCrudError(v.reason, null, `invalid vpId: ${vpId}`);
243
+ if (!v.ok) throw new SessionCrudError(v.reason, null, `invalid vpId: ${vpId}`);
244
244
  }
245
245
 
246
246
  // defaultVpId resolution: explicit > roster[0] > null. Null is allowed at
@@ -248,21 +248,21 @@ export function createGroupFromSpec(yeaftDir, spec, options = {}) {
248
248
  // (task-334m spec: `no_default_vp` surfaced on first send, not on create).
249
249
  let defaultVpId = spec.defaultVpId || null;
250
250
  if (defaultVpId && !roster.includes(defaultVpId)) {
251
- throw new GroupCrudError('default_not_in_roster', null, `${defaultVpId} not in roster`);
251
+ throw new SessionCrudError('default_not_in_roster', null, `${defaultVpId} not in roster`);
252
252
  }
253
253
  if (!defaultVpId) defaultVpId = roster[0] || null;
254
254
 
255
- const id = makeGroupId(name);
256
- const root = groupsRoot(groupYeaftDir);
255
+ const id = makeSessionId(name);
256
+ const root = sessionsRoot(groupYeaftDir);
257
257
  if (existsSync(join(root, id))) {
258
258
  // Extremely unlikely (ulid suffix), but surface deterministically.
259
- throw new GroupCrudError('duplicate', id);
259
+ throw new SessionCrudError('duplicate', id);
260
260
  }
261
261
 
262
- const handle = createGroup(root, { id, name, roster, defaultVpId, workDir: normalizedWorkDir });
262
+ const handle = createSession(root, { id, name, roster, defaultVpId, workDir: normalizedWorkDir });
263
263
  const meta = handle.getMeta();
264
264
  handle.close();
265
- if (normalizedWorkDir) registerGroupWorkDir(yeaftDir, id, normalizedWorkDir);
265
+ if (normalizedWorkDir) registerSessionWorkDir(yeaftDir, id, normalizedWorkDir);
266
266
 
267
267
  // Per-group config (v1: model only). We always create an empty
268
268
  // config.json so the file's presence signals "owned by this group" and
@@ -270,9 +270,9 @@ export function createGroupFromSpec(yeaftDir, spec, options = {}) {
270
270
  // wizard spec (currently just `config.model`) are persisted here so
271
271
  // the engine cache picks them up on the very first turn.
272
272
  try {
273
- ensureGroupConfigFile(yeaftDir, id);
273
+ ensureSessionConfigFile(yeaftDir, id);
274
274
  if (spec && spec.config && typeof spec.config === 'object') {
275
- saveGroupConfig(yeaftDir, id, spec.config);
275
+ saveSessionConfig(yeaftDir, id, spec.config);
276
276
  }
277
277
  } catch (err) {
278
278
  console.warn(`[group-crud] failed to seed config.json for ${id}:`, err?.message || err);
@@ -284,7 +284,7 @@ export function createGroupFromSpec(yeaftDir, spec, options = {}) {
284
284
  try {
285
285
  seedSummaryIfMissingSync(
286
286
  { kind: 'group', id },
287
- buildGroupSeedSummary({ name, roster, defaultVpId }),
287
+ buildSessionSeedSummary({ name, roster, defaultVpId }),
288
288
  { root: memoryRoot },
289
289
  );
290
290
  } catch (err) {
@@ -297,10 +297,10 @@ export function createGroupFromSpec(yeaftDir, spec, options = {}) {
297
297
  /**
298
298
  * (A.2) Rename — updates meta.name; preserves everything else.
299
299
  */
300
- export function renameGroup(yeaftDir, groupId, newName) {
300
+ export function renameSession(yeaftDir, sessionId, newName) {
301
301
  const name = String(newName || '').trim();
302
- if (!name) throw new GroupCrudError('invalid_name', groupId);
303
- const handle = requireGroup(yeaftDir, groupId);
302
+ if (!name) throw new SessionCrudError('invalid_name', sessionId);
303
+ const handle = requireSession(yeaftDir, sessionId);
304
304
  const meta = handle.getMeta();
305
305
  handle.saveMeta({ ...meta, name });
306
306
  const next = handle.getMeta();
@@ -315,12 +315,12 @@ export function renameGroup(yeaftDir, groupId, newName) {
315
315
  * `text` must be a string. Trimmed before persist so leading/trailing
316
316
  * whitespace doesn't pollute the prompt.
317
317
  */
318
- export function updateGroupAnnouncement(yeaftDir, groupId, text) {
318
+ export function updateSessionAnnouncement(yeaftDir, sessionId, text) {
319
319
  if (typeof text !== 'string') {
320
- throw new GroupCrudError('invalid_announcement', groupId);
320
+ throw new SessionCrudError('invalid_announcement', sessionId);
321
321
  }
322
322
  const announcement = text.trim();
323
- const handle = requireGroup(yeaftDir, groupId);
323
+ const handle = requireSession(yeaftDir, sessionId);
324
324
  const meta = handle.getMeta();
325
325
  handle.saveMeta({ ...meta, announcement });
326
326
  const next = handle.getMeta();
@@ -332,13 +332,13 @@ export function updateGroupAnnouncement(yeaftDir, groupId, text) {
332
332
  * (A.2.c) Persist the model selected in the group conversation header.
333
333
  * Returns the persisted config object so the caller can broadcast it.
334
334
  *
335
- * Throws GroupConfigError on validation failure (unknown key, bad type).
336
- * Group must exist (we call requireGroup to assert).
335
+ * Throws SessionConfigError on validation failure (unknown key, bad type).
336
+ * Group must exist (we call requireSession to assert).
337
337
  */
338
- export function updateGroupConfig(yeaftDir, groupId, partial) {
339
- const handle = requireGroup(yeaftDir, groupId);
338
+ export function updateSessionConfig(yeaftDir, sessionId, partial) {
339
+ const handle = requireSession(yeaftDir, sessionId);
340
340
  try {
341
- return saveGroupConfig(yeaftDir, groupId, partial || {});
341
+ return saveSessionConfig(yeaftDir, sessionId, partial || {});
342
342
  } finally {
343
343
  handle.close();
344
344
  }
@@ -346,26 +346,26 @@ export function updateGroupConfig(yeaftDir, groupId, partial) {
346
346
 
347
347
  /**
348
348
  * (A.3) Archive — renames the dir to `.archived-<ts>-<id>`. Directory
349
- * prefix `.` keeps `listGroups` from picking it up (readdirSync filter in
349
+ * prefix `.` keeps `listSessions` from picking it up (readdirSync filter in
350
350
  * the caller). Reversible: user can rename back manually for recovery.
351
351
  *
352
352
  * We do NOT support hard-delete here — that's an upstream UI flow with its
353
353
  * own second-confirm modal (acceptance #4 in task-334-slice-specs.md 334m).
354
354
  */
355
- export function archiveGroup(yeaftDir, groupId) {
356
- const groupYeaftDir = resolveGroupYeaftDir(yeaftDir, groupId);
357
- const root = groupsRoot(groupYeaftDir);
358
- const srcDir = join(root, groupId);
359
- if (!existsSync(srcDir) || !loadGroupMeta(srcDir)) {
360
- throw new GroupCrudError('not_found', groupId);
355
+ export function archiveSession(yeaftDir, sessionId) {
356
+ const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
357
+ const root = sessionsRoot(groupYeaftDir);
358
+ const srcDir = join(root, sessionId);
359
+ if (!existsSync(srcDir) || !loadSessionMeta(srcDir)) {
360
+ throw new SessionCrudError('not_found', sessionId);
361
361
  }
362
362
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
363
363
  // Append 4 hex chars to disambiguate same-millisecond archives (nit #5).
364
364
  const suffix = randomBytes(2).toString('hex');
365
- const dstDir = join(root, `.archived-${ts}-${suffix}-${groupId}`);
365
+ const dstDir = join(root, `.archived-${ts}-${suffix}-${sessionId}`);
366
366
  renameSync(srcDir, dstDir);
367
- unregisterGroupWorkDir(yeaftDir, groupId);
368
- return { groupId, archivedAs: dstDir };
367
+ unregisterSessionWorkDir(yeaftDir, sessionId);
368
+ return { sessionId, archivedAs: dstDir };
369
369
  }
370
370
 
371
371
  /**
@@ -373,27 +373,27 @@ export function archiveGroup(yeaftDir, groupId) {
373
373
  * contents (group.json, messages/, tasks/, vps/). Irreversible.
374
374
  *
375
375
  * Bug 8 fix: replaces the soft-archive flow that left `.archived-*` dirs
376
- * lying around in `~/.yeaft/groups/`. Per user request, "delete" means
376
+ * lying around in `~/.yeaft/sessions/`. Per user request, "delete" means
377
377
  * physical deletion, not rename.
378
378
  *
379
- * Also sweeps any sibling `.archived-*-<groupId>` dirs that were left
379
+ * Also sweeps any sibling `.archived-*-<sessionId>` dirs that were left
380
380
  * behind by the previous soft-archive implementation, so a single
381
381
  * delete cleans up legacy state too.
382
382
  */
383
- export function deleteGroup(yeaftDir, groupId, options = {}) {
384
- const groupYeaftDir = resolveGroupYeaftDir(yeaftDir, groupId);
383
+ export function deleteSession(yeaftDir, sessionId, options = {}) {
384
+ const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
385
385
  const memoryRoot = options.memoryRoot || (groupYeaftDir ? join(groupYeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
386
- const root = groupsRoot(groupYeaftDir);
387
- const srcDir = join(root, groupId);
388
- const liveExists = existsSync(srcDir) && !!loadGroupMeta(srcDir);
386
+ const root = sessionsRoot(groupYeaftDir);
387
+ const srcDir = join(root, sessionId);
388
+ const liveExists = existsSync(srcDir) && !!loadSessionMeta(srcDir);
389
389
 
390
- // Collect any leftover soft-archive directories matching this groupId.
390
+ // Collect any leftover soft-archive directories matching this sessionId.
391
391
  const legacyDirs = [];
392
392
  if (existsSync(root)) {
393
393
  for (const name of readdirSync(root)) {
394
394
  if (!name.startsWith('.archived-')) continue;
395
- // Soft-archive format: .archived-<ts>-<suffix>-<groupId>
396
- if (!name.endsWith(`-${groupId}`)) continue;
395
+ // Soft-archive format: .archived-<ts>-<suffix>-<sessionId>
396
+ if (!name.endsWith(`-${sessionId}`)) continue;
397
397
  const p = join(root, name);
398
398
  try {
399
399
  if (statSync(p).isDirectory()) legacyDirs.push(p);
@@ -402,7 +402,7 @@ export function deleteGroup(yeaftDir, groupId, options = {}) {
402
402
  }
403
403
 
404
404
  if (!liveExists && legacyDirs.length === 0) {
405
- throw new GroupCrudError('not_found', groupId);
405
+ throw new SessionCrudError('not_found', sessionId);
406
406
  }
407
407
 
408
408
  if (liveExists) {
@@ -415,13 +415,13 @@ export function deleteGroup(yeaftDir, groupId, options = {}) {
415
415
  // Cascade: drop the group's memory scope so a recreate with the same id
416
416
  // starts clean. Best-effort — never let memory cleanup fail the CRUD op.
417
417
  try {
418
- removeScopeDirSync({ kind: 'group', id: groupId }, { root: memoryRoot });
418
+ removeScopeDirSync({ kind: 'group', id: sessionId }, { root: memoryRoot });
419
419
  } catch (err) {
420
- console.warn(`[group-crud] failed to remove memory dir for ${groupId}:`, err?.message || err);
420
+ console.warn(`[group-crud] failed to remove memory dir for ${sessionId}:`, err?.message || err);
421
421
  }
422
422
 
423
- unregisterGroupWorkDir(yeaftDir, groupId);
424
- return { groupId, deleted: true, legacyCleanedUp: legacyDirs.length };
423
+ unregisterSessionWorkDir(yeaftDir, sessionId);
424
+ return { sessionId, deleted: true, legacyCleanedUp: legacyDirs.length };
425
425
  }
426
426
 
427
427
  /**
@@ -429,8 +429,8 @@ export function deleteGroup(yeaftDir, groupId, options = {}) {
429
429
  * orphans of the old soft-archive flow. Used at boot so users don't see
430
430
  * ghost groups in subsequent loads. Returns the list of removed paths.
431
431
  */
432
- export function purgeArchivedGroups(yeaftDir) {
433
- const root = groupsRoot(yeaftDir);
432
+ export function purgeArchivedSessions(yeaftDir) {
433
+ const root = sessionsRoot(yeaftDir);
434
434
  if (!existsSync(root)) return [];
435
435
  const removed = [];
436
436
  for (const name of readdirSync(root)) {
@@ -451,8 +451,8 @@ export function purgeArchivedGroups(yeaftDir) {
451
451
  * (A.4) Add a VP to the group roster. Idempotent — no-op if already present.
452
452
  * Returns the new meta.
453
453
  */
454
- export function addMember(yeaftDir, groupId, vpId) {
455
- const handle = requireGroup(yeaftDir, groupId);
454
+ export function addMember(yeaftDir, sessionId, vpId) {
455
+ const handle = requireSession(yeaftDir, sessionId);
456
456
  try {
457
457
  const meta = handle.getMeta();
458
458
  const next = rosterAdd(meta, vpId);
@@ -467,8 +467,8 @@ export function addMember(yeaftDir, groupId, vpId) {
467
467
  * (A.5) Remove a VP from the group roster. If the removed id was default,
468
468
  * roster.removeVp rotates to the next member (or null).
469
469
  */
470
- export function removeMember(yeaftDir, groupId, vpId) {
471
- const handle = requireGroup(yeaftDir, groupId);
470
+ export function removeMember(yeaftDir, sessionId, vpId) {
471
+ const handle = requireSession(yeaftDir, sessionId);
472
472
  try {
473
473
  const meta = handle.getMeta();
474
474
  if (!meta.roster.includes(vpId)) {
@@ -484,8 +484,8 @@ export function removeMember(yeaftDir, groupId, vpId) {
484
484
  }
485
485
 
486
486
  /** Expose default-VP setter for UI "set as default" affordance. */
487
- export function setGroupDefaultVp(yeaftDir, groupId, vpId) {
488
- const handle = requireGroup(yeaftDir, groupId);
487
+ export function setSessionDefaultVp(yeaftDir, sessionId, vpId) {
488
+ const handle = requireSession(yeaftDir, sessionId);
489
489
  try {
490
490
  const meta = handle.getMeta();
491
491
  const next = setDefaultVp(meta, vpId);
@@ -496,35 +496,35 @@ export function setGroupDefaultVp(yeaftDir, groupId, vpId) {
496
496
  }
497
497
  }
498
498
 
499
- export function requireGroup(yeaftDir, groupId) {
500
- const groupYeaftDir = resolveGroupYeaftDir(yeaftDir, groupId);
501
- const root = groupsRoot(groupYeaftDir);
502
- const dir = join(root, groupId);
503
- if (!existsSync(dir) || !loadGroupMeta(dir)) {
504
- throw new GroupCrudError('not_found', groupId);
499
+ export function requireSession(yeaftDir, sessionId) {
500
+ const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
501
+ const root = sessionsRoot(groupYeaftDir);
502
+ const dir = join(root, sessionId);
503
+ if (!existsSync(dir) || !loadSessionMeta(dir)) {
504
+ throw new SessionCrudError('not_found', sessionId);
505
505
  }
506
- return openGroup(root, groupId);
506
+ return openSession(root, sessionId);
507
507
  }
508
508
 
509
509
  /** Convenience: snapshot all non-archived groups for WS broadcast. */
510
- export function snapshotGroups(yeaftDir) {
510
+ export function snapshotSessions(yeaftDir) {
511
511
  const byId = new Map();
512
- for (const group of listGroups(groupsRoot(yeaftDir))) {
512
+ for (const group of listSessions(sessionsRoot(yeaftDir))) {
513
513
  byId.set(group.id, group);
514
514
  }
515
515
  const registry = readWorkDirRegistry(yeaftDir);
516
- for (const [groupId, workDir] of Object.entries(registry)) {
516
+ for (const [sessionId, workDir] of Object.entries(registry)) {
517
517
  const groupYeaftDir = yeaftDirForWorkDir(workDir);
518
- const dir = join(groupsRoot(groupYeaftDir), groupId);
519
- const meta = existsSync(dir) ? loadGroupMeta(dir) : null;
518
+ const dir = join(sessionsRoot(groupYeaftDir), sessionId);
519
+ const meta = existsSync(dir) ? loadSessionMeta(dir) : null;
520
520
  if (meta) byId.set(meta.id, meta);
521
521
  }
522
522
  // Attach per-group config overrides (v1: just `model`). Frontend can
523
523
  // render the effective model without re-querying.
524
524
  for (const meta of byId.values()) {
525
- meta.config = loadGroupConfig(yeaftDir, meta.id);
525
+ meta.config = loadSessionConfig(yeaftDir, meta.id);
526
526
  }
527
527
  return Array.from(byId.values()).sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
528
528
  }
529
529
 
530
- export { DEFAULT_GROUP_ID };
530
+ export { DEFAULT_SESSION_ID };