@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 +1 -1
- package/yeaft/debug-trace.js +1 -1
- package/yeaft/dream/state.js +13 -5
- package/yeaft/init.js +0 -1
- package/yeaft/migrate/sessions.js +61 -4
- package/yeaft/sessions/ids.js +2 -2
- package/yeaft/sessions/seed-default.js +11 -11
- package/yeaft/sessions/session-crud.js +19 -16
package/package.json
CHANGED
package/yeaft/debug-trace.js
CHANGED
|
@@ -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({
|
package/yeaft/dream/state.js
CHANGED
|
@@ -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/
|
|
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
|
-
* (`
|
|
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, '
|
|
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) {
|
|
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, '
|
|
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
|
@@ -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 =
|
|
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.
|
|
220
|
-
//
|
|
221
|
-
//
|
|
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');
|
package/yeaft/sessions/ids.js
CHANGED
|
@@ -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) || '
|
|
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 `
|
|
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
|
|
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
|
|
5
|
-
* the provided roster (typically `[defaultVpId]`). Idempotent: if the
|
|
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 `
|
|
11
|
-
* - seed is the only place that writes the "default
|
|
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 = '
|
|
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 `
|
|
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-
|
|
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
|
-
* `
|
|
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
|
|
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 `
|
|
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
|
-
*
|
|
2
|
+
* session-crud.js — High-level Session CRUD API (task-334m).
|
|
3
3
|
*
|
|
4
|
-
* Wraps the primitives from
|
|
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
|
|
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
|
|
14
|
-
* disk, seed `
|
|
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 || '
|
|
253
|
+
const slug = String(name || 'session')
|
|
254
254
|
.toLowerCase()
|
|
255
255
|
.replace(/[^a-z0-9]+/g, '-')
|
|
256
256
|
.replace(/^-+|-+$/g, '')
|
|
257
|
-
.slice(0, 24) || '
|
|
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
|
|
264
|
-
* `
|
|
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
|
|
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(`[
|
|
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: '
|
|
366
|
+
{ kind: 'session', id },
|
|
367
367
|
buildSessionSeedSummary({ name, roster, defaultVpId }),
|
|
368
368
|
{ root: memoryRoot },
|
|
369
369
|
);
|
|
370
370
|
} catch (err) {
|
|
371
|
-
console.warn(`[
|
|
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(`[
|
|
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
|
|
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
|
|
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);
|