@yeaft/webchat-agent 1.0.88 → 1.0.90
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
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* migrate/project-sessions-to-user.js — copy workDir-backed Yeaft Sessions into
|
|
4
|
+
* the agent-local user Yeaft directory so normal startup discovers them from
|
|
5
|
+
* `<userYeaftDir>/sessions/` without depending on the workDir registry.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node agent/yeaft/migrate/project-sessions-to-user.js <projectDir> [userYeaftDir]
|
|
9
|
+
* node agent/yeaft/migrate/project-sessions-to-user.js --dry-run <projectDir>
|
|
10
|
+
* node agent/yeaft/migrate/project-sessions-to-user.js --overwrite <projectDir>
|
|
11
|
+
*
|
|
12
|
+
* Default userYeaftDir is `~/.yeaft`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
cpSync,
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
rmSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
} from 'fs';
|
|
22
|
+
import { homedir } from 'os';
|
|
23
|
+
import { join } from 'path';
|
|
24
|
+
import {
|
|
25
|
+
loadSessionMeta,
|
|
26
|
+
} from '../sessions/session-store.js';
|
|
27
|
+
import {
|
|
28
|
+
normalizeWorkDir,
|
|
29
|
+
scanWorkdirSessions,
|
|
30
|
+
sessionsRoot,
|
|
31
|
+
unregisterSessionWorkDir,
|
|
32
|
+
yeaftDirForWorkDir,
|
|
33
|
+
} from '../sessions/session-crud.js';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} MigrationOptions
|
|
37
|
+
* @property {boolean} [dryRun]
|
|
38
|
+
* @property {boolean} [overwrite]
|
|
39
|
+
* @property {boolean} [deleteSource]
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Copy every readable Session under `<projectDir>/.yeaft/sessions/` into
|
|
44
|
+
* `<userYeaftDir>/sessions/` and copy matching memory scopes. Existing
|
|
45
|
+
* destination Sessions are skipped unless `overwrite` is true.
|
|
46
|
+
*
|
|
47
|
+
* We deliberately unregister migrated Session ids from the legacy workDir
|
|
48
|
+
* registry after a successful copy. Otherwise startup would still prefer the
|
|
49
|
+
* project-backed copy over the newly local copy.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} projectDir
|
|
52
|
+
* @param {string} [userYeaftDir]
|
|
53
|
+
* @param {MigrationOptions} [options]
|
|
54
|
+
* @returns {{ok:boolean, projectDir:string, userYeaftDir:string, scanned:number, copied:number, skipped:number, overwritten:number, removedSources:number, sessions:Array<object>, warnings:string[]}}
|
|
55
|
+
*/
|
|
56
|
+
export function migrateProjectSessionsToUser(projectDir, userYeaftDir = join(homedir(), '.yeaft'), options = {}) {
|
|
57
|
+
const normalizedProjectDir = normalizeWorkDir(projectDir);
|
|
58
|
+
const normalizedUserYeaftDir = normalizeWorkDir(userYeaftDir || join(homedir(), '.yeaft'));
|
|
59
|
+
const dryRun = !!options.dryRun;
|
|
60
|
+
const overwrite = !!options.overwrite;
|
|
61
|
+
const deleteSource = !!options.deleteSource;
|
|
62
|
+
const warnings = [];
|
|
63
|
+
const sessions = [];
|
|
64
|
+
|
|
65
|
+
if (!normalizedProjectDir) {
|
|
66
|
+
throw new Error('projectDir required');
|
|
67
|
+
}
|
|
68
|
+
if (!normalizedUserYeaftDir) {
|
|
69
|
+
throw new Error('userYeaftDir required');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const projectYeaftDir = yeaftDirForWorkDir(normalizedProjectDir);
|
|
73
|
+
const projectSessionsRoot = sessionsRoot(projectYeaftDir);
|
|
74
|
+
const userSessionsRoot = sessionsRoot(normalizedUserYeaftDir);
|
|
75
|
+
const found = scanWorkdirSessions(normalizedProjectDir);
|
|
76
|
+
|
|
77
|
+
let copied = 0;
|
|
78
|
+
let skipped = 0;
|
|
79
|
+
let overwritten = 0;
|
|
80
|
+
let removedSources = 0;
|
|
81
|
+
|
|
82
|
+
if (!dryRun) {
|
|
83
|
+
mkdirSync(userSessionsRoot, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const meta of found) {
|
|
87
|
+
const sessionId = meta.id;
|
|
88
|
+
const sourceDir = join(projectSessionsRoot, sessionId);
|
|
89
|
+
const destDir = join(userSessionsRoot, sessionId);
|
|
90
|
+
const row = {
|
|
91
|
+
id: sessionId,
|
|
92
|
+
name: meta.name || sessionId,
|
|
93
|
+
sourceDir,
|
|
94
|
+
destDir,
|
|
95
|
+
status: 'pending',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (!existsSync(sourceDir) || !loadSessionMeta(sourceDir)) {
|
|
99
|
+
row.status = 'skipped';
|
|
100
|
+
row.reason = 'source_missing_or_corrupt';
|
|
101
|
+
skipped += 1;
|
|
102
|
+
sessions.push(row);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (existsSync(destDir)) {
|
|
107
|
+
if (!overwrite) {
|
|
108
|
+
row.status = 'skipped';
|
|
109
|
+
row.reason = 'destination_exists';
|
|
110
|
+
skipped += 1;
|
|
111
|
+
sessions.push(row);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!dryRun) rmSync(destDir, { recursive: true, force: true });
|
|
115
|
+
overwritten += 1;
|
|
116
|
+
row.overwritten = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!dryRun) {
|
|
120
|
+
cpSync(sourceDir, destDir, { recursive: true, errorOnExist: false });
|
|
121
|
+
const copiedMemoryDirs = copySessionMemory(projectYeaftDir, normalizedUserYeaftDir, sessionId, { overwrite, dryRun, warnings });
|
|
122
|
+
unregisterSessionWorkDir(normalizedUserYeaftDir, sessionId);
|
|
123
|
+
markMigrated(destDir, normalizedProjectDir);
|
|
124
|
+
if (deleteSource) {
|
|
125
|
+
rmSync(sourceDir, { recursive: true, force: true });
|
|
126
|
+
removeCopiedSourceMemory(copiedMemoryDirs, warnings);
|
|
127
|
+
removedSources += 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
row.status = dryRun ? 'would_copy' : 'copied';
|
|
132
|
+
copied += 1;
|
|
133
|
+
sessions.push(row);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
projectDir: normalizedProjectDir,
|
|
139
|
+
userYeaftDir: normalizedUserYeaftDir,
|
|
140
|
+
scanned: found.length,
|
|
141
|
+
copied,
|
|
142
|
+
skipped,
|
|
143
|
+
overwritten,
|
|
144
|
+
removedSources,
|
|
145
|
+
sessions,
|
|
146
|
+
warnings,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function copySessionMemory(projectYeaftDir, userYeaftDir, sessionId, { overwrite, dryRun, warnings }) {
|
|
151
|
+
const pairs = [
|
|
152
|
+
[join(projectYeaftDir, 'memory', 'session', sessionId), join(userYeaftDir, 'memory', 'session', sessionId)],
|
|
153
|
+
[join(projectYeaftDir, 'memory', 'sessions', sessionId), join(userYeaftDir, 'memory', 'sessions', sessionId)],
|
|
154
|
+
[join(projectYeaftDir, 'memory', 'group', sessionId), join(userYeaftDir, 'memory', 'group', sessionId)],
|
|
155
|
+
];
|
|
156
|
+
const copied = [];
|
|
157
|
+
for (const [src, dst] of pairs) {
|
|
158
|
+
if (!existsSync(src)) continue;
|
|
159
|
+
if (existsSync(dst)) {
|
|
160
|
+
if (!overwrite) {
|
|
161
|
+
warnings.push(`memory destination exists; skipping ${dst}`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!dryRun) rmSync(dst, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
if (!dryRun) {
|
|
167
|
+
mkdirSync(join(dst, '..'), { recursive: true });
|
|
168
|
+
cpSync(src, dst, { recursive: true, errorOnExist: false });
|
|
169
|
+
}
|
|
170
|
+
copied.push(src);
|
|
171
|
+
}
|
|
172
|
+
return copied;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function removeCopiedSourceMemory(copiedMemoryDirs, warnings) {
|
|
176
|
+
for (const dir of copiedMemoryDirs) {
|
|
177
|
+
try {
|
|
178
|
+
rmSync(dir, { recursive: true, force: true });
|
|
179
|
+
} catch (err) {
|
|
180
|
+
warnings.push(`failed to remove source memory ${dir}: ${err?.message || err}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function markMigrated(destDir, projectDir) {
|
|
186
|
+
const markerDir = join(destDir, '.migrations');
|
|
187
|
+
mkdirSync(markerDir, { recursive: true });
|
|
188
|
+
writeFileSync(
|
|
189
|
+
join(markerDir, 'project-sessions-to-user.json'),
|
|
190
|
+
`${JSON.stringify({ migratedAt: new Date().toISOString(), sourceProjectDir: projectDir }, null, 2)}\n`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseCliArgs(argv) {
|
|
195
|
+
const args = argv.slice(2);
|
|
196
|
+
const options = { dryRun: false, overwrite: false, deleteSource: false };
|
|
197
|
+
const positional = [];
|
|
198
|
+
for (const arg of args) {
|
|
199
|
+
if (arg === '--dry-run') options.dryRun = true;
|
|
200
|
+
else if (arg === '--overwrite') options.overwrite = true;
|
|
201
|
+
else if (arg === '--delete-source') options.deleteSource = true;
|
|
202
|
+
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
203
|
+
else positional.push(arg);
|
|
204
|
+
}
|
|
205
|
+
return { options, positional };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function printUsage() {
|
|
209
|
+
console.log('Usage: node agent/yeaft/migrate/project-sessions-to-user.js [--dry-run] [--overwrite] [--delete-source] <projectDir> [userYeaftDir]');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
213
|
+
const { options, positional } = parseCliArgs(process.argv);
|
|
214
|
+
if (options.help || positional.length < 1) {
|
|
215
|
+
printUsage();
|
|
216
|
+
process.exitCode = options.help ? 0 : 1;
|
|
217
|
+
} else {
|
|
218
|
+
try {
|
|
219
|
+
const result = migrateProjectSessionsToUser(positional[0], positional[1], options);
|
|
220
|
+
console.log(JSON.stringify(result, null, 2));
|
|
221
|
+
if (result.scanned === 0) process.exitCode = 2;
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error(err?.message || String(err));
|
|
224
|
+
process.exitCode = 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
package/yeaft/session.js
CHANGED
|
@@ -45,7 +45,7 @@ import { TaskManager } from './tasks/manager.js';
|
|
|
45
45
|
// resumes with the same onDemand/recent membership it had on
|
|
46
46
|
// disconnect. Engine.#runQuery uses the registry to populate the
|
|
47
47
|
// AMS each turn and to run `memory/adjust.js` post-turn.
|
|
48
|
-
import { ensureDefaultSessionIfEmpty,
|
|
48
|
+
import { ensureDefaultSessionIfEmpty, migrateRegisteredWorkDirSessions } from './sessions/session-crud.js';
|
|
49
49
|
import { seedDefaultVps } from './vp/seed-defaults.js';
|
|
50
50
|
import { topUpDefaultVps } from './vp/seed-topup.js';
|
|
51
51
|
import { archiveLegacyScopes } from './memory/seed-backfill.js';
|
|
@@ -71,7 +71,7 @@ const DEFAULT_COMPACT_TRIGGER_RATIO = 0.7;
|
|
|
71
71
|
/**
|
|
72
72
|
* @typedef {Object} SessionOptions
|
|
73
73
|
* @property {string} [dir] — Yeaft data directory override (default: ~/.yeaft)
|
|
74
|
-
* @property {string} [workDir] — Session workDir;
|
|
74
|
+
* @property {string} [workDir] — Session workDir; used only for project-tier assets such as skills/MCP, while Session data stays under the user-level dir
|
|
75
75
|
* @property {string} [model] — Model override
|
|
76
76
|
* @property {string} [language] — Language override ('en' | 'zh')
|
|
77
77
|
* @property {boolean} [debug] — Debug mode override
|
|
@@ -150,9 +150,10 @@ export async function loadSession(options = {}) {
|
|
|
150
150
|
|
|
151
151
|
// ─── 1. Determine config + store directories ─────────────
|
|
152
152
|
// Must happen BEFORE loadConfig so that first-run generates a
|
|
153
|
-
// default config.json that loadConfig can read.
|
|
154
|
-
//
|
|
155
|
-
//
|
|
153
|
+
// default config.json that loadConfig can read. Session data
|
|
154
|
+
// (metadata/roster/config/messages/memory/tasks) is agent-user data
|
|
155
|
+
// and stays under configDir (`~/.yeaft`). workDir is only a project
|
|
156
|
+
// tier for assets such as skills and MCP config.
|
|
156
157
|
const overrides = { ...configOverrides };
|
|
157
158
|
if (dir) overrides.dir = dir;
|
|
158
159
|
if (model) overrides.model = model;
|
|
@@ -161,15 +162,13 @@ export async function loadSession(options = {}) {
|
|
|
161
162
|
|
|
162
163
|
const sessionWorkDir = typeof workDir === 'string' && workDir.trim() ? workDir.trim() : '';
|
|
163
164
|
const configDir = overrides.dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
164
|
-
const yeaftDir =
|
|
165
|
+
const yeaftDir = configDir;
|
|
165
166
|
const configInitResult = initYeaftDir(configDir);
|
|
166
|
-
const storeInitResult =
|
|
167
|
+
const storeInitResult = configInitResult;
|
|
167
168
|
overrides.dir = configDir;
|
|
168
169
|
|
|
169
170
|
// Log any warnings from directory initialization.
|
|
170
|
-
const initWarnings =
|
|
171
|
-
? configInitResult.warnings
|
|
172
|
-
: [...configInitResult.warnings, ...storeInitResult.warnings];
|
|
171
|
+
const initWarnings = configInitResult.warnings;
|
|
173
172
|
for (const w of initWarnings) {
|
|
174
173
|
console.warn(`[Yeaft] ${w}`);
|
|
175
174
|
}
|
|
@@ -219,6 +218,22 @@ export async function loadSession(options = {}) {
|
|
|
219
218
|
// dream directly. Existing users have already migrated
|
|
220
219
|
// (state file in ~/.yeaft/.memory-v2-migration.json).
|
|
221
220
|
|
|
221
|
+
// ─── 2.3 Project-session migration ─────────────────────
|
|
222
|
+
// Session data used to be allowed under `<workDir>/.yeaft/sessions`.
|
|
223
|
+
// It is now user-level only (`~/.yeaft/sessions`). Copy any registered
|
|
224
|
+
// project-backed sessions into the user root before stores open.
|
|
225
|
+
try {
|
|
226
|
+
const migration = migrateRegisteredWorkDirSessions(configDir);
|
|
227
|
+
if (migration.migrated.length > 0 || migration.errors.length > 0) {
|
|
228
|
+
console.log(
|
|
229
|
+
`[Yeaft] session project-store migration: migrated=${migration.migrated.length} ` +
|
|
230
|
+
`errors=${migration.errors.length}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.warn(`[Yeaft] session project-store migration failed: ${err?.message || err}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
222
237
|
// ─── 2a. Permission pre-check ─────────────────────────
|
|
223
238
|
// If the data dir is not writable, mark session as read-only.
|
|
224
239
|
// Persistence (conversation, memory, dream) is skipped in this mode.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session-config.js — Per-session selected model override state.
|
|
3
3
|
*
|
|
4
|
-
* Each session
|
|
4
|
+
* Each session carries its header-selected model override in `config.json` at
|
|
5
5
|
* ~/.yeaft/sessions/<sessionId>/config.json
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
*
|
|
7
|
+
* Session config is user-level state. Project `.yeaft` directories may provide
|
|
8
|
+
* project assets such as skills/MCP config, but must not own Session config.
|
|
8
9
|
*
|
|
9
10
|
* v1 schema (intentionally tiny — extend via additive keys only):
|
|
10
11
|
* {
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
import { existsSync, readFileSync } from 'fs';
|
|
25
26
|
import { join } from 'path';
|
|
26
27
|
import { writeAtomic } from '../storage/index.js';
|
|
27
|
-
import { sessionsRoot
|
|
28
|
+
import { sessionsRoot } from './session-crud.js';
|
|
28
29
|
|
|
29
30
|
const CONFIG_FILE = 'config.json';
|
|
30
31
|
|
|
@@ -41,14 +42,12 @@ export class SessionConfigError extends Error {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
|
-
* Resolve the on-disk path for a session's config.json.
|
|
45
|
-
*
|
|
46
|
-
* keep their config alongside the session metadata.
|
|
45
|
+
* Resolve the on-disk path for a session's config.json. Always uses the
|
|
46
|
+
* agent-local Yeaft root (`~/.yeaft` in production), not a project `.yeaft`.
|
|
47
47
|
*/
|
|
48
48
|
export function sessionConfigPath(yeaftDir, sessionId) {
|
|
49
49
|
if (!yeaftDir) return null;
|
|
50
|
-
|
|
51
|
-
return join(sessionsRoot(sessionYeaftDir), sessionId, CONFIG_FILE);
|
|
50
|
+
return join(sessionsRoot(yeaftDir), sessionId, CONFIG_FILE);
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
/**
|
|
@@ -43,12 +43,13 @@ import {
|
|
|
43
43
|
mkdirSync,
|
|
44
44
|
readFileSync,
|
|
45
45
|
writeFileSync,
|
|
46
|
+
cpSync,
|
|
46
47
|
} from 'fs';
|
|
47
48
|
import { randomBytes } from 'crypto';
|
|
48
49
|
import { homedir } from 'os';
|
|
49
50
|
import { isAbsolute, join, resolve } from 'path';
|
|
50
51
|
import {
|
|
51
|
-
openSession, createSession,
|
|
52
|
+
openSession, createSession, loadSessionMeta,
|
|
52
53
|
} from './session-store.js';
|
|
53
54
|
import { addVp as rosterAdd, removeVp as rosterRemove, setDefaultVp } from './roster.js';
|
|
54
55
|
import { seedDefaultSession, DEFAULT_SESSION_ID } from './seed-default.js';
|
|
@@ -56,6 +57,14 @@ import { nextSessionId, validateVpId, isReservedVpId } from './ids.js';
|
|
|
56
57
|
import { scanVpLibrary, DEFAULT_VP_LIB_DIR } from '../vp/vp-store.js';
|
|
57
58
|
import { seedSummaryIfMissingSync, removeScopeDirSync } from '../memory/store.js';
|
|
58
59
|
import { ensureSessionConfigFile, saveSessionConfig, loadSessionConfig } from './session-config.js';
|
|
60
|
+
import {
|
|
61
|
+
addOrUpdateManifestSession,
|
|
62
|
+
ensureSessionsManifest,
|
|
63
|
+
hasSessionManifest,
|
|
64
|
+
listManifestSessions,
|
|
65
|
+
removeManifestSession,
|
|
66
|
+
resolveManifestSessionDir,
|
|
67
|
+
} from './session-manifest.js';
|
|
59
68
|
|
|
60
69
|
/**
|
|
61
70
|
* Default memory root used when callers don't pass `options.memoryRoot`.
|
|
@@ -155,6 +164,59 @@ export function unregisterSessionWorkDir(defaultYeaftDir, sessionId) {
|
|
|
155
164
|
writeWorkDirRegistry(defaultYeaftDir, registry);
|
|
156
165
|
}
|
|
157
166
|
|
|
167
|
+
function ensureSessionManifestReady(yeaftDir) {
|
|
168
|
+
return ensureSessionsManifest(yeaftDir, {
|
|
169
|
+
sessionsRoot: sessionsRoot(yeaftDir),
|
|
170
|
+
registry: readWorkDirRegistry(yeaftDir),
|
|
171
|
+
yeaftDirForWorkDir,
|
|
172
|
+
sessionsRootForYeaftDir: sessionsRoot,
|
|
173
|
+
copySessionExtras: (projectYeaftDir, sessionId) => copySessionExtras(projectYeaftDir, yeaftDir, sessionId),
|
|
174
|
+
unregisterSessionWorkDir: (sessionId) => unregisterSessionWorkDir(yeaftDir, sessionId),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function copySessionExtras(sourceYeaftDir, destYeaftDir, sessionId) {
|
|
179
|
+
for (const family of ['session', 'sessions', 'group']) {
|
|
180
|
+
const src = join(sourceYeaftDir, 'memory', family, sessionId);
|
|
181
|
+
const dst = join(destYeaftDir, 'memory', family, sessionId);
|
|
182
|
+
if (!existsSync(src) || existsSync(dst)) continue;
|
|
183
|
+
mkdirSync(join(dst, '..'), { recursive: true });
|
|
184
|
+
cpSync(src, dst, { recursive: true, errorOnExist: false });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* One-way compatibility migration for sessions previously stored under a
|
|
190
|
+
* project `.yeaft/sessions` tree. New steady-state discovery uses
|
|
191
|
+
* `sessions-manifest.json`; this helper bootstraps that manifest once and
|
|
192
|
+
* reports what happened for boot logs/tests.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} yeaftDir user-level Yeaft root
|
|
195
|
+
* @returns {{migrated:string[], skipped:string[], errors:Array<{sessionId:string,error:string}>}}
|
|
196
|
+
*/
|
|
197
|
+
export function migrateRegisteredWorkDirSessions(yeaftDir) {
|
|
198
|
+
const result = { migrated: [], skipped: [], errors: [] };
|
|
199
|
+
if (!yeaftDir || hasSessionManifest(yeaftDir)) return result;
|
|
200
|
+
|
|
201
|
+
const invalidTargets = new Set();
|
|
202
|
+
const registry = readWorkDirRegistry(yeaftDir);
|
|
203
|
+
for (const [sessionId, workDir] of Object.entries(registry)) {
|
|
204
|
+
const normalized = normalizeWorkDir(workDir);
|
|
205
|
+
if (!sessionId || !normalized) continue;
|
|
206
|
+
const targetDir = join(sessionsRoot(yeaftDir), sessionId);
|
|
207
|
+
if (existsSync(targetDir) && !loadSessionMeta(targetDir)) {
|
|
208
|
+
invalidTargets.add(sessionId);
|
|
209
|
+
result.errors.push({ sessionId, error: 'target session directory exists but session metadata is invalid' });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const bootstrap = ensureSessionManifestReady(yeaftDir);
|
|
214
|
+
result.migrated = Array.isArray(bootstrap.migratedIds) ? bootstrap.migratedIds.slice() : [];
|
|
215
|
+
result.skipped = (Array.isArray(bootstrap.skippedIds) ? bootstrap.skippedIds : [])
|
|
216
|
+
.filter(sessionId => !invalidTargets.has(sessionId));
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
158
220
|
/**
|
|
159
221
|
* Scan the `.yeaft/sessions/` directory under `workDir` and return every
|
|
160
222
|
* session meta we can read. Read-only: never touches the registry.
|
|
@@ -228,18 +290,46 @@ export function restoreSessionToRegistry(defaultYeaftDir, sessionId, workDir) {
|
|
|
228
290
|
if (!sessionId) throw new SessionCrudError('invalid_session_id', null);
|
|
229
291
|
const normalized = normalizeWorkDir(workDir);
|
|
230
292
|
if (!normalized) throw new SessionCrudError('invalid_workdir', sessionId);
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
if (!existsSync(
|
|
234
|
-
const meta = loadSessionMeta(
|
|
235
|
-
if (!meta) throw new SessionCrudError('corrupt_meta', sessionId, `session metadata missing or unreadable at ${
|
|
236
|
-
|
|
237
|
-
|
|
293
|
+
const projectYeaftDir = yeaftDirForWorkDir(normalized);
|
|
294
|
+
const sourceDir = join(sessionsRoot(projectYeaftDir), sessionId);
|
|
295
|
+
if (!existsSync(sourceDir)) throw new SessionCrudError('not_found', sessionId);
|
|
296
|
+
const meta = loadSessionMeta(sourceDir);
|
|
297
|
+
if (!meta) throw new SessionCrudError('corrupt_meta', sessionId, `session metadata missing or unreadable at ${sourceDir} (expected session.json or legacy group.json)`);
|
|
298
|
+
|
|
299
|
+
ensureSessionManifestReady(defaultYeaftDir);
|
|
300
|
+
const root = sessionsRoot(defaultYeaftDir);
|
|
301
|
+
const destDir = join(root, sessionId);
|
|
302
|
+
const manifestDir = resolveManifestSessionDir(defaultYeaftDir, sessionId);
|
|
303
|
+
if (existsSync(destDir)) {
|
|
304
|
+
const existing = loadSessionMeta(destDir);
|
|
305
|
+
if (existing && manifestDir === destDir) return { ...existing, workDir: existing.workDir || normalized };
|
|
306
|
+
throw new SessionCrudError('duplicate', sessionId, `session already exists at ${destDir}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
mkdirSync(root, { recursive: true });
|
|
310
|
+
cpSync(sourceDir, destDir, { recursive: true, errorOnExist: true });
|
|
311
|
+
const importedMeta = { ...meta, workDir: normalized };
|
|
312
|
+
const handle = openSession(root, sessionId);
|
|
313
|
+
try {
|
|
314
|
+
handle.saveMeta(importedMeta);
|
|
315
|
+
} finally {
|
|
316
|
+
handle.close();
|
|
317
|
+
}
|
|
318
|
+
copySessionExtras(projectYeaftDir, defaultYeaftDir, sessionId);
|
|
319
|
+
addOrUpdateManifestSession(defaultYeaftDir, importedMeta, destDir);
|
|
320
|
+
unregisterSessionWorkDir(defaultYeaftDir, sessionId);
|
|
321
|
+
return importedMeta;
|
|
238
322
|
}
|
|
239
323
|
|
|
240
324
|
export function resolveSessionYeaftDir(defaultYeaftDir, sessionId) {
|
|
241
325
|
if (!defaultYeaftDir || !sessionId) return defaultYeaftDir;
|
|
242
326
|
|
|
327
|
+
const manifestReady = hasSessionManifest(defaultYeaftDir);
|
|
328
|
+
ensureSessionManifestReady(defaultYeaftDir);
|
|
329
|
+
const manifestDir = resolveManifestSessionDir(defaultYeaftDir, sessionId);
|
|
330
|
+
if (manifestDir) return join(manifestDir, '..', '..');
|
|
331
|
+
if (manifestReady) return defaultYeaftDir;
|
|
332
|
+
|
|
243
333
|
const registry = readWorkDirRegistry(defaultYeaftDir);
|
|
244
334
|
const workDir = normalizeWorkDir(registry[sessionId]);
|
|
245
335
|
if (workDir) {
|
|
@@ -290,7 +380,8 @@ function scanSortedVpIds(libDir) {
|
|
|
290
380
|
export function ensureDefaultSessionIfEmpty(yeaftDir, options = {}) {
|
|
291
381
|
const libDir = options.libDir || DEFAULT_VP_LIB_DIR;
|
|
292
382
|
const memoryRoot = options.memoryRoot || DEFAULT_MEMORY_ROOT;
|
|
293
|
-
|
|
383
|
+
ensureSessionManifestReady(yeaftDir);
|
|
384
|
+
const existing = listManifestSessions(yeaftDir).map(row => row.meta);
|
|
294
385
|
if (existing.length > 0) {
|
|
295
386
|
return { seeded: false, sessionId: existing[0].id };
|
|
296
387
|
}
|
|
@@ -307,6 +398,8 @@ export function ensureDefaultSessionIfEmpty(yeaftDir, options = {}) {
|
|
|
307
398
|
defaultVpId,
|
|
308
399
|
memoryRoot,
|
|
309
400
|
});
|
|
401
|
+
const meta = group.getMeta();
|
|
402
|
+
if (meta) addOrUpdateManifestSession(yeaftDir, meta, join(sessionsRoot(yeaftDir), meta.id));
|
|
310
403
|
return {
|
|
311
404
|
seeded: created,
|
|
312
405
|
sessionId: group.id,
|
|
@@ -328,7 +421,8 @@ export function ensureDefaultSessionIfEmpty(yeaftDir, options = {}) {
|
|
|
328
421
|
export function createSessionFromSpec(yeaftDir, spec, options = {}) {
|
|
329
422
|
const input = spec || {};
|
|
330
423
|
const normalizedWorkDir = normalizeWorkDir(input.workDir);
|
|
331
|
-
|
|
424
|
+
ensureSessionManifestReady(yeaftDir);
|
|
425
|
+
const groupYeaftDir = yeaftDir;
|
|
332
426
|
const memoryRoot = options.memoryRoot || (groupYeaftDir ? join(groupYeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
|
|
333
427
|
const libDir = options.libDir || DEFAULT_VP_LIB_DIR;
|
|
334
428
|
const name = String(input.name || '').trim();
|
|
@@ -364,7 +458,7 @@ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
|
|
|
364
458
|
const handle = createSession(root, { id, name, roster, defaultVpId, workDir: normalizedWorkDir });
|
|
365
459
|
const meta = handle.getMeta();
|
|
366
460
|
handle.close();
|
|
367
|
-
|
|
461
|
+
addOrUpdateManifestSession(yeaftDir, meta, join(root, id));
|
|
368
462
|
|
|
369
463
|
// Per-session config (v1: model only). We always create an empty
|
|
370
464
|
// config.json so hand-editing tools can find a session-level override
|
|
@@ -463,6 +557,7 @@ export function archiveSession(yeaftDir, sessionId) {
|
|
|
463
557
|
// still cleared in case it points at a stale row.
|
|
464
558
|
if (!existsSync(srcDir) || !loadSessionMeta(srcDir)) {
|
|
465
559
|
unregisterSessionWorkDir(yeaftDir, sessionId);
|
|
560
|
+
removeManifestSession(yeaftDir, sessionId);
|
|
466
561
|
return { sessionId, archivedAs: null, alreadyGone: true };
|
|
467
562
|
}
|
|
468
563
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -471,6 +566,7 @@ export function archiveSession(yeaftDir, sessionId) {
|
|
|
471
566
|
const dstDir = join(root, `.archived-${ts}-${suffix}-${sessionId}`);
|
|
472
567
|
renameSync(srcDir, dstDir);
|
|
473
568
|
unregisterSessionWorkDir(yeaftDir, sessionId);
|
|
569
|
+
removeManifestSession(yeaftDir, sessionId);
|
|
474
570
|
return { sessionId, archivedAs: dstDir, alreadyGone: false };
|
|
475
571
|
}
|
|
476
572
|
|
|
@@ -529,6 +625,7 @@ export function deleteSession(yeaftDir, sessionId, options = {}) {
|
|
|
529
625
|
}
|
|
530
626
|
|
|
531
627
|
unregisterSessionWorkDir(yeaftDir, sessionId);
|
|
628
|
+
removeManifestSession(yeaftDir, sessionId);
|
|
532
629
|
return {
|
|
533
630
|
sessionId,
|
|
534
631
|
deleted: liveExists,
|
|
@@ -621,16 +718,10 @@ export function requireSession(yeaftDir, sessionId) {
|
|
|
621
718
|
|
|
622
719
|
/** Convenience: snapshot all non-archived groups for WS broadcast. */
|
|
623
720
|
export function snapshotSessions(yeaftDir) {
|
|
721
|
+
ensureSessionManifestReady(yeaftDir);
|
|
624
722
|
const byId = new Map();
|
|
625
|
-
for (const
|
|
626
|
-
byId.set(
|
|
627
|
-
}
|
|
628
|
-
const registry = readWorkDirRegistry(yeaftDir);
|
|
629
|
-
for (const [sessionId, workDir] of Object.entries(registry)) {
|
|
630
|
-
const groupYeaftDir = yeaftDirForWorkDir(workDir);
|
|
631
|
-
const dir = join(sessionsRoot(groupYeaftDir), sessionId);
|
|
632
|
-
const meta = existsSync(dir) ? loadSessionMeta(dir) : null;
|
|
633
|
-
if (meta) byId.set(meta.id, meta);
|
|
723
|
+
for (const row of listManifestSessions(yeaftDir)) {
|
|
724
|
+
byId.set(row.meta.id, row.meta);
|
|
634
725
|
}
|
|
635
726
|
// Attach per-group config overrides (v1: just `model`). Frontend can
|
|
636
727
|
// render the effective model without re-querying.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-manifest.js — canonical index for Yeaft Session folders.
|
|
3
|
+
*
|
|
4
|
+
* Boot flow:
|
|
5
|
+
* 1. If `<yeaftDir>/sessions-manifest.json` exists, load it and trust only
|
|
6
|
+
* valid entries whose folder still contains readable session metadata.
|
|
7
|
+
* 2. If it does not exist, bootstrap once from legacy folder-based storage:
|
|
8
|
+
* copy registered workDir-backed Sessions into `<yeaftDir>/sessions/`, then
|
|
9
|
+
* index `<yeaftDir>/sessions/` into the manifest.
|
|
10
|
+
*
|
|
11
|
+
* The legacy `group-workdirs.json` registry remains readable for bootstrap only.
|
|
12
|
+
* New steady-state discovery should use this manifest.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
cpSync,
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
statSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from 'fs';
|
|
25
|
+
import { join } from 'path';
|
|
26
|
+
import { loadSessionMeta } from './session-store.js';
|
|
27
|
+
|
|
28
|
+
export const SESSIONS_MANIFEST_FILE = 'sessions-manifest.json';
|
|
29
|
+
const MANIFEST_VERSION = 1;
|
|
30
|
+
|
|
31
|
+
export function sessionManifestPath(yeaftDir) {
|
|
32
|
+
return join(yeaftDir, SESSIONS_MANIFEST_FILE);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function hasSessionManifest(yeaftDir) {
|
|
36
|
+
return !!yeaftDir && existsSync(sessionManifestPath(yeaftDir));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function loadSessionsManifest(yeaftDir) {
|
|
40
|
+
if (!hasSessionManifest(yeaftDir)) return null;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(readFileSync(sessionManifestPath(yeaftDir), 'utf8'));
|
|
43
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
44
|
+
if (!Array.isArray(parsed.sessions)) return null;
|
|
45
|
+
return {
|
|
46
|
+
version: Number(parsed.version) || MANIFEST_VERSION,
|
|
47
|
+
generatedAt: typeof parsed.generatedAt === 'string' ? parsed.generatedAt : '',
|
|
48
|
+
sessions: parsed.sessions
|
|
49
|
+
.filter(row => row && typeof row === 'object' && typeof row.id === 'string' && typeof row.path === 'string')
|
|
50
|
+
.map(row => ({
|
|
51
|
+
id: row.id,
|
|
52
|
+
path: row.path,
|
|
53
|
+
name: typeof row.name === 'string' ? row.name : row.id,
|
|
54
|
+
createdAt: typeof row.createdAt === 'string' ? row.createdAt : '',
|
|
55
|
+
workDir: typeof row.workDir === 'string' ? row.workDir : '',
|
|
56
|
+
})),
|
|
57
|
+
};
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function writeSessionsManifest(yeaftDir, sessions) {
|
|
64
|
+
if (!yeaftDir) throw new Error('yeaftDir required');
|
|
65
|
+
mkdirSync(yeaftDir, { recursive: true });
|
|
66
|
+
const manifest = {
|
|
67
|
+
version: MANIFEST_VERSION,
|
|
68
|
+
generatedAt: new Date().toISOString(),
|
|
69
|
+
sessions: dedupeSessions(sessions).sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || ''))),
|
|
70
|
+
};
|
|
71
|
+
writeFileSync(sessionManifestPath(yeaftDir), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
72
|
+
return manifest;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildManifestFromLocalSessions(yeaftDir, sessionsRoot) {
|
|
76
|
+
const rows = [];
|
|
77
|
+
if (!sessionsRoot || !existsSync(sessionsRoot)) return rows;
|
|
78
|
+
let entries;
|
|
79
|
+
try {
|
|
80
|
+
entries = readdirSync(sessionsRoot);
|
|
81
|
+
} catch {
|
|
82
|
+
return rows;
|
|
83
|
+
}
|
|
84
|
+
for (const name of entries) {
|
|
85
|
+
if (name.startsWith('.')) continue;
|
|
86
|
+
const dir = join(sessionsRoot, name);
|
|
87
|
+
try { if (!statSync(dir).isDirectory()) continue; } catch { continue; }
|
|
88
|
+
const meta = loadSessionMeta(dir);
|
|
89
|
+
if (!meta) continue;
|
|
90
|
+
rows.push(manifestRowFromMeta(meta, dir));
|
|
91
|
+
}
|
|
92
|
+
return rows;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function listManifestSessions(yeaftDir) {
|
|
96
|
+
const manifest = loadSessionsManifest(yeaftDir);
|
|
97
|
+
if (!manifest) return [];
|
|
98
|
+
const rows = [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
for (const entry of manifest.sessions) {
|
|
101
|
+
if (seen.has(entry.id)) continue;
|
|
102
|
+
seen.add(entry.id);
|
|
103
|
+
const meta = loadSessionMeta(entry.path);
|
|
104
|
+
if (!meta) continue;
|
|
105
|
+
rows.push({ meta, dir: entry.path });
|
|
106
|
+
}
|
|
107
|
+
return rows.sort((a, b) => String(a.meta.createdAt || '').localeCompare(String(b.meta.createdAt || '')));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function resolveManifestSessionDir(yeaftDir, sessionId) {
|
|
111
|
+
const manifest = loadSessionsManifest(yeaftDir);
|
|
112
|
+
if (!manifest || !sessionId) return null;
|
|
113
|
+
const entry = manifest.sessions.find(row => row.id === sessionId);
|
|
114
|
+
if (!entry) return null;
|
|
115
|
+
if (!loadSessionMeta(entry.path)) return null;
|
|
116
|
+
return entry.path;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Bootstrap the manifest from legacy storage if it does not exist.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} yeaftDir
|
|
123
|
+
* @param {{sessionsRoot:string, registry?:object, yeaftDirForWorkDir:(workDir:string)=>string, sessionsRootForYeaftDir:(dir:string)=>string, copySessionExtras?:(projectYeaftDir:string, sessionId:string)=>void, unregisterSessionWorkDir?:(sessionId:string)=>void}} options
|
|
124
|
+
*/
|
|
125
|
+
export function ensureSessionsManifest(yeaftDir, options) {
|
|
126
|
+
if (hasSessionManifest(yeaftDir)) {
|
|
127
|
+
return { created: false, migrated: 0, skipped: 0, manifest: loadSessionsManifest(yeaftDir) };
|
|
128
|
+
}
|
|
129
|
+
const root = options.sessionsRoot;
|
|
130
|
+
mkdirSync(root, { recursive: true });
|
|
131
|
+
|
|
132
|
+
let migrated = 0;
|
|
133
|
+
let skipped = 0;
|
|
134
|
+
const migratedIds = [];
|
|
135
|
+
const skippedIds = [];
|
|
136
|
+
const registry = options.registry && typeof options.registry === 'object' ? options.registry : {};
|
|
137
|
+
for (const [sessionId, workDir] of Object.entries(registry)) {
|
|
138
|
+
if (!sessionId || !workDir) continue;
|
|
139
|
+
const projectYeaftDir = options.yeaftDirForWorkDir(workDir);
|
|
140
|
+
const projectRoot = options.sessionsRootForYeaftDir(projectYeaftDir);
|
|
141
|
+
const sourceDir = join(projectRoot, sessionId);
|
|
142
|
+
const destDir = join(root, sessionId);
|
|
143
|
+
const meta = existsSync(sourceDir) ? loadSessionMeta(sourceDir) : null;
|
|
144
|
+
if (!meta) {
|
|
145
|
+
skipped += 1;
|
|
146
|
+
skippedIds.push(sessionId);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (existsSync(destDir)) {
|
|
150
|
+
if (loadSessionMeta(destDir)) {
|
|
151
|
+
skipped += 1;
|
|
152
|
+
skippedIds.push(sessionId);
|
|
153
|
+
if (typeof options.unregisterSessionWorkDir === 'function') {
|
|
154
|
+
options.unregisterSessionWorkDir(sessionId);
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
skipped += 1;
|
|
159
|
+
skippedIds.push(sessionId);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
cpSync(sourceDir, destDir, { recursive: true, errorOnExist: false });
|
|
163
|
+
migrated += 1;
|
|
164
|
+
migratedIds.push(sessionId);
|
|
165
|
+
if (typeof options.copySessionExtras === 'function') {
|
|
166
|
+
options.copySessionExtras(projectYeaftDir, sessionId);
|
|
167
|
+
}
|
|
168
|
+
if (typeof options.unregisterSessionWorkDir === 'function') {
|
|
169
|
+
options.unregisterSessionWorkDir(sessionId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const manifest = writeSessionsManifest(yeaftDir, buildManifestFromLocalSessions(yeaftDir, root));
|
|
174
|
+
return { created: true, migrated, skipped, migratedIds, skippedIds, manifest };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function addOrUpdateManifestSession(yeaftDir, meta, dir) {
|
|
178
|
+
const current = loadSessionsManifest(yeaftDir) || { sessions: [] };
|
|
179
|
+
const rows = current.sessions.filter(row => row.id !== meta.id);
|
|
180
|
+
rows.push(manifestRowFromMeta(meta, dir));
|
|
181
|
+
return writeSessionsManifest(yeaftDir, rows);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function removeManifestSession(yeaftDir, sessionId) {
|
|
185
|
+
const current = loadSessionsManifest(yeaftDir);
|
|
186
|
+
if (!current) return null;
|
|
187
|
+
return writeSessionsManifest(yeaftDir, current.sessions.filter(row => row.id !== sessionId));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function manifestRowFromMeta(meta, dir) {
|
|
191
|
+
return {
|
|
192
|
+
id: meta.id,
|
|
193
|
+
name: meta.name || meta.id,
|
|
194
|
+
path: dir,
|
|
195
|
+
createdAt: meta.createdAt || '',
|
|
196
|
+
workDir: meta.workDir || '',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function dedupeSessions(sessions) {
|
|
201
|
+
const byId = new Map();
|
|
202
|
+
for (const row of sessions || []) {
|
|
203
|
+
if (!row || !row.id || !row.path) continue;
|
|
204
|
+
byId.set(row.id, {
|
|
205
|
+
id: row.id,
|
|
206
|
+
name: row.name || row.id,
|
|
207
|
+
path: row.path,
|
|
208
|
+
createdAt: row.createdAt || '',
|
|
209
|
+
workDir: row.workDir || '',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return Array.from(byId.values());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function __testRemoveSessionsManifest(yeaftDir) {
|
|
216
|
+
rmSync(sessionManifestPath(yeaftDir), { force: true });
|
|
217
|
+
}
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -45,12 +45,11 @@ import {
|
|
|
45
45
|
removeMember,
|
|
46
46
|
setSessionDefaultVp,
|
|
47
47
|
snapshotSessions,
|
|
48
|
-
resolveSessionYeaftDir,
|
|
49
48
|
sessionsRoot,
|
|
50
49
|
scanWorkdirSessions,
|
|
51
50
|
restoreSessionToRegistry,
|
|
52
51
|
readWorkDirRegistry,
|
|
53
|
-
|
|
52
|
+
migrateRegisteredWorkDirSessions,
|
|
54
53
|
} from './sessions/session-crud.js';
|
|
55
54
|
import { openSession, loadSessionMeta } from './sessions/session-store.js';
|
|
56
55
|
import { loadSessionConfig, resolveSessionConfig, SessionConfigError } from './sessions/session-config.js';
|
|
@@ -189,8 +188,8 @@ function scheduleYeaftLoadHistoryMetadataReplay(sessionId) {
|
|
|
189
188
|
let projectRuntime = null;
|
|
190
189
|
if (sessionId) {
|
|
191
190
|
try {
|
|
192
|
-
const
|
|
193
|
-
const meta = loadSessionMeta(join(sessionsRoot(
|
|
191
|
+
const metaRoot = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
192
|
+
const meta = loadSessionMeta(join(sessionsRoot(metaRoot), sessionId));
|
|
194
193
|
projectRuntime = await ensureProjectRuntimeForSessionMeta(meta);
|
|
195
194
|
} catch { /* best-effort project metadata */ }
|
|
196
195
|
}
|
|
@@ -429,13 +428,6 @@ function projectRuntimeKey(workDir) {
|
|
|
429
428
|
return normalizeSessionWorkDir(workDir) || '__agent_cwd__';
|
|
430
429
|
}
|
|
431
430
|
|
|
432
|
-
function resolveStoreYeaftDirForSession(defaultYeaftDir, { sessionId = null, sessionMeta = null, workDir = '' } = {}) {
|
|
433
|
-
const normalizedWorkDir = normalizeSessionWorkDir(workDir || sessionMeta?.workDir);
|
|
434
|
-
if (normalizedWorkDir) return yeaftDirForWorkDir(normalizedWorkDir);
|
|
435
|
-
if (sessionId) return resolveSessionYeaftDir(defaultYeaftDir, sessionId);
|
|
436
|
-
return defaultYeaftDir;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
431
|
function createThreadId() {
|
|
440
432
|
return `thr_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
441
433
|
}
|
|
@@ -1315,10 +1307,8 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
|
|
|
1315
1307
|
const key = threadKey(sessionId, vpId, threadId);
|
|
1316
1308
|
if (!session) throw new Error('getOrCreateVpEngine: session not loaded');
|
|
1317
1309
|
// Per-session config overlay (v1: model only). Falls back to the
|
|
1318
|
-
// session's user-level config when no override is set.
|
|
1319
|
-
//
|
|
1320
|
-
// sessions from there, while still allowing a later agent-local session in
|
|
1321
|
-
// the same bridge runtime to read its own override after a workDir-first boot.
|
|
1310
|
+
// session's user-level config when no override is set. Session config is
|
|
1311
|
+
// always resolved from the agent-local root; project `.yeaft` is ignored.
|
|
1322
1312
|
const sessionConfigRoot = liveConfigRoot();
|
|
1323
1313
|
const groupCfg = loadSessionConfig(sessionConfigRoot, sessionId);
|
|
1324
1314
|
const effectiveConfig = resolveSessionConfig(session.config, groupCfg);
|
|
@@ -2465,14 +2455,10 @@ export function handleYeaftCreateSession(msg) {
|
|
|
2465
2455
|
}
|
|
2466
2456
|
|
|
2467
2457
|
/**
|
|
2468
|
-
* `yeaft_scan_workdir_sessions` —
|
|
2469
|
-
*
|
|
2470
|
-
*
|
|
2471
|
-
*
|
|
2472
|
-
* throws on missing/empty directories.
|
|
2473
|
-
*
|
|
2474
|
-
* Pairs with `handleYeaftRestoreSession` for the "Restore session from a
|
|
2475
|
-
* workdir" flow — see plan rosy-snuggling-waterfall.md.
|
|
2458
|
+
* `yeaft_scan_workdir_sessions` — compatibility endpoint for the retired
|
|
2459
|
+
* workdir Session scan flow. Session data now lives under the user-level
|
|
2460
|
+
* `~/.yeaft/sessions`; project `.yeaft` is reserved for project assets such as
|
|
2461
|
+
* skills and MCP config.
|
|
2476
2462
|
*/
|
|
2477
2463
|
export function handleYeaftScanWorkdirSessions(msg) {
|
|
2478
2464
|
const requestId = msg && msg.requestId;
|
|
@@ -2480,11 +2466,8 @@ export function handleYeaftScanWorkdirSessions(msg) {
|
|
|
2480
2466
|
const workDir = String(msg && msg.workDir || '').trim();
|
|
2481
2467
|
if (!workDir) throw new SessionCrudError('invalid_workdir', null, 'workDir required');
|
|
2482
2468
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
2483
|
-
// scanWorkdirSessions is a
|
|
2484
|
-
//
|
|
2485
|
-
// `alreadyRegistered` because that flag couples the per-workdir scan
|
|
2486
|
-
// to a specific agent's registry (the same scan from a CLI tool or
|
|
2487
|
-
// a different agent would compare against a different registry).
|
|
2469
|
+
// scanWorkdirSessions is now a compatibility no-op because Session data is
|
|
2470
|
+
// user-level only. Keep the decoration shape stable for older clients.
|
|
2488
2471
|
const sessions = scanWorkdirSessions(workDir);
|
|
2489
2472
|
const registry = readWorkDirRegistry(yeaftDir);
|
|
2490
2473
|
const decorated = sessions.map(s => ({
|
|
@@ -3533,8 +3516,8 @@ async function runYeaftSessionSend(msg) {
|
|
|
3533
3516
|
return;
|
|
3534
3517
|
}
|
|
3535
3518
|
|
|
3536
|
-
// Open the session metadata first so
|
|
3537
|
-
//
|
|
3519
|
+
// Open the user-level session metadata first so project runtime loading can
|
|
3520
|
+
// use the stored workDir without blocking the user-message hot path. If
|
|
3538
3521
|
// metadata is missing we still boot the agent runtime for a useful error
|
|
3539
3522
|
// response, then fail on the session open check.
|
|
3540
3523
|
let sessionHandle = null;
|
|
@@ -3542,8 +3525,7 @@ async function runYeaftSessionSend(msg) {
|
|
|
3542
3525
|
let sessionMetaForRuntime = null;
|
|
3543
3526
|
const openSessionStart = perfNowMs();
|
|
3544
3527
|
try {
|
|
3545
|
-
|
|
3546
|
-
sessionRoot = sessionsRoot(groupYeaftDir);
|
|
3528
|
+
sessionRoot = sessionsRoot(yeaftDir);
|
|
3547
3529
|
const dir = join(sessionRoot, sessionId);
|
|
3548
3530
|
if (existsSync(dir) && loadSessionMeta(dir)) {
|
|
3549
3531
|
sessionHandle = openSession(sessionRoot, sessionId);
|
|
@@ -3564,6 +3546,17 @@ async function runYeaftSessionSend(msg) {
|
|
|
3564
3546
|
|
|
3565
3547
|
const ensureSessionStart = perfNowMs();
|
|
3566
3548
|
await ensureSessionLoaded({ sessionMeta: sessionMetaForRuntime, perfTraceId });
|
|
3549
|
+
if (!sessionHandle) {
|
|
3550
|
+
try {
|
|
3551
|
+
const dir = join(sessionRoot, sessionId);
|
|
3552
|
+
if (existsSync(dir) && loadSessionMeta(dir)) {
|
|
3553
|
+
sessionHandle = openSession(sessionRoot, sessionId);
|
|
3554
|
+
sessionMetaForRuntime = sessionHandle.getMeta();
|
|
3555
|
+
}
|
|
3556
|
+
} catch (err) {
|
|
3557
|
+
console.warn('[Yeaft] yeaft_session_chat: migrated session reopen failed', err?.message || err);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3567
3560
|
scheduleProjectRuntimeLoad(sessionMetaForRuntime?.workDir);
|
|
3568
3561
|
traceDuration('session_send.ensure_session_loaded', ensureSessionStart);
|
|
3569
3562
|
|
|
@@ -3927,11 +3920,6 @@ async function ensureSessionLoaded(opts = {}) {
|
|
|
3927
3920
|
sessionLoadPromise = (async () => {
|
|
3928
3921
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
3929
3922
|
const normalizedWorkDir = normalizeSessionWorkDir(opts?.workDir || opts?.sessionMeta?.workDir);
|
|
3930
|
-
const sessionYeaftDir = resolveStoreYeaftDirForSession(yeaftDir, {
|
|
3931
|
-
sessionId: opts?.sessionId || opts?.sessionMeta?.id || null,
|
|
3932
|
-
sessionMeta: opts?.sessionMeta || null,
|
|
3933
|
-
workDir: normalizedWorkDir,
|
|
3934
|
-
});
|
|
3935
3923
|
session = await loadSession({
|
|
3936
3924
|
...(yeaftDir && { dir: yeaftDir }),
|
|
3937
3925
|
...(normalizedWorkDir && { workDir: normalizedWorkDir }),
|
|
@@ -5694,13 +5682,15 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
5694
5682
|
ensureYeaftConversationId();
|
|
5695
5683
|
|
|
5696
5684
|
let sessionMetaForRuntime = null;
|
|
5697
|
-
|
|
5685
|
+
const historyYeaftDir = yeaftDir;
|
|
5698
5686
|
if (sessionId) {
|
|
5699
5687
|
try {
|
|
5700
|
-
|
|
5701
|
-
const metaDir = join(sessionsRoot(
|
|
5688
|
+
migrateRegisteredWorkDirSessions(yeaftDir);
|
|
5689
|
+
const metaDir = join(sessionsRoot(yeaftDir), sessionId);
|
|
5702
5690
|
sessionMetaForRuntime = loadSessionMeta(metaDir);
|
|
5703
|
-
} catch {
|
|
5691
|
+
} catch (err) {
|
|
5692
|
+
console.warn('[Yeaft] load_history project-store migration failed:', err?.message || err);
|
|
5693
|
+
}
|
|
5704
5694
|
}
|
|
5705
5695
|
|
|
5706
5696
|
// First paint must not wait for full Yeaft runtime boot (MCP connects,
|
|
@@ -5708,7 +5698,7 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
5708
5698
|
// source of truth and can be opened cheaply, so replay the visible message
|
|
5709
5699
|
// window immediately, then finish loadSession below for actual turns.
|
|
5710
5700
|
const coldStoreStart = perfNowMs();
|
|
5711
|
-
const coldStore = new ConversationStore(
|
|
5701
|
+
const coldStore = new ConversationStore(historyYeaftDir);
|
|
5712
5702
|
traceDuration('history.cold_store_open', coldStoreStart);
|
|
5713
5703
|
if (sessionId && (afterSeqRaw !== null || afterMessageId)) {
|
|
5714
5704
|
let afterSeq = afterSeqRaw;
|