@yeaft/webchat-agent 0.1.925 → 0.1.926

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.925",
3
+ "version": "0.1.926",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -529,7 +529,7 @@ export class DebugTrace {
529
529
  const target = typeof data.target === 'string' ? data.target : '';
530
530
  if (sessionId) {
531
531
  const isBroadcast = !evtGroupId && !target;
532
- const isThisGroup = evtGroupId === sessionId || target === `group/${sessionId}`;
532
+ const isThisGroup = evtGroupId === sessionId || target === `group/${sessionId}` || target === `session/${sessionId}`;
533
533
  if (!isBroadcast && !isThisGroup) continue;
534
534
  }
535
535
  dreamEvents.push({
@@ -6,7 +6,7 @@
6
6
  * 1. Per-group control state (used to decide whether a group enters
7
7
  * triage and how far to advance the cursor):
8
8
  *
9
- * ~/.yeaft/memory/group/<id>/.dream-state
9
+ * ~/.yeaft/memory/session/<id>/.dream-state
10
10
  *
11
11
  * A 3-line text file:
12
12
  *
@@ -18,7 +18,7 @@
18
18
  * empty / null / 0. The file is rewritten atomically every dream.
19
19
  *
20
20
  * The virtual `_no-group/` group lives at the same path layout
21
- * (`group/_no-group/.dream-state`) and uses the same accessor.
21
+ * (`session/_no-group/.dream-state`) and uses the same accessor.
22
22
  *
23
23
  * 2. Per-scope observability marker, embedded inside the scope's
24
24
  * `memory.md` between two HTML comments at the file's tail:
@@ -63,11 +63,19 @@ const DREAM_BLOCK_CLOSE = '<!-- /dream-state -->';
63
63
  * @returns {Promise<{ lastDreamMessageId: string|null, lastDreamAt: string|null, messageCount: number }>}
64
64
  */
65
65
  export async function readGroupState(root, sessionId) {
66
- const abs = join(root, 'group', sessionId, STATE_FILE);
66
+ const abs = join(root, 'session', sessionId, STATE_FILE);
67
+ const legacyAbs = join(root, 'group', sessionId, STATE_FILE);
67
68
  const empty = { lastDreamMessageId: null, lastDreamAt: null, messageCount: 0 };
68
69
  let raw;
69
70
  try { raw = await fsp.readFile(abs, 'utf8'); }
70
- catch (err) { if (err && err.code === 'ENOENT') return empty; throw err; }
71
+ catch (err) {
72
+ if (!err || err.code !== 'ENOENT') throw err;
73
+ try { raw = await fsp.readFile(legacyAbs, 'utf8'); }
74
+ catch (legacyErr) {
75
+ if (legacyErr && legacyErr.code === 'ENOENT') return empty;
76
+ throw legacyErr;
77
+ }
78
+ }
71
79
  return parseGroupState(raw);
72
80
  }
73
81
 
@@ -80,7 +88,7 @@ export async function readGroupState(root, sessionId) {
80
88
  * @param {{ lastDreamMessageId?: string|null, lastDreamAt?: string|null, messageCount?: number }} state
81
89
  */
82
90
  export async function writeGroupState(root, sessionId, state) {
83
- const dir = join(root, 'group', sessionId);
91
+ const dir = join(root, 'session', sessionId);
84
92
  await fsp.mkdir(dir, { recursive: true });
85
93
  const abs = join(dir, STATE_FILE);
86
94
  const body =
package/yeaft/init.js CHANGED
@@ -82,7 +82,6 @@ const SUBDIRS = [
82
82
  'chat/messages',
83
83
  'chat/cold',
84
84
  'chat/blobs',
85
- 'groups',
86
85
  'sessions',
87
86
  'memory/entries',
88
87
  'tasks',
@@ -43,6 +43,7 @@ import {
43
43
  readFileSync,
44
44
  renameSync,
45
45
  statSync,
46
+ rmSync,
46
47
  unlinkSync,
47
48
  writeFileSync,
48
49
  } from 'fs';
@@ -50,7 +51,7 @@ import { join } from 'path';
50
51
  import { openSegmentIndex } from '../memory/index-db.js';
51
52
 
52
53
  const SENTINEL = '.yeaft-migration.done';
53
- const SENTINEL_VERSION = 2;
54
+ const SENTINEL_VERSION = 3;
54
55
 
55
56
  /**
56
57
  * Run the sessions migration. No-op when sentinel exists.
@@ -216,9 +217,13 @@ export function migrateSessions(yeaftDir) {
216
217
  // so that any pre-rename row still on disk gets the new key shape.
217
218
  const frontmatterRewrites = rewriteAllMessageFrontmatter(yeaftDir, warnings);
218
219
 
219
- // 8. Sentinel version 2 = this consolidated migration (covers what the
220
- // old `.session-migration-v1.done` sentinel covered plus the frontmatter
221
- // rewrite).
220
+ // 8. Cleanup: if this is rerunning after a v2 sentinel, legacy groups/ or
221
+ // chats/ directories may have been recreated. Merge non-duplicate files
222
+ // into sessions/<id>, then remove empty legacy directories.
223
+ const cleanup = cleanupLegacySessionDirs(yeaftDir, warnings);
224
+ moved += cleanup.moved;
225
+
226
+ // 9. Sentinel — version 3 = consolidated migration plus legacy cleanup.
222
227
  writeFileSync(sentinel, JSON.stringify({
223
228
  version: SENTINEL_VERSION,
224
229
  migratedAt: new Date().toISOString(),
@@ -410,6 +415,58 @@ function listDirs(root) {
410
415
  return out;
411
416
  }
412
417
 
418
+ function cleanupLegacySessionDirs(yeaftDir, warnings) {
419
+ const sessionsRoot = join(yeaftDir, 'sessions');
420
+ let moved = 0;
421
+ for (const legacyName of ['groups', 'chats']) {
422
+ const legacyRoot = join(yeaftDir, legacyName);
423
+ if (!existsSync(legacyRoot)) continue;
424
+ for (const id of listDirs(legacyRoot)) {
425
+ const src = join(legacyRoot, id);
426
+ const dst = join(sessionsRoot, id);
427
+ if (!existsSync(dst)) continue;
428
+ mergeLegacyDirIntoSession(src, dst, warnings, `${legacyName}/${id}`);
429
+ removeDirIfEmpty(src, warnings, `${legacyName}/${id}`);
430
+ if (!existsSync(src)) moved++;
431
+ }
432
+ removeDirIfEmpty(legacyRoot, warnings, legacyName);
433
+ }
434
+ return { moved };
435
+ }
436
+
437
+ function mergeLegacyDirIntoSession(src, dst, warnings, label) {
438
+ if (!existsSync(src) || !existsSync(dst)) return;
439
+ let entries = [];
440
+ try { entries = readdirSync(src, { withFileTypes: true }); }
441
+ catch (err) { warnings.push(`${label}: failed to scan legacy dir: ${err.message}`); return; }
442
+
443
+ for (const ent of entries) {
444
+ const from = join(src, ent.name);
445
+ const to = join(dst, ent.name);
446
+ if (!existsSync(to)) {
447
+ try { renameSync(from, to); }
448
+ catch (err) { warnings.push(`${label}: failed to move ${ent.name}: ${err.message}`); }
449
+ continue;
450
+ }
451
+ if (ent.isDirectory()) {
452
+ mergeLegacyDirIntoSession(from, to, warnings, `${label}/${ent.name}`);
453
+ removeDirIfEmpty(from, warnings, `${label}/${ent.name}`);
454
+ } else {
455
+ warnings.push(`${label}: kept duplicate legacy file ${ent.name}`);
456
+ }
457
+ }
458
+ }
459
+
460
+ function removeDirIfEmpty(dir, warnings, label) {
461
+ if (!existsSync(dir)) return;
462
+ try {
463
+ const entries = readdirSync(dir);
464
+ if (entries.length === 0) rmSync(dir, { recursive: true, force: true });
465
+ } catch (err) {
466
+ warnings.push(`${label}: failed to remove empty legacy dir: ${err.message}`);
467
+ }
468
+ }
469
+
413
470
  function rewriteGroupMetaToSessionMeta(sessionDir, warnings) {
414
471
  const oldPath = join(sessionDir, 'group.json');
415
472
  const newPath = join(sessionDir, 'meta.json');
@@ -40,13 +40,13 @@ export function nextMsgId() {
40
40
  export function nextSessionId(slug = 'default') {
41
41
  // Slug-tolerant: lowercase a-z0-9_- only, capped at 24 chars so the
42
42
  // total id stays compact after the suffix is appended.
43
- const safe = String(slug).toLowerCase().replace(/[^a-z0-9_-]+/g, '-').slice(0, 24) || 'group';
43
+ const safe = String(slug).toLowerCase().replace(/[^a-z0-9_-]+/g, '-').slice(0, 24) || 'session';
44
44
  // Append 8 crockford-base32 chars (~40 bits) so re-creating a session
45
45
  // with the same display name yields a fresh id instead of throwing
46
46
  // `duplicate` on the existsSync check in session-crud.js. The
47
47
  // `duplicate` branch is now a true defensive guard rather than the
48
48
  // first-collision footgun it used to be.
49
- return `grp_${safe}_${randEncoded(8)}`;
49
+ return `session_${safe}_${randEncoded(8)}`;
50
50
  }
51
51
 
52
52
  /**
@@ -1,14 +1,14 @@
1
1
  /**
2
- * seed-default.js — First-boot default group (architecture §10 D1).
2
+ * seed-default.js — First-boot default session (architecture §10 D1).
3
3
  *
4
- * When multi-VP mode is first enabled for a user, seed a default group with
5
- * the provided roster (typically `[defaultVpId]`). Idempotent: if the group
4
+ * When multi-VP mode is first enabled for a user, seed a default session with
5
+ * the provided roster (typically `[defaultVpId]`). Idempotent: if the session
6
6
  * already exists on disk, returns the existing handle without overwriting.
7
7
  *
8
8
  * Separation from group-store.createSession:
9
9
  * - createSession throws on duplicate; seed returns the existing handle.
10
- * - seed picks a stable id `grp_default` so UI can deep-link to it.
11
- * - seed is the only place that writes the "default group exists" side
10
+ * - seed picks a stable id `session_default` so UI can deep-link to it.
11
+ * - seed is the only place that writes the "default session exists" side
12
12
  * effect during the bootstrap flow.
13
13
  */
14
14
 
@@ -18,20 +18,20 @@ import { homedir } from 'os';
18
18
  import { openSession, createSession, loadSessionMeta } from './session-store.js';
19
19
  import { seedSummaryIfMissingSync } from '../memory/store.js';
20
20
 
21
- export const DEFAULT_SESSION_ID = 'grp_default';
21
+ export const DEFAULT_SESSION_ID = 'session_default';
22
22
 
23
23
  /**
24
24
  * Default memory root used when callers don't pass `options.memoryRoot`.
25
- * See `groups/group-crud.js` and `vp/vp-crud.js` for the same default;
25
+ * See `sessions/session-crud.js` and `vp/vp-crud.js` for the same default;
26
26
  * production code threads `<yeaftDir>/memory` through to keep test/prod
27
27
  * isolation honest.
28
28
  */
29
29
  const DEFAULT_MEMORY_ROOT = join(homedir(), '.yeaft', 'memory');
30
30
 
31
31
  /**
32
- * Build the default-group seed summary body. Pulled into a helper so
32
+ * Build the default-session seed summary body. Pulled into a helper so
33
33
  * tests can pin the exact format. Mirrors `buildSessionSeedSummary` in
34
- * `group-crud.js` shape, with the "Default group" wording reserved for
34
+ * `session-crud.js` shape, with the "Default session" wording reserved for
35
35
  * the bootstrap path.
36
36
  *
37
37
  * @param {{ name?: string, roster?: string[], defaultVpId?: string|null }} spec
@@ -42,7 +42,7 @@ export function buildDefaultSessionSeedSummary(spec) {
42
42
  const roster = Array.isArray(spec?.roster) ? spec.roster : [];
43
43
  const defaultVpId = spec?.defaultVpId || null;
44
44
  const lines = [`# ${name}`, ''];
45
- lines.push(`Default group with ${roster.length} member${roster.length === 1 ? '' : 's'}.`);
45
+ lines.push(`Default session with ${roster.length} member${roster.length === 1 ? '' : 's'}.`);
46
46
  if (roster.length > 0) lines.push('', `**Members:** ${roster.join(', ')}`);
47
47
  if (defaultVpId) lines.push('', `**Default VP:** ${defaultVpId}`);
48
48
  return lines.join('\n').trim();
@@ -77,7 +77,7 @@ export function seedDefaultSession(yeaftDir, spec = {}) {
77
77
  });
78
78
 
79
79
  // Seed Layer-A resident summary so the very first session — even on a
80
- // brand-new install where only `grp_default` exists — renders a non-
80
+ // brand-new install where only `session_default` exists — renders a non-
81
81
  // empty memory section in the system prompt. No-op once Dream-v2 (or
82
82
  // createSessionFromSpec) has already written one. Best-effort: a memory-
83
83
  // root permission failure must NOT break the bootstrap flow.
@@ -1,17 +1,17 @@
1
1
  /**
2
- * group-crud.js — High-level Group CRUD API (task-334m).
2
+ * session-crud.js — High-level Session CRUD API (task-334m).
3
3
  *
4
- * Wraps the primitives from group-store.js + roster.js into the 5 operations
4
+ * Wraps the primitives from session-store.js + roster.js into the 5 operations
5
5
  * wired to WS events (§Δ10 334m + R6 §Δ31.2):
6
- * createSessionFromSpec — wizard "create new group" (empty or user-picked roster)
6
+ * createSessionFromSpec — wizard "create new session" (empty or user-picked roster)
7
7
  * renameSession — update meta.name; preserves roster / defaultVpId
8
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
- * ensureDefaultSessionIfEmpty(yeaftDir, {libDir}) — if NO group exists on
14
- * disk, seed `grp_default` with roster = every VP in the library, and
13
+ * ensureDefaultSessionIfEmpty(yeaftDir, {libDir}) — if NO session exists on
14
+ * disk, seed `session_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):
@@ -250,21 +250,21 @@ export function resolveSessionYeaftDir(defaultYeaftDir, sessionId) {
250
250
 
251
251
  /** Build a safe group id from a display name (slug + ulid-lite suffix). */
252
252
  export function makeSessionId(name) {
253
- const slug = String(name || 'group')
253
+ const slug = String(name || 'session')
254
254
  .toLowerCase()
255
255
  .replace(/[^a-z0-9]+/g, '-')
256
256
  .replace(/^-+|-+$/g, '')
257
- .slice(0, 24) || 'group';
257
+ .slice(0, 24) || 'session';
258
258
  return nextSessionId(slug);
259
259
  }
260
260
 
261
261
  /**
262
262
  * (B) D1 seed — called at boot (or when multi-VP is first enabled). Idempotent:
263
- * returns `{seeded:false}` if any group already exists on disk (including
264
- * `grp_default`). When empty, seeds with roster = full VP library, sorted
263
+ * returns `{seeded:false}` if any session already exists on disk (including
264
+ * `session_default`). When empty, seeds with roster = full VP library, sorted
265
265
  * alphabetically; defaultVpId = roster[0].
266
266
  *
267
- * When the VP library is also empty, we still seed an empty-roster group so
267
+ * When the VP library is also empty, we still seed an empty-roster session so
268
268
  * the UI has somewhere to land — but defaultVpId is null and downstream
269
269
  * message send will return `no_default_vp` until the user adds a VP.
270
270
  */
@@ -355,7 +355,7 @@ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
355
355
  saveSessionConfig(yeaftDir, id, spec.config);
356
356
  }
357
357
  } catch (err) {
358
- console.warn(`[group-crud] failed to seed config.json for ${id}:`, err?.message || err);
358
+ console.warn(`[session-crud] failed to seed config.json for ${id}:`, err?.message || err);
359
359
  }
360
360
 
361
361
  // Seed Layer-A resident summary so the first session has memory content
@@ -363,12 +363,12 @@ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
363
363
  // Best-effort: a memory-root permission failure must NOT break group create.
364
364
  try {
365
365
  seedSummaryIfMissingSync(
366
- { kind: 'group', id },
366
+ { kind: 'session', id },
367
367
  buildSessionSeedSummary({ name, roster, defaultVpId }),
368
368
  { root: memoryRoot },
369
369
  );
370
370
  } catch (err) {
371
- console.warn(`[group-crud] failed to seed summary.md for ${id}:`, err?.message || err);
371
+ console.warn(`[session-crud] failed to seed summary.md for ${id}:`, err?.message || err);
372
372
  }
373
373
 
374
374
  return meta;
@@ -501,9 +501,12 @@ export function deleteSession(yeaftDir, sessionId, options = {}) {
501
501
  // starts clean. Best-effort — never let memory cleanup fail the CRUD op.
502
502
  // Runs unconditionally so the idempotent path also clears stale memory.
503
503
  try {
504
+ removeScopeDirSync({ kind: 'session', id: sessionId }, { root: memoryRoot });
505
+ // Legacy pre-session memory scopes used memory/group/<id>. Delete both so
506
+ // idempotent removal clears stale summaries for old grp_* sessions too.
504
507
  removeScopeDirSync({ kind: 'group', id: sessionId }, { root: memoryRoot });
505
508
  } catch (err) {
506
- console.warn(`[group-crud] failed to remove memory dir for ${sessionId}:`, err?.message || err);
509
+ console.warn(`[session-crud] failed to remove memory dir for ${sessionId}:`, err?.message || err);
507
510
  }
508
511
 
509
512
  unregisterSessionWorkDir(yeaftDir, sessionId);
@@ -516,9 +519,9 @@ export function deleteSession(yeaftDir, sessionId, options = {}) {
516
519
  }
517
520
 
518
521
  /**
519
- * Sweep any leftover `.archived-*` directories under groups/ that are
522
+ * Sweep any leftover `.archived-*` directories under sessions/ that are
520
523
  * orphans of the old soft-archive flow. Used at boot so users don't see
521
- * ghost groups in subsequent loads. Returns the list of removed paths.
524
+ * ghost sessions in subsequent loads. Returns the list of removed paths.
522
525
  */
523
526
  export function purgeArchivedSessions(yeaftDir) {
524
527
  const root = sessionsRoot(yeaftDir);