@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.
- package/connection/message-router.js +20 -13
- package/package.json +1 -1
- package/yeaft/attachments.js +2 -2
- package/yeaft/cli.js +13 -13
- package/yeaft/compact/compactor.js +20 -20
- package/yeaft/conversation/persist.js +95 -95
- package/yeaft/debug-trace.js +12 -12
- package/yeaft/dream-v2/apply.js +15 -15
- package/yeaft/dream-v2/merge.js +12 -12
- package/yeaft/dream-v2/prompts/{extract-group.md → extract-session.md} +12 -12
- package/yeaft/dream-v2/prompts/index.js +3 -3
- package/yeaft/dream-v2/prompts/triage-pass1.md +1 -1
- package/yeaft/dream-v2/runner.js +37 -37
- package/yeaft/dream-v2/segment.js +3 -3
- package/yeaft/dream-v2/session-wiring.js +22 -22
- package/yeaft/dream-v2/state.js +7 -7
- package/yeaft/dream-v2/triage.js +22 -22
- package/yeaft/engine.js +65 -65
- package/yeaft/memory/ams-registry.js +22 -22
- package/yeaft/memory/seed-backfill.js +9 -9
- package/yeaft/memory/store-v2.js +27 -27
- package/yeaft/prompts.js +9 -9
- package/yeaft/routing/loop-guard.js +14 -14
- package/yeaft/routing/router.js +5 -5
- package/yeaft/session.js +5 -5
- package/yeaft/sessions/coordinator.js +96 -20
- package/yeaft/{groups → sessions}/ids.js +3 -3
- package/yeaft/{groups → sessions}/index.js +28 -28
- package/yeaft/sessions/pre-flow.js +178 -42
- package/yeaft/{groups → sessions}/seed-default.js +19 -19
- package/yeaft/{groups/group-config.js → sessions/session-config.js} +29 -29
- package/yeaft/{groups/group-crud.js → sessions/session-crud.js} +113 -113
- package/yeaft/sessions/session-store.js +85 -154
- package/yeaft/stop-hooks.js +4 -4
- package/yeaft/tools/todo-write.js +1 -1
- package/yeaft/tools/types.js +2 -2
- package/yeaft/vp/registry.js +1 -1
- package/yeaft/vp/vp-crud.js +1 -1
- package/yeaft/vp-status-broker.js +28 -28
- package/yeaft/web-bridge.js +411 -398
- package/yeaft/groups/coordinator.js +0 -221
- package/yeaft/groups/group-store.js +0 -212
- package/yeaft/groups/pre-flow.js +0 -329
- /package/yeaft/{groups → sessions}/feature-flag.js +0 -0
- /package/yeaft/{groups → sessions}/project-doc.js +0 -0
- /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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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 `
|
|
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 `
|
|
26
|
+
* Error shape — every throw is a `SessionCrudError` with a stable `.code`:
|
|
27
27
|
* 'not_found' — group id has no dir / meta
|
|
28
|
-
* 'duplicate' —
|
|
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
|
-
|
|
50
|
-
} from './
|
|
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 {
|
|
53
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
|
92
|
-
constructor(code,
|
|
93
|
-
super(message || `${code}: ${
|
|
94
|
-
this.name = '
|
|
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.
|
|
96
|
+
this.sessionId = sessionId;
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
const GROUP_WORKDIR_REGISTRY = 'group-workdirs.json';
|
|
101
101
|
|
|
102
|
-
export function
|
|
103
|
-
return join(yeaftDir, '
|
|
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
|
|
139
|
+
export function registerSessionWorkDir(defaultYeaftDir, sessionId, workDir) {
|
|
140
140
|
const normalized = normalizeWorkDir(workDir);
|
|
141
|
-
if (!defaultYeaftDir || !
|
|
141
|
+
if (!defaultYeaftDir || !sessionId || !normalized) return;
|
|
142
142
|
const registry = readWorkDirRegistry(defaultYeaftDir);
|
|
143
|
-
registry[
|
|
143
|
+
registry[sessionId] = normalized;
|
|
144
144
|
writeWorkDirRegistry(defaultYeaftDir, registry);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
export function
|
|
148
|
-
if (!defaultYeaftDir || !
|
|
147
|
+
export function unregisterSessionWorkDir(defaultYeaftDir, sessionId) {
|
|
148
|
+
if (!defaultYeaftDir || !sessionId) return;
|
|
149
149
|
const registry = readWorkDirRegistry(defaultYeaftDir);
|
|
150
|
-
if (!Object.prototype.hasOwnProperty.call(registry,
|
|
151
|
-
delete registry[
|
|
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
|
|
156
|
-
if (!defaultYeaftDir || !
|
|
157
|
-
const defaultGroupDir = join(
|
|
158
|
-
if (existsSync(defaultGroupDir) &&
|
|
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[
|
|
161
|
+
const workDir = normalizeWorkDir(registry[sessionId]);
|
|
162
162
|
if (workDir) {
|
|
163
163
|
const candidate = yeaftDirForWorkDir(workDir);
|
|
164
|
-
const candidateDir = join(
|
|
165
|
-
if (existsSync(candidateDir) &&
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
194
|
+
const existing = listSessions(sessionsRoot(yeaftDir));
|
|
195
195
|
if (existing.length > 0) {
|
|
196
|
-
return { seeded: false,
|
|
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 } =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
240
|
+
throw new SessionCrudError('reserved', null, `reserved vpId: ${vpId}`);
|
|
241
241
|
}
|
|
242
242
|
const v = validateVpId(vpId);
|
|
243
|
-
if (!v.ok) throw new
|
|
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
|
|
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 =
|
|
256
|
-
const root =
|
|
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
|
|
259
|
+
throw new SessionCrudError('duplicate', id);
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
const handle =
|
|
262
|
+
const handle = createSession(root, { id, name, roster, defaultVpId, workDir: normalizedWorkDir });
|
|
263
263
|
const meta = handle.getMeta();
|
|
264
264
|
handle.close();
|
|
265
|
-
if (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
|
-
|
|
273
|
+
ensureSessionConfigFile(yeaftDir, id);
|
|
274
274
|
if (spec && spec.config && typeof spec.config === 'object') {
|
|
275
|
-
|
|
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
|
-
|
|
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
|
|
300
|
+
export function renameSession(yeaftDir, sessionId, newName) {
|
|
301
301
|
const name = String(newName || '').trim();
|
|
302
|
-
if (!name) throw new
|
|
303
|
-
const handle =
|
|
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
|
|
318
|
+
export function updateSessionAnnouncement(yeaftDir, sessionId, text) {
|
|
319
319
|
if (typeof text !== 'string') {
|
|
320
|
-
throw new
|
|
320
|
+
throw new SessionCrudError('invalid_announcement', sessionId);
|
|
321
321
|
}
|
|
322
322
|
const announcement = text.trim();
|
|
323
|
-
const handle =
|
|
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
|
|
336
|
-
* Group must exist (we call
|
|
335
|
+
* Throws SessionConfigError on validation failure (unknown key, bad type).
|
|
336
|
+
* Group must exist (we call requireSession to assert).
|
|
337
337
|
*/
|
|
338
|
-
export function
|
|
339
|
-
const handle =
|
|
338
|
+
export function updateSessionConfig(yeaftDir, sessionId, partial) {
|
|
339
|
+
const handle = requireSession(yeaftDir, sessionId);
|
|
340
340
|
try {
|
|
341
|
-
return
|
|
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 `
|
|
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
|
|
356
|
-
const groupYeaftDir =
|
|
357
|
-
const root =
|
|
358
|
-
const srcDir = join(root,
|
|
359
|
-
if (!existsSync(srcDir) || !
|
|
360
|
-
throw new
|
|
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}-${
|
|
365
|
+
const dstDir = join(root, `.archived-${ts}-${suffix}-${sessionId}`);
|
|
366
366
|
renameSync(srcDir, dstDir);
|
|
367
|
-
|
|
368
|
-
return {
|
|
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/
|
|
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-*-<
|
|
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
|
|
384
|
-
const groupYeaftDir =
|
|
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 =
|
|
387
|
-
const srcDir = join(root,
|
|
388
|
-
const liveExists = existsSync(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
|
|
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>-<
|
|
396
|
-
if (!name.endsWith(`-${
|
|
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
|
|
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:
|
|
418
|
+
removeScopeDirSync({ kind: 'group', id: sessionId }, { root: memoryRoot });
|
|
419
419
|
} catch (err) {
|
|
420
|
-
console.warn(`[group-crud] failed to remove memory dir for ${
|
|
420
|
+
console.warn(`[group-crud] failed to remove memory dir for ${sessionId}:`, err?.message || err);
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
-
|
|
424
|
-
return {
|
|
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
|
|
433
|
-
const root =
|
|
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,
|
|
455
|
-
const handle =
|
|
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,
|
|
471
|
-
const handle =
|
|
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
|
|
488
|
-
const handle =
|
|
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
|
|
500
|
-
const groupYeaftDir =
|
|
501
|
-
const root =
|
|
502
|
-
const dir = join(root,
|
|
503
|
-
if (!existsSync(dir) || !
|
|
504
|
-
throw new
|
|
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
|
|
506
|
+
return openSession(root, sessionId);
|
|
507
507
|
}
|
|
508
508
|
|
|
509
509
|
/** Convenience: snapshot all non-archived groups for WS broadcast. */
|
|
510
|
-
export function
|
|
510
|
+
export function snapshotSessions(yeaftDir) {
|
|
511
511
|
const byId = new Map();
|
|
512
|
-
for (const group of
|
|
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 [
|
|
516
|
+
for (const [sessionId, workDir] of Object.entries(registry)) {
|
|
517
517
|
const groupYeaftDir = yeaftDirForWorkDir(workDir);
|
|
518
|
-
const dir = join(
|
|
519
|
-
const meta = existsSync(dir) ?
|
|
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 =
|
|
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 {
|
|
530
|
+
export { DEFAULT_SESSION_ID };
|