@yeaft/webchat-agent 1.0.88 → 1.0.89

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": "1.0.88",
3
+ "version": "1.0.89",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
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, yeaftDirForWorkDir } from './sessions/session-crud.js';
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; when provided, storage lives under <workDir>/.yeaft while config still comes from dir/defaults
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. workDir-backed
154
- // Sessions store conversation/session data under <workDir>/.yeaft,
155
- // but runtime config still comes from the agent-local configDir.
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 = sessionWorkDir ? yeaftDirForWorkDir(sessionWorkDir) : configDir;
165
+ const yeaftDir = configDir;
165
166
  const configInitResult = initYeaftDir(configDir);
166
- const storeInitResult = yeaftDir === configDir ? configInitResult : initYeaftDir(yeaftDir);
167
+ const storeInitResult = configInitResult;
167
168
  overrides.dir = configDir;
168
169
 
169
170
  // Log any warnings from directory initialization.
170
- const initWarnings = yeaftDir === configDir
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 may carry its header-selected model override in `config.json` at
4
+ * Each session carries its header-selected model override in `config.json` at
5
5
  * ~/.yeaft/sessions/<sessionId>/config.json
6
- * or, for workDir-backed sessions:
7
- * <workDir>/.yeaft/sessions/<sessionId>/config.json
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, resolveSessionYeaftDir } from './session-crud.js';
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. Honours the
45
- * per-session workDir registry so sessions bound to a project directory
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
- const sessionYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
51
- return join(sessionsRoot(sessionYeaftDir), sessionId, CONFIG_FILE);
50
+ return join(sessionsRoot(yeaftDir), sessionId, CONFIG_FILE);
52
51
  }
53
52
 
54
53
  /**
@@ -38,6 +38,7 @@ import {
38
38
  existsSync,
39
39
  renameSync,
40
40
  rmSync,
41
+ cpSync,
41
42
  readdirSync,
42
43
  statSync,
43
44
  mkdirSync,
@@ -156,101 +157,94 @@ export function unregisterSessionWorkDir(defaultYeaftDir, sessionId) {
156
157
  }
157
158
 
158
159
  /**
159
- * Scan the `.yeaft/sessions/` directory under `workDir` and return every
160
- * session meta we can read. Read-only: never touches the registry.
160
+ * One-way compatibility migration for sessions previously stored under a
161
+ * project `.yeaft/sessions` tree. New reads/writes never use that location, but
162
+ * older installs may still have their only `session.json` + conversation jsonl
163
+ * there and a `group-workdirs.json` registry entry pointing to it.
161
164
  *
162
- * Each returned record carries `workDir` (the normalized path we scanned).
163
- * This utility deliberately does NOT decorate `alreadyRegistered` that
164
- * cross-references the central workdir registry, which is a separate
165
- * concern owned by the handler / caller (see
166
- * `handleYeaftScanWorkdirSessions`). Keeping the utility layer-pure makes
167
- * it usable from contexts that don't have / don't care about the registry
168
- * (e.g. CLI tools, future per-workdir snapshots).
165
+ * We copy missing session directories into the user-level root and never
166
+ * overwrite an existing user-level session. The old project directory is left in
167
+ * place as a safety backup; subsequent runtime paths ignore it.
169
168
  *
170
- * Returns `[]` for missing dir / empty dir / unreadable dir — never throws.
171
- * Sort is `createdAt` descending (most-recent first) because the restore
172
- * UI typically cares about "the session I made yesterday".
169
+ * @param {string} yeaftDir user-level Yeaft root
170
+ * @returns {{migrated:string[], skipped:string[], errors:Array<{sessionId:string,error:string}>}}
171
+ */
172
+ export function migrateRegisteredWorkDirSessions(yeaftDir) {
173
+ const result = { migrated: [], skipped: [], errors: [] };
174
+ if (!yeaftDir) return result;
175
+ const registry = readWorkDirRegistry(yeaftDir);
176
+ const userRoot = sessionsRoot(yeaftDir);
177
+ for (const [sessionId, workDir] of Object.entries(registry)) {
178
+ const normalized = normalizeWorkDir(workDir);
179
+ if (!sessionId || !normalized) continue;
180
+ const sourceDir = join(sessionsRoot(yeaftDirForWorkDir(normalized)), sessionId);
181
+ const targetDir = join(userRoot, sessionId);
182
+ let tmpDir = null;
183
+ try {
184
+ if (existsSync(targetDir)) {
185
+ const targetMeta = loadSessionMeta(targetDir);
186
+ if (targetMeta) {
187
+ result.skipped.push(sessionId);
188
+ } else {
189
+ result.errors.push({ sessionId, error: 'target session directory exists but session metadata is invalid' });
190
+ }
191
+ continue;
192
+ }
193
+ const sourceMeta = existsSync(sourceDir) ? loadSessionMeta(sourceDir) : null;
194
+ if (!sourceMeta) {
195
+ result.skipped.push(sessionId);
196
+ continue;
197
+ }
198
+ mkdirSync(userRoot, { recursive: true });
199
+ tmpDir = join(userRoot, `.migrating-${sessionId}-${randomBytes(6).toString('hex')}`);
200
+ cpSync(sourceDir, tmpDir, { recursive: true, errorOnExist: true });
201
+ const copiedMeta = loadSessionMeta(tmpDir);
202
+ if (!copiedMeta || copiedMeta.id !== sessionId) {
203
+ throw new Error('copied session metadata failed validation');
204
+ }
205
+ renameSync(tmpDir, targetDir);
206
+ tmpDir = null;
207
+ result.migrated.push(sessionId);
208
+ } catch (err) {
209
+ if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
210
+ result.errors.push({ sessionId, error: err?.message || String(err) });
211
+ }
212
+ }
213
+ return result;
214
+ }
215
+
216
+ /**
217
+ * Workdir Session storage was retired: Session metadata/roster/config/history
218
+ * live under the agent-local Yeaft root (`~/.yeaft/sessions`), not under a
219
+ * project `.yeaft/sessions` tree. Keep this helper as a compatibility no-op for
220
+ * older UI flows that may still ask to scan a workdir.
173
221
  *
174
222
  * @param {string} workDir — the working directory to scan.
175
223
  * @returns {Array<object>}
176
224
  */
177
225
  export function scanWorkdirSessions(workDir) {
178
- const normalized = normalizeWorkDir(workDir);
179
- if (!normalized) return [];
180
- const groupYeaftDir = yeaftDirForWorkDir(normalized);
181
- const root = sessionsRoot(groupYeaftDir);
182
- if (!existsSync(root)) return [];
183
- const out = [];
184
- let entries;
185
- try {
186
- entries = readdirSync(root);
187
- } catch {
188
- return [];
189
- }
190
- for (const name of entries) {
191
- if (name.startsWith('.')) continue; // skip .archived-* and dotfiles
192
- const dir = join(root, name);
193
- try { if (!statSync(dir).isDirectory()) continue; } catch { continue; }
194
- const meta = loadSessionMeta(dir);
195
- if (!meta) continue;
196
- out.push({
197
- ...meta,
198
- workDir: normalized,
199
- });
200
- }
201
- return out.sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')));
226
+ return [];
202
227
  }
203
228
 
204
229
  /**
205
- * Register `(sessionId, workDir)` in the central registry so the next
206
- * `snapshotSessions()` includes this session.
207
- *
208
- * Validates that `<workDir>/.yeaft/sessions/<sessionId>/session.json`
209
- * exists and is parseable, with legacy `group.json` as a read fallback.
210
- * Throws:
211
- * - `not_found` — the session dir is not on disk at this workdir
212
- * - `corrupt_meta` — the dir exists but session metadata is missing /
213
- * unreadable / can't be parsed. Surfaced as a distinct
214
- * code so the UI can tell the user "the file is broken"
215
- * instead of "you picked the wrong workdir" (review
216
- * finding I1).
217
- *
218
- * Idempotent: if the same `(sessionId, workDir)` is already registered,
219
- * we still rewrite the entry (with the normalized path) and return the
220
- * fresh meta — no error.
230
+ * Compatibility shim for the retired workdir-restore flow. Session metadata is
231
+ * no longer accepted from project `.yeaft/sessions`; users should create or load
232
+ * Sessions from the user-level `~/.yeaft/sessions` store.
221
233
  *
222
234
  * @param {string} defaultYeaftDir
223
235
  * @param {string} sessionId
224
236
  * @param {string} workDir
225
- * @returns {object} the session meta, with `workDir` set to the normalized path.
237
+ * @returns {object}
226
238
  */
227
239
  export function restoreSessionToRegistry(defaultYeaftDir, sessionId, workDir) {
228
240
  if (!sessionId) throw new SessionCrudError('invalid_session_id', null);
229
- const normalized = normalizeWorkDir(workDir);
230
- if (!normalized) throw new SessionCrudError('invalid_workdir', sessionId);
231
- const groupYeaftDir = yeaftDirForWorkDir(normalized);
232
- const dir = join(sessionsRoot(groupYeaftDir), sessionId);
233
- if (!existsSync(dir)) throw new SessionCrudError('not_found', sessionId);
234
- const meta = loadSessionMeta(dir);
235
- if (!meta) throw new SessionCrudError('corrupt_meta', sessionId, `session metadata missing or unreadable at ${dir} (expected session.json or legacy group.json)`);
236
- registerSessionWorkDir(defaultYeaftDir, sessionId, normalized);
237
- return { ...meta, workDir: normalized };
241
+ throw new SessionCrudError('not_found', sessionId, 'workdir Session restore is retired; Sessions are stored under the user-level Yeaft folder');
238
242
  }
239
243
 
240
244
  export function resolveSessionYeaftDir(defaultYeaftDir, sessionId) {
241
- if (!defaultYeaftDir || !sessionId) return defaultYeaftDir;
242
-
243
- const registry = readWorkDirRegistry(defaultYeaftDir);
244
- const workDir = normalizeWorkDir(registry[sessionId]);
245
- if (workDir) {
246
- const candidate = yeaftDirForWorkDir(workDir);
247
- const candidateDir = join(sessionsRoot(candidate), sessionId);
248
- if (existsSync(candidateDir) && loadSessionMeta(candidateDir)) return candidate;
249
- }
250
-
251
- const defaultSessionDir = join(sessionsRoot(defaultYeaftDir), sessionId);
252
- if (existsSync(defaultSessionDir) && loadSessionMeta(defaultSessionDir)) return defaultYeaftDir;
253
-
245
+ // Legacy helper retained for old call sites. Session storage now always
246
+ // resolves to the agent-local root; project `.yeaft` is only for project
247
+ // assets such as skills/MCP, never Session data.
254
248
  return defaultYeaftDir;
255
249
  }
256
250
 
@@ -328,8 +322,7 @@ export function ensureDefaultSessionIfEmpty(yeaftDir, options = {}) {
328
322
  export function createSessionFromSpec(yeaftDir, spec, options = {}) {
329
323
  const input = spec || {};
330
324
  const normalizedWorkDir = normalizeWorkDir(input.workDir);
331
- const groupYeaftDir = normalizedWorkDir ? yeaftDirForWorkDir(normalizedWorkDir) : yeaftDir;
332
- const memoryRoot = options.memoryRoot || (groupYeaftDir ? join(groupYeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
325
+ const memoryRoot = options.memoryRoot || (yeaftDir ? join(yeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
333
326
  const libDir = options.libDir || DEFAULT_VP_LIB_DIR;
334
327
  const name = String(input.name || '').trim();
335
328
  if (!name) throw new SessionCrudError('invalid_name', null, 'group name required');
@@ -355,7 +348,7 @@ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
355
348
  if (!defaultVpId) defaultVpId = roster[0] || null;
356
349
 
357
350
  const id = makeSessionId(name);
358
- const root = sessionsRoot(groupYeaftDir);
351
+ const root = sessionsRoot(yeaftDir);
359
352
  if (existsSync(join(root, id))) {
360
353
  // Extremely unlikely (ulid suffix), but surface deterministically.
361
354
  throw new SessionCrudError('duplicate', id);
@@ -456,8 +449,7 @@ export function updateSessionConfig(yeaftDir, sessionId, partial) {
456
449
  * own second-confirm modal (acceptance #4 in task-334-slice-specs.md 334m).
457
450
  */
458
451
  export function archiveSession(yeaftDir, sessionId) {
459
- const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
460
- const root = sessionsRoot(groupYeaftDir);
452
+ const root = sessionsRoot(yeaftDir);
461
453
  const srcDir = join(root, sessionId);
462
454
  // Idempotent — nothing on disk, nothing to archive. Workdir-registry is
463
455
  // still cleared in case it points at a stale row.
@@ -487,9 +479,8 @@ export function archiveSession(yeaftDir, sessionId) {
487
479
  * delete cleans up legacy state too.
488
480
  */
489
481
  export function deleteSession(yeaftDir, sessionId, options = {}) {
490
- const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
491
- const memoryRoot = options.memoryRoot || (groupYeaftDir ? join(groupYeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
492
- const root = sessionsRoot(groupYeaftDir);
482
+ const memoryRoot = options.memoryRoot || (yeaftDir ? join(yeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
483
+ const root = sessionsRoot(yeaftDir);
493
484
  const srcDir = join(root, sessionId);
494
485
  const liveExists = existsSync(srcDir) && !!loadSessionMeta(srcDir);
495
486
 
@@ -610,8 +601,7 @@ export function setSessionDefaultVp(yeaftDir, sessionId, vpId) {
610
601
  }
611
602
 
612
603
  export function requireSession(yeaftDir, sessionId) {
613
- const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
614
- const root = sessionsRoot(groupYeaftDir);
604
+ const root = sessionsRoot(yeaftDir);
615
605
  const dir = join(root, sessionId);
616
606
  if (!existsSync(dir) || !loadSessionMeta(dir)) {
617
607
  throw new SessionCrudError('not_found', sessionId);
@@ -627,10 +617,8 @@ export function snapshotSessions(yeaftDir) {
627
617
  }
628
618
  const registry = readWorkDirRegistry(yeaftDir);
629
619
  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);
620
+ const meta = byId.get(sessionId);
621
+ if (meta && !meta.workDir) meta.workDir = normalizeWorkDir(workDir);
634
622
  }
635
623
  // Attach per-group config overrides (v1: just `model`). Frontend can
636
624
  // render the effective model without re-querying.
@@ -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
- yeaftDirForWorkDir,
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 groupYeaftDir = resolveSessionYeaftDir(ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR, sessionId);
193
- const meta = loadSessionMeta(join(sessionsRoot(groupYeaftDir), sessionId));
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. Prefer the agent-local
1319
- // config root: sessionConfigPath() resolves registered workDir-backed
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` — read-only probe: list every yeaft
2469
- * session physically present under `<workDir>/.yeaft/sessions/` along with
2470
- * an `alreadyRegistered` flag (so the restore UI can disable sessions
2471
- * already visible in the sidebar). Never mutates the registry; never
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 layer-pure utility it doesn't read the
2484
- // central workdir registry. The handler is the right layer to fold in
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 a workDir-backed Session can schedule
3537
- // project runtime loading without blocking the user-message hot path. If
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
- const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
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
- let sessionYeaftDir = yeaftDir;
5685
+ const historyYeaftDir = yeaftDir;
5698
5686
  if (sessionId) {
5699
5687
  try {
5700
- sessionYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
5701
- const metaDir = join(sessionsRoot(sessionYeaftDir), sessionId);
5688
+ migrateRegisteredWorkDirSessions(yeaftDir);
5689
+ const metaDir = join(sessionsRoot(yeaftDir), sessionId);
5702
5690
  sessionMetaForRuntime = loadSessionMeta(metaDir);
5703
- } catch { /* best-effort metadata hint */ }
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(sessionYeaftDir);
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;