@yeaft/webchat-agent 0.1.916 → 0.1.917
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 +2 -2
- package/yeaft/conversation/persist.js +0 -12
- package/yeaft/{dream-v2 → dream}/apply.js +3 -3
- package/yeaft/{dream-v2 → dream}/limits.js +1 -1
- package/yeaft/{dream-v2 → dream}/merge.js +1 -1
- package/yeaft/{dream-v2 → dream}/prompts/index.js +1 -1
- package/yeaft/{dream-v2 → dream}/runner.js +2 -2
- package/yeaft/{dream-v2 → dream}/schedule.js +1 -1
- package/yeaft/{dream-v2 → dream}/segment.js +1 -1
- package/yeaft/{dream-v2 → dream}/session-wiring.js +6 -6
- package/yeaft/{dream-v2 → dream}/snapshot.js +1 -1
- package/yeaft/{dream-v2 → dream}/state.js +3 -3
- package/yeaft/{dream-v2 → dream}/triage.js +2 -2
- package/yeaft/engine.js +2 -2
- package/yeaft/history-compact.js +1 -1
- package/yeaft/init.js +19 -25
- package/yeaft/memory/adjust.js +1 -1
- package/yeaft/memory/ams.js +1 -1
- package/yeaft/memory/preflow.js +1 -1
- package/yeaft/memory/seed-backfill.js +1 -1
- package/yeaft/memory/{store-v2.js → store.js} +2 -2
- package/yeaft/migrate/sessions.js +485 -0
- package/yeaft/session.js +4 -4
- package/yeaft/sessions/seed-default.js +1 -1
- package/yeaft/sessions/session-crud.js +1 -1
- package/yeaft/stop-hooks.js +1 -1
- package/yeaft/vp/vp-crud.js +1 -1
- package/yeaft/web-bridge.js +1 -1
- package/yeaft/migrate/sessions-v1.js +0 -295
- /package/yeaft/{dream-v2 → dream}/prompts/create.md +0 -0
- /package/yeaft/{dream-v2 → dream}/prompts/extract-session.md +0 -0
- /package/yeaft/{dream-v2 → dream}/prompts/extract-topic.md +0 -0
- /package/yeaft/{dream-v2 → dream}/prompts/extract-user.md +0 -0
- /package/yeaft/{dream-v2 → dream}/prompts/extract-vp.md +0 -0
- /package/yeaft/{dream-v2 → dream}/prompts/summarize-scope.md +0 -0
- /package/yeaft/{dream-v2 → dream}/prompts/triage-pass1.md +0 -0
- /package/yeaft/{dream-v2 → dream}/prompts/triage-pass2.md +0 -0
- /package/yeaft/{dream-v2 → dream}/prompts/update.md +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yeaft/webchat-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.917",
|
|
4
4
|
"description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"files": [
|
|
37
37
|
"**/*.js",
|
|
38
38
|
"yeaft/templates/**/*.md",
|
|
39
|
-
"yeaft/dream
|
|
39
|
+
"yeaft/dream/prompts/**/*.md"
|
|
40
40
|
],
|
|
41
41
|
"license": "MIT",
|
|
42
42
|
"author": "Yeaft",
|
|
@@ -320,18 +320,6 @@ export function parseMessage(raw) {
|
|
|
320
320
|
case 'threadId': msg.threadId = value; break;
|
|
321
321
|
case 'sourceThreadId': msg.sourceThreadId = value; break;
|
|
322
322
|
case 'sessionId': msg.sessionId = value; break;
|
|
323
|
-
// Legacy alias: pre-rename messages stamped `groupId:` (PR #881
|
|
324
|
-
// groups → sessions sweep). Without this case, files in
|
|
325
|
-
// ~/.yeaft/conversation/messages/*.md from before the rename
|
|
326
|
-
// parse with msg.sessionId undefined and #loadSessionHotMessages'
|
|
327
|
-
// `.filter(m => m?.sessionId)` drops every row — LLM replay sees
|
|
328
|
-
// an empty history. `if (!msg.sessionId)` keeps new-key precedence
|
|
329
|
-
// for hand-edited files that carry both keys. See
|
|
330
|
-
// migrate/sessions-v1.js — that pass moved directories but did
|
|
331
|
-
// NOT rewrite per-message frontmatter, which is why this alias
|
|
332
|
-
// has to live forever (or until a frontmatter-rewrite migration
|
|
333
|
-
// lands and the legacy dir is empty).
|
|
334
|
-
case 'groupId': if (!msg.sessionId) msg.sessionId = value; break;
|
|
335
323
|
case 'chatId': msg.chatId = value; break;
|
|
336
324
|
case 'speakerVpId': msg.speakerVpId = value; break;
|
|
337
325
|
case 'attachmentsB64':
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dream
|
|
2
|
+
* dream/apply.js.
|
|
3
3
|
*
|
|
4
4
|
* Execute one merged-target action: either UPDATE an existing scope's
|
|
5
5
|
* memory.md/summary.md, or CREATE a new one (currently topic-only).
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { promises as fsp } from 'fs';
|
|
22
22
|
import { join, dirname } from 'path';
|
|
23
23
|
|
|
24
|
-
import { writeMemory, writeSummary, readMemory, readSummary } from '../memory/store
|
|
24
|
+
import { writeMemory, writeSummary, readMemory, readSummary } from '../memory/store.js';
|
|
25
25
|
import { withDreamMarker } from './state.js';
|
|
26
26
|
import { batchSourcesForApply, needsBatchedApply, truncateMessage } from './segment.js';
|
|
27
27
|
import { snapshotScope } from './snapshot.js';
|
|
@@ -111,7 +111,7 @@ function renderSourceBlocks(sources, language) {
|
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
113
|
* Translate `target` like 'group/g-eng' or 'topic/sci/phys' to a Scope
|
|
114
|
-
* understood by store
|
|
114
|
+
* understood by store. Throws if the path is malformed.
|
|
115
115
|
*
|
|
116
116
|
* @param {string} target
|
|
117
117
|
* @returns {{ kind: string, id?: string, path?: string[] }}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dream
|
|
2
|
+
* dream/prompts/index.js — load + render the four dream prompts.
|
|
3
3
|
*
|
|
4
4
|
* Prompts live as `.md` files in this directory. We read once and cache.
|
|
5
5
|
* Rendering is plain `{{name}}` substitution; no nested logic, no escaping —
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dream
|
|
2
|
+
* dream/runner.js.
|
|
3
3
|
*
|
|
4
4
|
* Orchestrates one full dream pass:
|
|
5
5
|
*
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
import { existsSync } from 'fs';
|
|
42
42
|
import { join } from 'path';
|
|
43
43
|
|
|
44
|
-
import { listScopes, readSummary } from '../memory/store
|
|
44
|
+
import { listScopes, readSummary } from '../memory/store.js';
|
|
45
45
|
import {
|
|
46
46
|
DEFAULT_LIMITS,
|
|
47
47
|
} from './limits.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dream
|
|
2
|
+
* dream/session-wiring.js wire-up for session.js.
|
|
3
3
|
*
|
|
4
|
-
* Bridges the framework-agnostic `runDream` orchestrator (dream
|
|
4
|
+
* Bridges the framework-agnostic `runDream` orchestrator (dream/runner.js)
|
|
5
5
|
* to the live yeaft session: groups store, conversation log, LLM adapter,
|
|
6
6
|
* and the engine's progress event sink.
|
|
7
7
|
*
|
|
@@ -137,7 +137,7 @@ function makeLlm(session) {
|
|
|
137
137
|
const adapter = session.adapter;
|
|
138
138
|
const model = session.config?.fastModelId || session.config?.model;
|
|
139
139
|
if (!adapter || typeof adapter.call !== 'function') {
|
|
140
|
-
throw new Error(`dream
|
|
140
|
+
throw new Error(`dream: no adapter.call available (pass=${pass})`);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// Bug 2: emit loop event so the debug panel shows dream LLM API calls.
|
|
@@ -304,7 +304,7 @@ export function createV2DreamScheduler(session) {
|
|
|
304
304
|
// Best-effort console for debug builds.
|
|
305
305
|
if (session.config?.debug) {
|
|
306
306
|
// eslint-disable-next-line no-console
|
|
307
|
-
console.log('[dream
|
|
307
|
+
console.log('[dream]', stampedEvt);
|
|
308
308
|
}
|
|
309
309
|
} catch { /* never let progress reporting kill the run */ }
|
|
310
310
|
};
|
|
@@ -473,7 +473,7 @@ export async function bootInitEmptyGroups(args) {
|
|
|
473
473
|
if (empty.length === 0) return out;
|
|
474
474
|
if (args.config?.debug) {
|
|
475
475
|
// eslint-disable-next-line no-console
|
|
476
|
-
console.log(`[dream
|
|
476
|
+
console.log(`[dream] boot init: triggering empty-AMS dream for ${empty.length} group(s):`, empty);
|
|
477
477
|
}
|
|
478
478
|
// Fire-and-forget; the scheduler swallows its own failures.
|
|
479
479
|
Promise.resolve(args.dreamScheduler.triggerDreamForScopes(empty)).catch(() => {});
|
|
@@ -554,7 +554,7 @@ export async function bootCatchUpStaleDream(args) {
|
|
|
554
554
|
|
|
555
555
|
if (args.config?.debug) {
|
|
556
556
|
// eslint-disable-next-line no-console
|
|
557
|
-
console.log(`[dream
|
|
557
|
+
console.log(`[dream] boot catch-up: stale (lastDreamAt=${out.lastDreamAt}, ageMs=${out.ageMs}); firing one non-manual tick.`);
|
|
558
558
|
}
|
|
559
559
|
|
|
560
560
|
// Non-manual: MIN_NEW_PER_GROUP still applies. Use the public
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dream
|
|
2
|
+
* dream/state.js.
|
|
3
3
|
*
|
|
4
4
|
* Three pieces of state, tracked separately:
|
|
5
5
|
*
|
|
@@ -115,7 +115,7 @@ function parseGroupState(raw) {
|
|
|
115
115
|
|
|
116
116
|
// ─── per-scope dream error sink ────────────────────────────────
|
|
117
117
|
//
|
|
118
|
-
// Why: dream
|
|
118
|
+
// Why: dream silently swallowed exceptions at the triage / apply
|
|
119
119
|
// catch sites — the only sink was `trace.event('dream_progress', evt)`
|
|
120
120
|
// and a `config.debug`-gated `console.log` in `session-wiring.js`. With
|
|
121
121
|
// `debug=false` (the default), there was no on-disk evidence that a
|
|
@@ -130,7 +130,7 @@ function parseGroupState(raw) {
|
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
132
|
* Resolve a memoryRoot + scope-string to the scope directory.
|
|
133
|
-
* The scope string is the same shape dream
|
|
133
|
+
* The scope string is the same shape dream already uses internally:
|
|
134
134
|
* `'user'`, `'vp/<vpId>'`, `'group/<sessionId>'`, `'feature/<id>'`, etc.
|
|
135
135
|
*
|
|
136
136
|
* Pure path-join; does NOT create the directory. The writer creates it.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dream
|
|
2
|
+
* dream/triage.js.
|
|
3
3
|
*
|
|
4
4
|
* Decide, for one group's diff, which scopes should be touched by Apply.
|
|
5
5
|
* The decision is two-staged on purpose:
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* below.)
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
import { isValidTopic } from '../memory/store
|
|
38
|
+
import { isValidTopic } from '../memory/store.js';
|
|
39
39
|
import { truncateMessage } from './segment.js';
|
|
40
40
|
import { render } from './prompts/index.js';
|
|
41
41
|
|
package/yeaft/engine.js
CHANGED
|
@@ -28,7 +28,7 @@ import { runCompact as runCompactOrchestrator } from './compact/orchestrator.js'
|
|
|
28
28
|
import { evaluateCompactTriggers } from './compact/triggers.js';
|
|
29
29
|
import { archiveTurn } from './archive/turn-archive.js';
|
|
30
30
|
import { archiveToolResults } from './archive/tool-results.js';
|
|
31
|
-
import { readSummary as readScopeSummary } from './memory/store
|
|
31
|
+
import { readSummary as readScopeSummary } from './memory/store.js';
|
|
32
32
|
import { runAdjust } from './memory/adjust.js';
|
|
33
33
|
import { isVpSeedBackfillStub } from './memory/seed-backfill.js';
|
|
34
34
|
import { runStopHooks } from './stop-hooks.js';
|
|
@@ -261,7 +261,7 @@ export function buildResidentEntries(args) {
|
|
|
261
261
|
// session-qualified. The legacy bare `vp/<id>` scope was a structural
|
|
262
262
|
// bug — `summaries.vp` is actually loaded from `group/<sessionId>/vp/<id>/summary.md`
|
|
263
263
|
// (see #loadLayerASummaries, kind:'group-vp'), so labelling it `vp/<id>`
|
|
264
|
-
// in the Resident layer (a) collides with the ACL regex in store
|
|
264
|
+
// in the Resident layer (a) collides with the ACL regex in store
|
|
265
265
|
// (which only recognises `<root>/<sid>/vp/...`) and (b) makes the same
|
|
266
266
|
// VP persona leak across DIFFERENT sessions whenever the AMS rehydrates
|
|
267
267
|
// by id rather than by full scope path. The session-qualified form
|
package/yeaft/history-compact.js
CHANGED
|
@@ -150,7 +150,7 @@ export const DEFAULT_MESSAGE_TOKEN_BUDGET = 32768;
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* Estimate the token weight of a single message including role overhead
|
|
153
|
-
* and any tool-call structure. Mirrors `dream
|
|
153
|
+
* and any tool-call structure. Mirrors `dream/segment.js` approach: a
|
|
154
154
|
* couple of tokens per message for role/wrapping plus the body.
|
|
155
155
|
*
|
|
156
156
|
* @param {{role:string, content?:string, toolCalls?:Array, toolCallId?:string}} m
|
package/yeaft/init.js
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, writeFileSync, accessSync, constants } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
|
-
// NOTE:
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
11
|
+
// NOTE: migrateSessions runs at the end of initYeaftDir(). It collapses
|
|
12
|
+
// legacy groups/ + chats/ + memory/{group,chat}/ into the unified sessions/
|
|
13
|
+
// layout AND rewrites pre-rename per-message frontmatter (groupId → sessionId).
|
|
14
|
+
// Idempotent via the `.yeaft-migration.done` sentinel file.
|
|
15
|
+
import { migrateSessions } from './migrate/sessions.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Check if an error is a permission error (EACCES or EPERM).
|
|
@@ -215,27 +216,20 @@ export function initYeaftDir(dir) {
|
|
|
215
216
|
created.push(mcpExamplePath);
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
// NOTE: sessions
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
.
|
|
231
|
-
|
|
232
|
-
console.log(`[yeaft] session migration complete (${res.moved} dirs moved${res.warnings?.length ? `, ${res.warnings.length} warnings` : ''})`);
|
|
233
|
-
if (res.warnings?.length) for (const w of res.warnings) console.warn(`[yeaft] migration: ${w}`);
|
|
234
|
-
}
|
|
235
|
-
})
|
|
236
|
-
.catch((err) => {
|
|
237
|
-
console.warn(`[yeaft] session migration failed (continuing): ${err?.message || err}`);
|
|
238
|
-
});
|
|
219
|
+
// NOTE: sessions migration runs synchronously here. It MUST complete before
|
|
220
|
+
// any LLM request fires, because step 7 (per-message frontmatter rewrite)
|
|
221
|
+
// is what lets the persist.js parser drop the legacy `groupId:` row alias.
|
|
222
|
+
// Idempotent (sentinel file) so re-running on a fully-migrated dir is a
|
|
223
|
+
// no-op; on a partial-crash dir, each step is independently resumable.
|
|
224
|
+
try {
|
|
225
|
+
const res = migrateSessions(root);
|
|
226
|
+
if (res && res.migrated) {
|
|
227
|
+
console.log(`[yeaft] session migration complete (${res.moved} dirs moved, ${res.frontmatterRewrites} messages rewritten${res.warnings?.length ? `, ${res.warnings.length} warnings` : ''})`);
|
|
228
|
+
if (res.warnings?.length) for (const w of res.warnings) console.warn(`[yeaft] migration: ${w}`);
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.warn(`[yeaft] session migration failed (continuing): ${err?.message || err}`);
|
|
232
|
+
}
|
|
239
233
|
|
|
240
234
|
return { dir: root, created, writable, warnings };
|
|
241
235
|
}
|
package/yeaft/memory/adjust.js
CHANGED
package/yeaft/memory/ams.js
CHANGED
package/yeaft/memory/preflow.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* of truth for own-VP identity; surfacing the stub in Section 6 would
|
|
19
19
|
* dup the same `# Name / Role` text. New seed paths (vp-crud.js,
|
|
20
20
|
* group-crud.js, seed-default.js) use `seedSummaryIfMissingSync` from
|
|
21
|
-
* store
|
|
21
|
+
* store.js to write directly to the correct scope dir.
|
|
22
22
|
*
|
|
23
23
|
* - `archiveLegacyScopes(root)`: one-shot migration that moves the truly
|
|
24
24
|
* dead top-level `vp/`, `feature/`, `topic/` dirs to `.legacy/`. Per
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* memory/store
|
|
2
|
+
* memory/store.js — per-scope memory.md + summary.md (Layer-A storage).
|
|
3
3
|
*
|
|
4
4
|
* One pair of files per scope. No shards, no entries/, no index.md, no
|
|
5
5
|
* index.json. The five scope kinds — user, vp, group, feature, topic — share
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* What this module deliberately does NOT do:
|
|
35
35
|
* - No frontmatter parsing. memory.md and summary.md are pure markdown;
|
|
36
36
|
* the dream-state metadata block lives at the file's tail and is read
|
|
37
|
-
* by `dream
|
|
37
|
+
* by `dream/state.js`, not here.
|
|
38
38
|
* - No LLM calls, no extraction, no summarisation. Pure I/O.
|
|
39
39
|
*
|
|
40
40
|
* Reference: agent/yeaft/memory/DESIGN-H2-AMS.md.
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate/sessions.js — One-shot migration that consolidates legacy on-disk
|
|
3
|
+
* layouts into the current `sessions/`-based shape.
|
|
4
|
+
*
|
|
5
|
+
* Idempotent. Marks completion with `<yeaftDir>/.yeaft-migration.done`.
|
|
6
|
+
* Synchronous: callers (notably initYeaftDir) rely on the migration
|
|
7
|
+
* completing before the engine reads any conversation rows.
|
|
8
|
+
*
|
|
9
|
+
* Steps:
|
|
10
|
+
* 0. Snapshot conversation directories into
|
|
11
|
+
* `<yeaftDir>/.legacy-backup-<YYYYMMDD>/` so the in-place rewrites
|
|
12
|
+
* below have a recovery point. Skipped if today's backup exists.
|
|
13
|
+
* 1. ~/.yeaft/groups/<g>/ → ~/.yeaft/sessions/<g>/
|
|
14
|
+
* group.json → meta.json with shape:
|
|
15
|
+
* { id, vpIds: roster, displayName: name, workDir, createdAt,
|
|
16
|
+
* lastTurnAt: null, archivedAt? }
|
|
17
|
+
* 2. ~/.yeaft/chats/<c>/ → ~/.yeaft/sessions/<c>/
|
|
18
|
+
* chat.json → meta.json with vpIds: [vpId]
|
|
19
|
+
* 3. ~/.yeaft/memory/group/<g>/ → ~/.yeaft/memory/session/<g>/
|
|
20
|
+
* 4. ~/.yeaft/memory/chat/<c>/ → ~/.yeaft/memory/session/<c>/
|
|
21
|
+
* Rewrites front-matter `scope:` fields from group/<id> / chat/<id>
|
|
22
|
+
* → session/<id>.
|
|
23
|
+
* 5. ~/.yeaft/memory/groups/<g>/ams.json → ~/.yeaft/memory/sessions/<g>/ams.json
|
|
24
|
+
* 6. SQLite FTS index `memory_segments.scope` rows: rewrite group/* / chat/*
|
|
25
|
+
* → session/*.
|
|
26
|
+
* 7. Per-message frontmatter: rewrite `groupId: X` → `sessionId: X` in every
|
|
27
|
+
* `*.md` under `conversation/messages|cold/` and
|
|
28
|
+
* `sessions/<id>/conversation/messages|cold/`. This is what closes the
|
|
29
|
+
* loop on PR #881's rename — without it, the parser would have to carry
|
|
30
|
+
* a permanent `groupId` alias.
|
|
31
|
+
*
|
|
32
|
+
* Collision policy: if `sessions/<x>` already exists, log + skip (we assume
|
|
33
|
+
* a prior partial run already moved it). If both groups/<x> and chats/<x>
|
|
34
|
+
* exist with the same id, bail loudly — that shouldn't happen because
|
|
35
|
+
* group ids are `grp_*` and chat ids are `chat_*`.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
cpSync,
|
|
40
|
+
existsSync,
|
|
41
|
+
mkdirSync,
|
|
42
|
+
readdirSync,
|
|
43
|
+
readFileSync,
|
|
44
|
+
renameSync,
|
|
45
|
+
statSync,
|
|
46
|
+
unlinkSync,
|
|
47
|
+
writeFileSync,
|
|
48
|
+
} from 'fs';
|
|
49
|
+
import { join } from 'path';
|
|
50
|
+
import { openSegmentIndex } from '../memory/index-db.js';
|
|
51
|
+
|
|
52
|
+
const SENTINEL = '.yeaft-migration.done';
|
|
53
|
+
const SENTINEL_VERSION = 2;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Run the sessions migration. No-op when sentinel exists.
|
|
57
|
+
*
|
|
58
|
+
* Synchronous: the per-message frontmatter rewrite (step 7) MUST finish
|
|
59
|
+
* before any conversation row is loaded, otherwise the parser would drop
|
|
60
|
+
* legacy `groupId:` rows. Keeping this synchronous lets initYeaftDir stay
|
|
61
|
+
* synchronous too — its 5+ callers all expect that.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} yeaftDir
|
|
64
|
+
* @returns {{ migrated: boolean, moved: number, frontmatterRewrites: number, warnings: string[] }}
|
|
65
|
+
*/
|
|
66
|
+
export function migrateSessions(yeaftDir) {
|
|
67
|
+
const warnings = [];
|
|
68
|
+
if (!yeaftDir || !existsSync(yeaftDir)) {
|
|
69
|
+
return { migrated: false, moved: 0, frontmatterRewrites: 0, warnings: ['yeaftDir missing'] };
|
|
70
|
+
}
|
|
71
|
+
const sentinel = join(yeaftDir, SENTINEL);
|
|
72
|
+
if (existsSync(sentinel)) {
|
|
73
|
+
// Version-aware: ignore a sentinel from an older schema. Future v3
|
|
74
|
+
// migrations can run on v2-completed trees because they read what
|
|
75
|
+
// we wrote and decide for themselves whether to re-execute.
|
|
76
|
+
let v = 0;
|
|
77
|
+
try { v = JSON.parse(readFileSync(sentinel, 'utf8'))?.version ?? 0; } catch { /* corrupt sentinel — treat as 0 */ }
|
|
78
|
+
if (v >= SENTINEL_VERSION) {
|
|
79
|
+
return { migrated: false, moved: 0, frontmatterRewrites: 0, warnings: [] };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Step 0 — backup conversation dirs before any in-place rewrite.
|
|
84
|
+
// Only operates on the conversation tree (the only thing step 7 touches
|
|
85
|
+
// in-place); directory renames in steps 1–5 don't need a backup because
|
|
86
|
+
// they're reversible by moving the directory back.
|
|
87
|
+
const backupRoot = join(yeaftDir, `.legacy-backup-${ymdLocal(new Date())}`);
|
|
88
|
+
backupConversationTrees(yeaftDir, backupRoot, warnings);
|
|
89
|
+
|
|
90
|
+
const sessionsRoot = join(yeaftDir, 'sessions');
|
|
91
|
+
const groupsRoot = join(yeaftDir, 'groups');
|
|
92
|
+
const chatsRoot = join(yeaftDir, 'chats');
|
|
93
|
+
const memoryRoot = join(yeaftDir, 'memory');
|
|
94
|
+
const memSessionRoot = join(memoryRoot, 'session');
|
|
95
|
+
const memSessionsAmsRoot = join(memoryRoot, 'sessions');
|
|
96
|
+
|
|
97
|
+
if (!existsSync(sessionsRoot)) mkdirSync(sessionsRoot, { recursive: true });
|
|
98
|
+
if (!existsSync(memSessionRoot)) mkdirSync(memSessionRoot, { recursive: true });
|
|
99
|
+
if (!existsSync(memSessionsAmsRoot)) mkdirSync(memSessionsAmsRoot, { recursive: true });
|
|
100
|
+
|
|
101
|
+
// ID collision check
|
|
102
|
+
const groupIds = listDirs(groupsRoot);
|
|
103
|
+
const chatIds = listDirs(chatsRoot);
|
|
104
|
+
const overlap = groupIds.filter((id) => chatIds.includes(id));
|
|
105
|
+
if (overlap.length > 0) {
|
|
106
|
+
throw new Error(`sessions migration: id collision between groups/ and chats/: ${overlap.join(',')}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let moved = 0;
|
|
110
|
+
|
|
111
|
+
// Reconcile any half-migrated sessions/<id>/ from a prior crash:
|
|
112
|
+
// a leftover group.json/chat.json with no meta.json means the rename
|
|
113
|
+
// succeeded but the rewrite didn't. Repair before moving on so the
|
|
114
|
+
// session is usable post-migration.
|
|
115
|
+
for (const id of listDirs(sessionsRoot)) {
|
|
116
|
+
const dst = join(sessionsRoot, id);
|
|
117
|
+
if (existsSync(join(dst, 'meta.json'))) continue;
|
|
118
|
+
if (existsSync(join(dst, 'group.json'))) {
|
|
119
|
+
rewriteGroupMetaToSessionMeta(dst, warnings);
|
|
120
|
+
warnings.push(`reconciled partial migration at sessions/${id}`);
|
|
121
|
+
} else if (existsSync(join(dst, 'chat.json'))) {
|
|
122
|
+
rewriteChatMetaToSessionMeta(dst, warnings);
|
|
123
|
+
warnings.push(`reconciled partial migration at sessions/${id}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 1. groups/<g>/ → sessions/<g>/
|
|
128
|
+
for (const id of groupIds) {
|
|
129
|
+
const src = join(groupsRoot, id);
|
|
130
|
+
const dst = join(sessionsRoot, id);
|
|
131
|
+
if (existsSync(dst)) {
|
|
132
|
+
// Partial-run reconcile: if meta.json wasn't written yet, retry the
|
|
133
|
+
// rewrite from the leftover group.json at the destination. Avoids
|
|
134
|
+
// permanently broken sessions when the prior run crashed between
|
|
135
|
+
// renameSync and rewriteGroupMetaToSessionMeta.
|
|
136
|
+
if (!existsSync(join(dst, 'meta.json')) && existsSync(join(dst, 'group.json'))) {
|
|
137
|
+
rewriteGroupMetaToSessionMeta(dst, warnings);
|
|
138
|
+
warnings.push(`reconciled partial migration at sessions/${id}`);
|
|
139
|
+
} else {
|
|
140
|
+
warnings.push(`sessions/${id} already exists; skipping groups/${id}`);
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
renameSync(src, dst);
|
|
145
|
+
rewriteGroupMetaToSessionMeta(dst, warnings);
|
|
146
|
+
moved++;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 2. chats/<c>/ → sessions/<c>/
|
|
150
|
+
for (const id of chatIds) {
|
|
151
|
+
const src = join(chatsRoot, id);
|
|
152
|
+
const dst = join(sessionsRoot, id);
|
|
153
|
+
if (existsSync(dst)) {
|
|
154
|
+
if (!existsSync(join(dst, 'meta.json')) && existsSync(join(dst, 'chat.json'))) {
|
|
155
|
+
rewriteChatMetaToSessionMeta(dst, warnings);
|
|
156
|
+
warnings.push(`reconciled partial migration at sessions/${id}`);
|
|
157
|
+
} else {
|
|
158
|
+
warnings.push(`sessions/${id} already exists; skipping chats/${id}`);
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
renameSync(src, dst);
|
|
163
|
+
rewriteChatMetaToSessionMeta(dst, warnings);
|
|
164
|
+
moved++;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 3+4. memory/{group,chat}/<id>/ → memory/session/<id>/
|
|
168
|
+
for (const family of ['group', 'chat']) {
|
|
169
|
+
const root = join(memoryRoot, family);
|
|
170
|
+
if (!existsSync(root)) continue;
|
|
171
|
+
for (const id of listDirs(root)) {
|
|
172
|
+
const src = join(root, id);
|
|
173
|
+
const dst = join(memSessionRoot, id);
|
|
174
|
+
if (existsSync(dst)) {
|
|
175
|
+
warnings.push(`memory/session/${id} already exists; skipping memory/${family}/${id}`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
renameSync(src, dst);
|
|
179
|
+
rewriteSegmentScopes(dst, family, id, warnings);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 5. memory/groups/<g>/ams.json → memory/sessions/<g>/ams.json
|
|
184
|
+
const amsGroupRoot = join(memoryRoot, 'groups');
|
|
185
|
+
if (existsSync(amsGroupRoot)) {
|
|
186
|
+
for (const id of listDirs(amsGroupRoot)) {
|
|
187
|
+
const srcDir = join(amsGroupRoot, id);
|
|
188
|
+
const dstDir = join(memSessionsAmsRoot, id);
|
|
189
|
+
if (existsSync(dstDir)) {
|
|
190
|
+
warnings.push(`memory/sessions/${id} already exists; skipping memory/groups/${id}`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
renameSync(srcDir, dstDir);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const amsChatRoot = join(memoryRoot, 'chats');
|
|
197
|
+
if (existsSync(amsChatRoot)) {
|
|
198
|
+
for (const id of listDirs(amsChatRoot)) {
|
|
199
|
+
const srcDir = join(amsChatRoot, id);
|
|
200
|
+
const dstDir = join(memSessionsAmsRoot, id);
|
|
201
|
+
if (existsSync(dstDir)) {
|
|
202
|
+
warnings.push(`memory/sessions/${id} already exists; skipping memory/chats/${id}`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
renameSync(srcDir, dstDir);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 6. Rewrite SQLite FTS index scope strings via the shared index-db module
|
|
210
|
+
// (which already handles ABI loading). Idempotent: WHERE clause skips
|
|
211
|
+
// already-rewritten rows. Synchronous: better-sqlite3 has no async API.
|
|
212
|
+
rewriteFtsScopes(memoryRoot, warnings);
|
|
213
|
+
|
|
214
|
+
// 7. Per-message frontmatter rewrite. Walks both the legacy flat
|
|
215
|
+
// conversation directory and every per-session conversation directory
|
|
216
|
+
// so that any pre-rename row still on disk gets the new key shape.
|
|
217
|
+
const frontmatterRewrites = rewriteAllMessageFrontmatter(yeaftDir, warnings);
|
|
218
|
+
|
|
219
|
+
// 8. Sentinel — version 2 = this consolidated migration (covers what the
|
|
220
|
+
// old `.session-migration-v1.done` sentinel covered plus the frontmatter
|
|
221
|
+
// rewrite).
|
|
222
|
+
writeFileSync(sentinel, JSON.stringify({
|
|
223
|
+
version: SENTINEL_VERSION,
|
|
224
|
+
migratedAt: new Date().toISOString(),
|
|
225
|
+
moved,
|
|
226
|
+
frontmatterRewrites,
|
|
227
|
+
warnings,
|
|
228
|
+
}, null, 2), 'utf8');
|
|
229
|
+
|
|
230
|
+
return { migrated: true, moved, frontmatterRewrites, warnings };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** YYYYMMDD in local time. */
|
|
234
|
+
function ymdLocal(d) {
|
|
235
|
+
const y = d.getFullYear();
|
|
236
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
237
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
238
|
+
return `${y}${m}${day}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Snapshot the conversation trees so step 7's in-place frontmatter rewrite
|
|
243
|
+
* is reversible. Only the dirs step 7 touches get backed up — directory
|
|
244
|
+
* renames in steps 1–5 are reversible by moving the directory back.
|
|
245
|
+
*
|
|
246
|
+
* Skips per-source-dir if the destination already exists (resume safety).
|
|
247
|
+
*/
|
|
248
|
+
function backupConversationTrees(yeaftDir, backupRoot, warnings) {
|
|
249
|
+
const sources = [
|
|
250
|
+
join(yeaftDir, 'conversation'),
|
|
251
|
+
join(yeaftDir, 'chat'),
|
|
252
|
+
join(yeaftDir, 'sessions'),
|
|
253
|
+
// Step 1/2 move groups/<g>/ and chats/<c>/ into sessions/<x>/, and
|
|
254
|
+
// step 7 then rewrites *.md inside the moved trees. Without backing
|
|
255
|
+
// these up at step 0, a regex bug would silently destroy the only
|
|
256
|
+
// copy of pre-rename conversation messages.
|
|
257
|
+
join(yeaftDir, 'groups'),
|
|
258
|
+
join(yeaftDir, 'chats'),
|
|
259
|
+
];
|
|
260
|
+
for (const src of sources) {
|
|
261
|
+
if (!existsSync(src)) continue;
|
|
262
|
+
const rel = src.slice(yeaftDir.length + 1);
|
|
263
|
+
const dst = join(backupRoot, rel);
|
|
264
|
+
if (existsSync(dst)) continue; // resume / second run today
|
|
265
|
+
try {
|
|
266
|
+
mkdirSync(dst, { recursive: true });
|
|
267
|
+
cpSync(src, dst, { recursive: true });
|
|
268
|
+
} catch (err) {
|
|
269
|
+
warnings.push(`backup ${src} → ${dst} failed: ${err.message}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function rewriteFtsScopes(memoryRoot, warnings) {
|
|
275
|
+
const dbPath = join(memoryRoot, 'index.db');
|
|
276
|
+
if (!existsSync(dbPath)) return;
|
|
277
|
+
// Best-effort: index-db.js is the single ABI-load site, so we route through
|
|
278
|
+
// it. Errors here are non-fatal — the FTS index can be rebuilt at runtime.
|
|
279
|
+
let idx;
|
|
280
|
+
try {
|
|
281
|
+
idx = openSegmentIndex(dbPath);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
warnings.push(`FTS rewrite skipped: ${err.message}`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const db = idx._db;
|
|
288
|
+
db.exec('BEGIN');
|
|
289
|
+
db.exec("UPDATE memory_segments SET scope = REPLACE(scope, 'group/', 'session/') WHERE scope LIKE 'group/%'");
|
|
290
|
+
db.exec("UPDATE memory_segments SET scope = REPLACE(scope, 'chat/', 'session/') WHERE scope LIKE 'chat/%'");
|
|
291
|
+
db.exec('COMMIT');
|
|
292
|
+
} catch (err) {
|
|
293
|
+
try { idx._db.exec('ROLLBACK'); } catch { /* ignore */ }
|
|
294
|
+
warnings.push(`FTS scope rewrite failed: ${err.message}`);
|
|
295
|
+
} finally {
|
|
296
|
+
try { idx.close(); } catch { /* ignore */ }
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Rewrite `groupId: X` → `sessionId: X` in every message frontmatter under
|
|
302
|
+
* the known conversation directories. Returns the number of files mutated.
|
|
303
|
+
*
|
|
304
|
+
* Idempotent: a second run finds nothing to rewrite and returns 0.
|
|
305
|
+
*/
|
|
306
|
+
function rewriteAllMessageFrontmatter(yeaftDir, warnings) {
|
|
307
|
+
let count = 0;
|
|
308
|
+
const dirs = [];
|
|
309
|
+
// Legacy flat layout.
|
|
310
|
+
for (const sub of ['messages', 'cold']) {
|
|
311
|
+
const d = join(yeaftDir, 'conversation', sub);
|
|
312
|
+
if (existsSync(d)) dirs.push(d);
|
|
313
|
+
}
|
|
314
|
+
// Defensive: chat mode dir (empty in production today, but cheap to scan).
|
|
315
|
+
for (const sub of ['messages', 'cold']) {
|
|
316
|
+
const d = join(yeaftDir, 'chat', sub);
|
|
317
|
+
if (existsSync(d)) dirs.push(d);
|
|
318
|
+
}
|
|
319
|
+
// Per-session layout.
|
|
320
|
+
const sessionsRoot = join(yeaftDir, 'sessions');
|
|
321
|
+
if (existsSync(sessionsRoot)) {
|
|
322
|
+
for (const id of listDirs(sessionsRoot)) {
|
|
323
|
+
for (const sub of ['messages', 'cold']) {
|
|
324
|
+
const d = join(sessionsRoot, id, 'conversation', sub);
|
|
325
|
+
if (existsSync(d)) dirs.push(d);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
for (const dir of dirs) {
|
|
330
|
+
count += rewriteFrontmatterInDir(dir, warnings);
|
|
331
|
+
}
|
|
332
|
+
return count;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Walk a single directory and rewrite any `.md` file whose YAML frontmatter
|
|
337
|
+
* carries `groupId:` but not `sessionId:`. If both keys are already present
|
|
338
|
+
* the redundant `groupId:` line is dropped. Atomic via tmp-then-rename.
|
|
339
|
+
*
|
|
340
|
+
* Returns the number of files mutated.
|
|
341
|
+
*/
|
|
342
|
+
function rewriteFrontmatterInDir(dir, warnings) {
|
|
343
|
+
let mutated = 0;
|
|
344
|
+
let entries;
|
|
345
|
+
try { entries = readdirSync(dir); } catch { return 0; }
|
|
346
|
+
for (const name of entries) {
|
|
347
|
+
if (!name.endsWith('.md')) continue;
|
|
348
|
+
const path = join(dir, name);
|
|
349
|
+
try {
|
|
350
|
+
const raw = readFileSync(path, 'utf8');
|
|
351
|
+
// Frontmatter must open with `---` (LF or CRLF) for us to touch it.
|
|
352
|
+
// Skip the legacy human-readable conv-XXXX.md transcript files (no
|
|
353
|
+
// frontmatter) — they're informational dumps, not parser inputs.
|
|
354
|
+
const openMatch = raw.match(/^---\r?\n/);
|
|
355
|
+
if (!openMatch) continue;
|
|
356
|
+
const openLen = openMatch[0].length;
|
|
357
|
+
// Find the closing `\n---` (allow CRLF before the close too).
|
|
358
|
+
const endIdx = raw.search(/\r?\n---/);
|
|
359
|
+
if (endIdx === -1 || endIdx < openLen) continue;
|
|
360
|
+
const fmBody = raw.slice(openLen, endIdx);
|
|
361
|
+
const after = raw.slice(endIdx);
|
|
362
|
+
// Detect line ending used by the file so we round-trip it.
|
|
363
|
+
const eol = raw.slice(0, openLen).includes('\r\n') ? '\r\n' : '\n';
|
|
364
|
+
const lines = fmBody.split(/\r?\n/);
|
|
365
|
+
const groupIdLineIdx = lines.findIndex((l) => /^groupId:\s*/.test(l));
|
|
366
|
+
if (groupIdLineIdx === -1) continue;
|
|
367
|
+
const hasSessionId = lines.some((l) => /^sessionId:\s*\S/.test(l));
|
|
368
|
+
const m = lines[groupIdLineIdx].match(/^groupId:\s*(.*?)\s*$/);
|
|
369
|
+
const value = m ? m[1] : '';
|
|
370
|
+
if (!hasSessionId && !value) {
|
|
371
|
+
// Pathological: `groupId:` with no value AND no existing sessionId.
|
|
372
|
+
// Don't manufacture `sessionId: ` — that would silently mint a
|
|
373
|
+
// useless row. Leave the file alone and log it so we can audit.
|
|
374
|
+
warnings.push(`frontmatter rewrite skipped (empty groupId, no sessionId): ${path}`);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
let newLines;
|
|
378
|
+
if (hasSessionId) {
|
|
379
|
+
// Redundant — drop the groupId line.
|
|
380
|
+
newLines = lines.filter((_, i) => i !== groupIdLineIdx);
|
|
381
|
+
} else {
|
|
382
|
+
// Replace in place to preserve frontmatter ordering.
|
|
383
|
+
newLines = lines.slice();
|
|
384
|
+
newLines[groupIdLineIdx] = `sessionId: ${value}`;
|
|
385
|
+
}
|
|
386
|
+
const next = `---${eol}${newLines.join(eol)}${after}`;
|
|
387
|
+
// Collision-safe tmp name: pid + counter. Parallel migration runs
|
|
388
|
+
// shouldn't happen (sentinel gates everything) but the atomic-rename
|
|
389
|
+
// contract is cheap insurance.
|
|
390
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
391
|
+
writeFileSync(tmp, next, 'utf8');
|
|
392
|
+
renameSync(tmp, path);
|
|
393
|
+
mutated++;
|
|
394
|
+
} catch (err) {
|
|
395
|
+
warnings.push(`frontmatter rewrite failed ${path}: ${err.message}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return mutated;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function listDirs(root) {
|
|
402
|
+
if (!root || !existsSync(root)) return [];
|
|
403
|
+
const out = [];
|
|
404
|
+
for (const name of readdirSync(root)) {
|
|
405
|
+
if (name.startsWith('.')) continue;
|
|
406
|
+
try {
|
|
407
|
+
if (statSync(join(root, name)).isDirectory()) out.push(name);
|
|
408
|
+
} catch { /* ignore */ }
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function rewriteGroupMetaToSessionMeta(sessionDir, warnings) {
|
|
414
|
+
const oldPath = join(sessionDir, 'group.json');
|
|
415
|
+
const newPath = join(sessionDir, 'meta.json');
|
|
416
|
+
if (!existsSync(oldPath)) {
|
|
417
|
+
if (!existsSync(newPath)) warnings.push(`no group.json in ${sessionDir}`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
const raw = JSON.parse(readFileSync(oldPath, 'utf8'));
|
|
422
|
+
const meta = {
|
|
423
|
+
id: raw.id,
|
|
424
|
+
displayName: raw.name || raw.id,
|
|
425
|
+
vpIds: Array.isArray(raw.roster) && raw.roster.length > 0 ? raw.roster.slice() : ['omni'],
|
|
426
|
+
// Preserve defaultVpId so Phase 2 coordinator can resolve "which VP
|
|
427
|
+
// answers when no @-mention". Easy to keep now, hard to backfill later.
|
|
428
|
+
...(raw.defaultVpId ? { defaultVpId: raw.defaultVpId } : {}),
|
|
429
|
+
workDir: typeof raw.workDir === 'string' ? raw.workDir : '',
|
|
430
|
+
createdAt: raw.createdAt || new Date().toISOString(),
|
|
431
|
+
lastTurnAt: null,
|
|
432
|
+
};
|
|
433
|
+
writeFileSync(newPath, JSON.stringify(meta, null, 2), 'utf8');
|
|
434
|
+
try { unlinkSync(oldPath); } catch { /* keep both if cannot delete */ }
|
|
435
|
+
} catch (err) {
|
|
436
|
+
warnings.push(`failed to rewrite ${oldPath}: ${err.message}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function rewriteChatMetaToSessionMeta(sessionDir, warnings) {
|
|
441
|
+
const oldPath = join(sessionDir, 'chat.json');
|
|
442
|
+
const newPath = join(sessionDir, 'meta.json');
|
|
443
|
+
if (!existsSync(oldPath)) {
|
|
444
|
+
if (!existsSync(newPath)) warnings.push(`no chat.json in ${sessionDir}`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const raw = JSON.parse(readFileSync(oldPath, 'utf8'));
|
|
449
|
+
const meta = {
|
|
450
|
+
id: raw.id,
|
|
451
|
+
displayName: raw.displayName || raw.id,
|
|
452
|
+
vpIds: [raw.vpId || 'omni'],
|
|
453
|
+
workDir: typeof raw.workDir === 'string' ? raw.workDir : '',
|
|
454
|
+
createdAt: raw.createdAt || new Date().toISOString(),
|
|
455
|
+
lastTurnAt: raw.lastTurnAt || null,
|
|
456
|
+
};
|
|
457
|
+
writeFileSync(newPath, JSON.stringify(meta, null, 2), 'utf8');
|
|
458
|
+
try { unlinkSync(oldPath); } catch { /* */ }
|
|
459
|
+
} catch (err) {
|
|
460
|
+
warnings.push(`failed to rewrite ${oldPath}: ${err.message}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function rewriteSegmentScopes(sessionMemoryDir, oldFamily, id, warnings) {
|
|
465
|
+
const segDir = join(sessionMemoryDir, 'segments');
|
|
466
|
+
if (!existsSync(segDir)) return;
|
|
467
|
+
let entries;
|
|
468
|
+
try { entries = readdirSync(segDir); } catch { return; }
|
|
469
|
+
const re = new RegExp(`^(\\s*scope:\\s*)${oldFamily}/${escapeRe(id)}\\b`, 'gm');
|
|
470
|
+
for (const name of entries) {
|
|
471
|
+
if (!name.endsWith('.md')) continue;
|
|
472
|
+
const path = join(segDir, name);
|
|
473
|
+
try {
|
|
474
|
+
const src = readFileSync(path, 'utf8');
|
|
475
|
+
const next = src.replace(re, `$1session/${id}`);
|
|
476
|
+
if (next !== src) writeFileSync(path, next, 'utf8');
|
|
477
|
+
} catch (err) {
|
|
478
|
+
warnings.push(`segment rewrite failed ${path}: ${err.message}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function escapeRe(s) {
|
|
484
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
485
|
+
}
|
package/yeaft/session.js
CHANGED
|
@@ -47,7 +47,7 @@ import { ensureDefaultSessionIfEmpty } from './sessions/session-crud.js';
|
|
|
47
47
|
import { seedDefaultVps } from './vp/seed-defaults.js';
|
|
48
48
|
import { topUpDefaultVps } from './vp/seed-topup.js';
|
|
49
49
|
import { archiveLegacyScopes } from './memory/seed-backfill.js';
|
|
50
|
-
import { createV2DreamScheduler, bootInitEmptyGroups, bootCatchUpStaleDream } from './dream
|
|
50
|
+
import { createV2DreamScheduler, bootInitEmptyGroups, bootCatchUpStaleDream } from './dream/session-wiring.js';
|
|
51
51
|
import { openSegmentIndex } from './memory/index-db.js';
|
|
52
52
|
import { syncAll as syncSegmentIndex } from './memory/segment-sync.js';
|
|
53
53
|
import { openAmsRegistry } from './memory/ams-registry.js';
|
|
@@ -204,7 +204,7 @@ export async function loadSession(options = {}) {
|
|
|
204
204
|
|
|
205
205
|
// ─── 2.2 R6 → v2 auto-migration retired ───────────────
|
|
206
206
|
// The R6 shard layout is gone — memory writes go through
|
|
207
|
-
// dream
|
|
207
|
+
// dream directly. Existing users have already migrated
|
|
208
208
|
// (state file in ~/.yeaft/.memory-v2-migration.json).
|
|
209
209
|
|
|
210
210
|
// ─── 2a. Permission pre-check ─────────────────────────
|
|
@@ -362,7 +362,7 @@ export async function loadSession(options = {}) {
|
|
|
362
362
|
// — kind:'group-vp'). The backfill therefore generated orphan files
|
|
363
363
|
// on every boot. See `memory/seed-backfill.js` for the historical
|
|
364
364
|
// context. Real seeding happens at create time via
|
|
365
|
-
// `seedSummaryIfMissingSync` from `store
|
|
365
|
+
// `seedSummaryIfMissingSync` from `store.js`, called by vp-crud /
|
|
366
366
|
// group-crud / seed-default — those write to the correct scope dirs.
|
|
367
367
|
}
|
|
368
368
|
|
|
@@ -468,7 +468,7 @@ export async function loadSession(options = {}) {
|
|
|
468
468
|
|
|
469
469
|
// ─── 9a. Create dream scheduler ────────────
|
|
470
470
|
// The legacy R6 dream-scheduler was retired alongside recall-r6;
|
|
471
|
-
// dream
|
|
471
|
+
// dream is the only active path (the `config.memoryV2` opt-out
|
|
472
472
|
// flag was retired in task-710 — wiring is unconditional).
|
|
473
473
|
// partialSession lets the v2 scheduler dereference adapter/config/
|
|
474
474
|
// engine/trace lazily — safe because callers attach more fields
|
|
@@ -16,7 +16,7 @@ import { existsSync, mkdirSync } from 'fs';
|
|
|
16
16
|
import { join } from 'path';
|
|
17
17
|
import { homedir } from 'os';
|
|
18
18
|
import { openSession, createSession, loadSessionMeta } from './session-store.js';
|
|
19
|
-
import { seedSummaryIfMissingSync } from '../memory/store
|
|
19
|
+
import { seedSummaryIfMissingSync } from '../memory/store.js';
|
|
20
20
|
|
|
21
21
|
export const DEFAULT_SESSION_ID = 'grp_default';
|
|
22
22
|
|
|
@@ -52,7 +52,7 @@ import { addVp as rosterAdd, removeVp as rosterRemove, setDefaultVp } from './ro
|
|
|
52
52
|
import { seedDefaultSession, DEFAULT_SESSION_ID } from './seed-default.js';
|
|
53
53
|
import { nextSessionId, validateVpId, isReservedVpId } from './ids.js';
|
|
54
54
|
import { scanVpLibrary, DEFAULT_VP_LIB_DIR } from '../vp/vp-store.js';
|
|
55
|
-
import { seedSummaryIfMissingSync, removeScopeDirSync } from '../memory/store
|
|
55
|
+
import { seedSummaryIfMissingSync, removeScopeDirSync } from '../memory/store.js';
|
|
56
56
|
import { ensureSessionConfigFile, saveSessionConfig, loadSessionConfig } from './session-config.js';
|
|
57
57
|
|
|
58
58
|
/**
|
package/yeaft/stop-hooks.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* entries-store extraction was retired in the H2-AMS rip.
|
|
10
10
|
*
|
|
11
11
|
* Dream V2 owns all background memory maintenance (scope summaries +
|
|
12
|
-
* memory writes via dream
|
|
12
|
+
* memory writes via dream/session-wiring.js → createV2DreamScheduler).
|
|
13
13
|
*
|
|
14
14
|
* Reference: yeaft-yeaft-core-systems.md §4.4
|
|
15
15
|
*/
|
package/yeaft/vp/vp-crud.js
CHANGED
|
@@ -29,7 +29,7 @@ import { STOCK_VP_IDS } from './stock-ids.js';
|
|
|
29
29
|
/**
|
|
30
30
|
* Default memory root used when callers don't pass `options.memoryRoot`.
|
|
31
31
|
* Memory lives at `<root>/vp/<id>/{summary.md,memory.md,segments/…}` —
|
|
32
|
-
* see `store
|
|
32
|
+
* see `store.scopeDir`. Production sites should thread the configured
|
|
33
33
|
* `<yeaftDir>/memory` through `options.memoryRoot` so a non-default yeaft
|
|
34
34
|
* directory (e.g. tests, sandboxed CI) doesn't write under `~/.yeaft/`.
|
|
35
35
|
*/
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -3519,7 +3519,7 @@ export async function handleYeaftDreamTrigger(msg = {}) {
|
|
|
3519
3519
|
// otherwise auto-run events can be persisted under the clicked group.
|
|
3520
3520
|
// The scheduler also short-circuits the underlying run for same-group,
|
|
3521
3521
|
// and a different group's filter would have been silently dropped
|
|
3522
|
-
// anyway (see dream
|
|
3522
|
+
// anyway (see dream/schedule.js inflight reuse), so the user-facing
|
|
3523
3523
|
// semantics are unchanged ("you already asked").
|
|
3524
3524
|
if (sessionId && (inflightScopedDreamGroups.size > 0 || session.dreamScheduler.isRunning)) {
|
|
3525
3525
|
const skippedResult = {
|
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* migrate/sessions-v1.js — One-shot migration: collapse groups/ + chats/ → sessions/
|
|
3
|
-
*
|
|
4
|
-
* Idempotent. Marks completion with a sentinel file
|
|
5
|
-
* `<yeaftDir>/.session-migration-v1.done`.
|
|
6
|
-
*
|
|
7
|
-
* Migrates:
|
|
8
|
-
* 1. ~/.yeaft/groups/<g>/ → ~/.yeaft/sessions/<g>/
|
|
9
|
-
* group.json → meta.json with shape:
|
|
10
|
-
* { id, vpIds: roster, displayName: name, workDir, createdAt,
|
|
11
|
-
* lastTurnAt: null, archivedAt? }
|
|
12
|
-
* 2. ~/.yeaft/chats/<c>/ → ~/.yeaft/sessions/<c>/
|
|
13
|
-
* chat.json → meta.json with vpIds: [vpId]
|
|
14
|
-
* 3. ~/.yeaft/memory/group/<g>/ → ~/.yeaft/memory/session/<g>/
|
|
15
|
-
* 4. ~/.yeaft/memory/chat/<c>/ → ~/.yeaft/memory/session/<c>/
|
|
16
|
-
* Rewrites front-matter `scope:` fields from group/<id> / chat/<id>
|
|
17
|
-
* → session/<id>.
|
|
18
|
-
* 5. ~/.yeaft/memory/groups/<g>/ams.json → ~/.yeaft/memory/sessions/<g>/ams.json
|
|
19
|
-
*
|
|
20
|
-
* Collision policy: if `sessions/<x>` already exists, log + skip (we assume
|
|
21
|
-
* a prior partial run already moved it). If both groups/<x> and chats/<x>
|
|
22
|
-
* exist with the same id, bail loudly — that shouldn't happen because
|
|
23
|
-
* group ids are `grp_*` and chat ids are `chat_*`.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
existsSync,
|
|
28
|
-
mkdirSync,
|
|
29
|
-
readdirSync,
|
|
30
|
-
readFileSync,
|
|
31
|
-
renameSync,
|
|
32
|
-
statSync,
|
|
33
|
-
unlinkSync,
|
|
34
|
-
writeFileSync,
|
|
35
|
-
} from 'fs';
|
|
36
|
-
import { join } from 'path';
|
|
37
|
-
|
|
38
|
-
const SENTINEL = '.session-migration-v1.done';
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Run the v1 sessions migration. No-op when sentinel exists.
|
|
42
|
-
*
|
|
43
|
-
* @param {string} yeaftDir
|
|
44
|
-
* @returns {{ migrated: boolean, moved: number, warnings: string[] }}
|
|
45
|
-
*/
|
|
46
|
-
export async function migrateSessionsV1(yeaftDir) {
|
|
47
|
-
const warnings = [];
|
|
48
|
-
if (!yeaftDir || !existsSync(yeaftDir)) {
|
|
49
|
-
return { migrated: false, moved: 0, warnings: ['yeaftDir missing'] };
|
|
50
|
-
}
|
|
51
|
-
const sentinel = join(yeaftDir, SENTINEL);
|
|
52
|
-
if (existsSync(sentinel)) {
|
|
53
|
-
return { migrated: false, moved: 0, warnings: [] };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const sessionsRoot = join(yeaftDir, 'sessions');
|
|
57
|
-
const groupsRoot = join(yeaftDir, 'groups');
|
|
58
|
-
const chatsRoot = join(yeaftDir, 'chats');
|
|
59
|
-
const memoryRoot = join(yeaftDir, 'memory');
|
|
60
|
-
const memSessionRoot = join(memoryRoot, 'session');
|
|
61
|
-
const memSessionsAmsRoot = join(memoryRoot, 'sessions');
|
|
62
|
-
|
|
63
|
-
if (!existsSync(sessionsRoot)) mkdirSync(sessionsRoot, { recursive: true });
|
|
64
|
-
if (!existsSync(memSessionRoot)) mkdirSync(memSessionRoot, { recursive: true });
|
|
65
|
-
if (!existsSync(memSessionsAmsRoot)) mkdirSync(memSessionsAmsRoot, { recursive: true });
|
|
66
|
-
|
|
67
|
-
// ID collision check
|
|
68
|
-
const groupIds = listDirs(groupsRoot);
|
|
69
|
-
const chatIds = listDirs(chatsRoot);
|
|
70
|
-
const overlap = groupIds.filter((id) => chatIds.includes(id));
|
|
71
|
-
if (overlap.length > 0) {
|
|
72
|
-
throw new Error(`sessions migration: id collision between groups/ and chats/: ${overlap.join(',')}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let moved = 0;
|
|
76
|
-
|
|
77
|
-
// 0. Reconcile any half-migrated sessions/<id>/ from a prior crash:
|
|
78
|
-
// a leftover group.json/chat.json with no meta.json means the rename
|
|
79
|
-
// succeeded but the rewrite didn't. Repair before moving on so the
|
|
80
|
-
// session is usable post-migration.
|
|
81
|
-
for (const id of listDirs(sessionsRoot)) {
|
|
82
|
-
const dst = join(sessionsRoot, id);
|
|
83
|
-
if (existsSync(join(dst, 'meta.json'))) continue;
|
|
84
|
-
if (existsSync(join(dst, 'group.json'))) {
|
|
85
|
-
rewriteGroupMetaToSessionMeta(dst, warnings);
|
|
86
|
-
warnings.push(`reconciled partial migration at sessions/${id}`);
|
|
87
|
-
} else if (existsSync(join(dst, 'chat.json'))) {
|
|
88
|
-
rewriteChatMetaToSessionMeta(dst, warnings);
|
|
89
|
-
warnings.push(`reconciled partial migration at sessions/${id}`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// 1. groups/<g>/ → sessions/<g>/
|
|
94
|
-
for (const id of groupIds) {
|
|
95
|
-
const src = join(groupsRoot, id);
|
|
96
|
-
const dst = join(sessionsRoot, id);
|
|
97
|
-
if (existsSync(dst)) {
|
|
98
|
-
// Partial-run reconcile: if meta.json wasn't written yet, retry the
|
|
99
|
-
// rewrite from the leftover group.json at the destination. Avoids
|
|
100
|
-
// permanently broken sessions when the prior run crashed between
|
|
101
|
-
// renameSync and rewriteGroupMetaToSessionMeta.
|
|
102
|
-
if (!existsSync(join(dst, 'meta.json')) && existsSync(join(dst, 'group.json'))) {
|
|
103
|
-
rewriteGroupMetaToSessionMeta(dst, warnings);
|
|
104
|
-
warnings.push(`reconciled partial migration at sessions/${id}`);
|
|
105
|
-
} else {
|
|
106
|
-
warnings.push(`sessions/${id} already exists; skipping groups/${id}`);
|
|
107
|
-
}
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
renameSync(src, dst);
|
|
111
|
-
rewriteGroupMetaToSessionMeta(dst, warnings);
|
|
112
|
-
moved++;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 2. chats/<c>/ → sessions/<c>/
|
|
116
|
-
for (const id of chatIds) {
|
|
117
|
-
const src = join(chatsRoot, id);
|
|
118
|
-
const dst = join(sessionsRoot, id);
|
|
119
|
-
if (existsSync(dst)) {
|
|
120
|
-
if (!existsSync(join(dst, 'meta.json')) && existsSync(join(dst, 'chat.json'))) {
|
|
121
|
-
rewriteChatMetaToSessionMeta(dst, warnings);
|
|
122
|
-
warnings.push(`reconciled partial migration at sessions/${id}`);
|
|
123
|
-
} else {
|
|
124
|
-
warnings.push(`sessions/${id} already exists; skipping chats/${id}`);
|
|
125
|
-
}
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
renameSync(src, dst);
|
|
129
|
-
rewriteChatMetaToSessionMeta(dst, warnings);
|
|
130
|
-
moved++;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 3+4. memory/{group,chat}/<id>/ → memory/session/<id>/
|
|
134
|
-
for (const family of ['group', 'chat']) {
|
|
135
|
-
const root = join(memoryRoot, family);
|
|
136
|
-
if (!existsSync(root)) continue;
|
|
137
|
-
for (const id of listDirs(root)) {
|
|
138
|
-
const src = join(root, id);
|
|
139
|
-
const dst = join(memSessionRoot, id);
|
|
140
|
-
if (existsSync(dst)) {
|
|
141
|
-
warnings.push(`memory/session/${id} already exists; skipping memory/${family}/${id}`);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
renameSync(src, dst);
|
|
145
|
-
rewriteSegmentScopes(dst, family, id, warnings);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 5. memory/groups/<g>/ams.json → memory/sessions/<g>/ams.json
|
|
150
|
-
const amsGroupRoot = join(memoryRoot, 'groups');
|
|
151
|
-
if (existsSync(amsGroupRoot)) {
|
|
152
|
-
for (const id of listDirs(amsGroupRoot)) {
|
|
153
|
-
const srcDir = join(amsGroupRoot, id);
|
|
154
|
-
const dstDir = join(memSessionsAmsRoot, id);
|
|
155
|
-
if (existsSync(dstDir)) {
|
|
156
|
-
warnings.push(`memory/sessions/${id} already exists; skipping memory/groups/${id}`);
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
renameSync(srcDir, dstDir);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const amsChatRoot = join(memoryRoot, 'chats');
|
|
163
|
-
if (existsSync(amsChatRoot)) {
|
|
164
|
-
for (const id of listDirs(amsChatRoot)) {
|
|
165
|
-
const srcDir = join(amsChatRoot, id);
|
|
166
|
-
const dstDir = join(memSessionsAmsRoot, id);
|
|
167
|
-
if (existsSync(dstDir)) {
|
|
168
|
-
warnings.push(`memory/sessions/${id} already exists; skipping memory/chats/${id}`);
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
renameSync(srcDir, dstDir);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// 6. Rewrite SQLite FTS index scope strings via the shared index-db
|
|
176
|
-
// module (which already handles ABI loading). Idempotent: WHERE clause
|
|
177
|
-
// skips already-rewritten rows.
|
|
178
|
-
try {
|
|
179
|
-
const dbPath = join(memoryRoot, 'index.db');
|
|
180
|
-
if (existsSync(dbPath)) {
|
|
181
|
-
const { openSegmentIndex } = await import('../memory/index-db.js');
|
|
182
|
-
const idx = openSegmentIndex(dbPath);
|
|
183
|
-
try {
|
|
184
|
-
const db = idx._db;
|
|
185
|
-
db.exec("BEGIN");
|
|
186
|
-
db.exec("UPDATE memory_segments SET scope = REPLACE(scope, 'group/', 'session/') WHERE scope LIKE 'group/%'");
|
|
187
|
-
db.exec("UPDATE memory_segments SET scope = REPLACE(scope, 'chat/', 'session/') WHERE scope LIKE 'chat/%'");
|
|
188
|
-
db.exec("COMMIT");
|
|
189
|
-
} catch (err) {
|
|
190
|
-
try { idx._db.exec("ROLLBACK"); } catch { /* ignore */ }
|
|
191
|
-
warnings.push(`FTS scope rewrite failed: ${err.message}`);
|
|
192
|
-
} finally {
|
|
193
|
-
try { idx.close(); } catch { /* ignore */ }
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
} catch (err) {
|
|
197
|
-
warnings.push(`FTS rewrite skipped: ${err.message}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// 7. sentinel
|
|
201
|
-
writeFileSync(sentinel, JSON.stringify({
|
|
202
|
-
version: 1,
|
|
203
|
-
migratedAt: new Date().toISOString(),
|
|
204
|
-
moved,
|
|
205
|
-
warnings,
|
|
206
|
-
}, null, 2), 'utf8');
|
|
207
|
-
|
|
208
|
-
return { migrated: true, moved, warnings };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function listDirs(root) {
|
|
212
|
-
if (!root || !existsSync(root)) return [];
|
|
213
|
-
const out = [];
|
|
214
|
-
for (const name of readdirSync(root)) {
|
|
215
|
-
if (name.startsWith('.')) continue;
|
|
216
|
-
try {
|
|
217
|
-
if (statSync(join(root, name)).isDirectory()) out.push(name);
|
|
218
|
-
} catch { /* ignore */ }
|
|
219
|
-
}
|
|
220
|
-
return out;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function rewriteGroupMetaToSessionMeta(sessionDir, warnings) {
|
|
224
|
-
const oldPath = join(sessionDir, 'group.json');
|
|
225
|
-
const newPath = join(sessionDir, 'meta.json');
|
|
226
|
-
if (!existsSync(oldPath)) {
|
|
227
|
-
if (!existsSync(newPath)) warnings.push(`no group.json in ${sessionDir}`);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
try {
|
|
231
|
-
const raw = JSON.parse(readFileSync(oldPath, 'utf8'));
|
|
232
|
-
const meta = {
|
|
233
|
-
id: raw.id,
|
|
234
|
-
displayName: raw.name || raw.id,
|
|
235
|
-
vpIds: Array.isArray(raw.roster) && raw.roster.length > 0 ? raw.roster.slice() : ['omni'],
|
|
236
|
-
// Preserve defaultVpId so Phase 2 coordinator can resolve "which VP
|
|
237
|
-
// answers when no @-mention". Easy to keep now, hard to backfill later.
|
|
238
|
-
...(raw.defaultVpId ? { defaultVpId: raw.defaultVpId } : {}),
|
|
239
|
-
workDir: typeof raw.workDir === 'string' ? raw.workDir : '',
|
|
240
|
-
createdAt: raw.createdAt || new Date().toISOString(),
|
|
241
|
-
lastTurnAt: null,
|
|
242
|
-
};
|
|
243
|
-
writeFileSync(newPath, JSON.stringify(meta, null, 2), 'utf8');
|
|
244
|
-
try { unlinkSync(oldPath); } catch { /* keep both if cannot delete */ }
|
|
245
|
-
} catch (err) {
|
|
246
|
-
warnings.push(`failed to rewrite ${oldPath}: ${err.message}`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function rewriteChatMetaToSessionMeta(sessionDir, warnings) {
|
|
251
|
-
const oldPath = join(sessionDir, 'chat.json');
|
|
252
|
-
const newPath = join(sessionDir, 'meta.json');
|
|
253
|
-
if (!existsSync(oldPath)) {
|
|
254
|
-
if (!existsSync(newPath)) warnings.push(`no chat.json in ${sessionDir}`);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
try {
|
|
258
|
-
const raw = JSON.parse(readFileSync(oldPath, 'utf8'));
|
|
259
|
-
const meta = {
|
|
260
|
-
id: raw.id,
|
|
261
|
-
displayName: raw.displayName || raw.id,
|
|
262
|
-
vpIds: [raw.vpId || 'omni'],
|
|
263
|
-
workDir: typeof raw.workDir === 'string' ? raw.workDir : '',
|
|
264
|
-
createdAt: raw.createdAt || new Date().toISOString(),
|
|
265
|
-
lastTurnAt: raw.lastTurnAt || null,
|
|
266
|
-
};
|
|
267
|
-
writeFileSync(newPath, JSON.stringify(meta, null, 2), 'utf8');
|
|
268
|
-
try { unlinkSync(oldPath); } catch { /* */ }
|
|
269
|
-
} catch (err) {
|
|
270
|
-
warnings.push(`failed to rewrite ${oldPath}: ${err.message}`);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function rewriteSegmentScopes(sessionMemoryDir, oldFamily, id, warnings) {
|
|
275
|
-
const segDir = join(sessionMemoryDir, 'segments');
|
|
276
|
-
if (!existsSync(segDir)) return;
|
|
277
|
-
let entries;
|
|
278
|
-
try { entries = readdirSync(segDir); } catch { return; }
|
|
279
|
-
const re = new RegExp(`^(\\s*scope:\\s*)${oldFamily}/${escapeRe(id)}\\b`, 'gm');
|
|
280
|
-
for (const name of entries) {
|
|
281
|
-
if (!name.endsWith('.md')) continue;
|
|
282
|
-
const path = join(segDir, name);
|
|
283
|
-
try {
|
|
284
|
-
const src = readFileSync(path, 'utf8');
|
|
285
|
-
const next = src.replace(re, `$1session/${id}`);
|
|
286
|
-
if (next !== src) writeFileSync(path, next, 'utf8');
|
|
287
|
-
} catch (err) {
|
|
288
|
-
warnings.push(`segment rewrite failed ${path}: ${err.message}`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function escapeRe(s) {
|
|
294
|
-
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
295
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|