@yeaft/webchat-agent 0.1.960 → 0.1.961
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/yeaft/conversation/persist.js +46 -40
- package/yeaft/conversation/search.js +23 -16
- package/yeaft/dream/apply.js +15 -2
- package/yeaft/dream/runner.js +2 -0
- package/yeaft/dream/session-wiring.js +113 -25
- package/yeaft/dream/state.js +1 -0
package/package.json
CHANGED
|
@@ -2,15 +2,12 @@
|
|
|
2
2
|
* persist.js — Conversation message persistence
|
|
3
3
|
*
|
|
4
4
|
* Each message is stored as a .md file with YAML frontmatter in
|
|
5
|
-
* ~/.yeaft/chat/messages/ or ~/.yeaft/
|
|
5
|
+
* ~/.yeaft/chat/messages/ or ~/.yeaft/sessions/<sessionId>/conversation/messages/. Design: zero JSON, all Markdown.
|
|
6
6
|
*
|
|
7
|
-
* Vocabulary note: the on-disk layout
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* surface above the disk layer (method names, params, comments) uses
|
|
12
|
-
* "session" vocabulary. See `ConversationStore.#sessionsDir` for the
|
|
13
|
-
* boundary annotation.
|
|
7
|
+
* Vocabulary note: the primary on-disk layout uses `sessions/<id>/`. Older
|
|
8
|
+
* installs may still have transcript files under `groups/<id>/`; those are
|
|
9
|
+
* read as a legacy fallback only. Every API surface above the disk layer
|
|
10
|
+
* uses "session" vocabulary.
|
|
14
11
|
*
|
|
15
12
|
* Message format:
|
|
16
13
|
* ---
|
|
@@ -199,12 +196,12 @@ function serializeMessage(msg) {
|
|
|
199
196
|
// render a small "#source" pill next to each bubble.
|
|
200
197
|
if (msg.sourceThreadId) fm.push(`sourceThreadId: ${msg.sourceThreadId}`);
|
|
201
198
|
// Bug 6: persist sessionId so history replay can stamp messages with the
|
|
202
|
-
//
|
|
203
|
-
// in the default
|
|
199
|
+
// session they originated in. Without this, every replayed message lands
|
|
200
|
+
// in the default session and switching back to the originating session
|
|
204
201
|
// shows an empty pane.
|
|
205
202
|
if (msg.sessionId) fm.push(`sessionId: ${msg.sessionId}`);
|
|
206
203
|
if (msg.chatId) fm.push(`chatId: ${msg.chatId}`);
|
|
207
|
-
//
|
|
204
|
+
// Session attribution: when a VP authors an assistant turn (either
|
|
208
205
|
// its own reply or a route_forward injection from another VP), stamp
|
|
209
206
|
// the speaker so the UI can render the message on the correct VP track.
|
|
210
207
|
// For real user messages this is unset.
|
|
@@ -424,25 +421,22 @@ export function parseMessage(raw) {
|
|
|
424
421
|
* messages/
|
|
425
422
|
* cold/
|
|
426
423
|
* blobs/
|
|
427
|
-
*
|
|
424
|
+
* sessions/<sessionId>/conversation/
|
|
428
425
|
* compact/
|
|
429
426
|
* messages/
|
|
430
427
|
* cold/
|
|
431
428
|
* blobs/
|
|
432
429
|
*
|
|
433
|
-
* Legacy compatibility: ~/.yeaft/conversation is read as an old mixed store
|
|
434
|
-
*
|
|
435
|
-
*
|
|
430
|
+
* Legacy compatibility: ~/.yeaft/conversation is read as an old mixed store,
|
|
431
|
+
* and ~/.yeaft/groups/<sessionId>/conversation is read as an old session
|
|
432
|
+
* transcript store. New writes are split by mode: records with sessionId go to
|
|
433
|
+
* sessions/<sessionId>/conversation/, all others go to chat/.
|
|
436
434
|
*/
|
|
437
435
|
export class ConversationStore {
|
|
438
436
|
#dir; // root dir (e.g. ~/.yeaft)
|
|
439
437
|
#chatDir; // ~/.yeaft/chat
|
|
440
|
-
// ~/.yeaft/
|
|
441
|
-
|
|
442
|
-
// after the session-v1 meta migration (which only collapses the meta
|
|
443
|
-
// files, not the conversation tree). All API surfaces above the disk
|
|
444
|
-
// layer have been renamed to the "session" vocabulary.
|
|
445
|
-
#sessionsDir;
|
|
438
|
+
#sessionsDir; // ~/.yeaft/sessions — primary Session transcript store
|
|
439
|
+
#legacySessionsDir; // ~/.yeaft/groups — read-only legacy Session transcripts
|
|
446
440
|
#legacyConvDir; // ~/.yeaft/conversation (read-only compatibility)
|
|
447
441
|
#convDir; // default thread dir root: ~/.yeaft/chat
|
|
448
442
|
#msgDir; // default hot messages dir: ~/.yeaft/chat/messages
|
|
@@ -464,7 +458,8 @@ export class ConversationStore {
|
|
|
464
458
|
constructor(dir) {
|
|
465
459
|
this.#dir = dir;
|
|
466
460
|
this.#chatDir = join(dir, 'chat');
|
|
467
|
-
this.#sessionsDir = join(dir, '
|
|
461
|
+
this.#sessionsDir = join(dir, 'sessions');
|
|
462
|
+
this.#legacySessionsDir = join(dir, 'groups');
|
|
468
463
|
this.#legacyConvDir = join(dir, 'conversation');
|
|
469
464
|
|
|
470
465
|
this.#convDir = this.#chatDir;
|
|
@@ -479,17 +474,16 @@ export class ConversationStore {
|
|
|
479
474
|
this.#legacyMsgDir = join(this.#legacyConvDir, 'messages');
|
|
480
475
|
this.#legacyColdDir = join(this.#legacyConvDir, 'cold');
|
|
481
476
|
|
|
482
|
-
// Per-(sessionId, vpId) compact summary files live under that
|
|
477
|
+
// Per-(sessionId, vpId) compact summary files live under that session's
|
|
483
478
|
// conversation directory. The legacy ~/.yeaft/conversation/compact directory
|
|
484
479
|
// is read for compatibility.
|
|
485
480
|
this.#legacyCompactScopedDir = join(this.#legacyConvDir, 'compact');
|
|
486
481
|
this.#nextSeq = null;
|
|
487
482
|
this.#nextSeqByThread = new Map();
|
|
488
483
|
|
|
489
|
-
// Ensure new chat and
|
|
490
|
-
// errors). Per-
|
|
491
|
-
// sessionId is known.
|
|
492
|
-
// new versions.
|
|
484
|
+
// Ensure new chat and session-root directories exist (graceful on permission
|
|
485
|
+
// errors). Per-session conversation directories are created lazily once a
|
|
486
|
+
// sessionId is known. Legacy directories are never created by new versions.
|
|
493
487
|
for (const d of [
|
|
494
488
|
this.#chatDir, join(this.#chatDir, 'blobs'), this.#chatMsgDir, this.#chatColdDir,
|
|
495
489
|
this.#sessionsDir,
|
|
@@ -724,7 +718,7 @@ export class ConversationStore {
|
|
|
724
718
|
}
|
|
725
719
|
|
|
726
720
|
/**
|
|
727
|
-
* Check whether ANY per-(
|
|
721
|
+
* Check whether ANY per-(session, vp) compact summary exists for `sessionId`.
|
|
728
722
|
* Used by the history-replay path to decide whether to flag
|
|
729
723
|
* `hasCompactSummary` for the UI without committing to one VP's view.
|
|
730
724
|
*
|
|
@@ -1626,6 +1620,10 @@ export class ConversationStore {
|
|
|
1626
1620
|
return dir;
|
|
1627
1621
|
}
|
|
1628
1622
|
|
|
1623
|
+
#legacySessionConversationDir(sessionId) {
|
|
1624
|
+
return join(this.#legacySessionsDir, this.#safeDirComponent(sessionId), 'conversation');
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1629
1627
|
#ensureConversationDirs(dir) {
|
|
1630
1628
|
for (const d of [dir, join(dir, 'blobs'), join(dir, 'messages'), join(dir, 'cold'), join(dir, 'compact')]) {
|
|
1631
1629
|
if (!existsSync(d)) mkdirSync(d, { recursive: true, mode: 0o755 });
|
|
@@ -1633,26 +1631,34 @@ export class ConversationStore {
|
|
|
1633
1631
|
}
|
|
1634
1632
|
|
|
1635
1633
|
#sessionConversationDirs() {
|
|
1636
|
-
if (!existsSync(this.#sessionsDir)) return [];
|
|
1637
1634
|
const dirs = [];
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1635
|
+
const seen = new Set();
|
|
1636
|
+
for (const root of [this.#sessionsDir, this.#legacySessionsDir]) {
|
|
1637
|
+
if (!existsSync(root)) continue;
|
|
1638
|
+
for (const name of readdirSync(root)) {
|
|
1639
|
+
const sessionDir = join(root, name);
|
|
1640
|
+
try {
|
|
1641
|
+
if (!statSync(sessionDir).isDirectory()) continue;
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
if (isPermissionError(err)) continue;
|
|
1644
|
+
throw err;
|
|
1645
|
+
}
|
|
1646
|
+
const conversationDir = join(sessionDir, 'conversation');
|
|
1647
|
+
if (!existsSync(conversationDir) || seen.has(conversationDir)) continue;
|
|
1648
|
+
seen.add(conversationDir);
|
|
1649
|
+
dirs.push(conversationDir);
|
|
1645
1650
|
}
|
|
1646
|
-
const conversationDir = join(sessionDir, 'conversation');
|
|
1647
|
-
if (existsSync(conversationDir)) dirs.push(conversationDir);
|
|
1648
1651
|
}
|
|
1649
1652
|
return dirs;
|
|
1650
1653
|
}
|
|
1651
1654
|
|
|
1652
1655
|
#sessionMessageDirs(kind, sessionId = null) {
|
|
1653
1656
|
if (sessionId) {
|
|
1654
|
-
const
|
|
1655
|
-
|
|
1657
|
+
const dirs = [
|
|
1658
|
+
join(this.#sessionConversationDir(sessionId), kind),
|
|
1659
|
+
join(this.#legacySessionConversationDir(sessionId), kind),
|
|
1660
|
+
];
|
|
1661
|
+
return dirs.filter(dir => existsSync(dir));
|
|
1656
1662
|
}
|
|
1657
1663
|
return this.#sessionConversationDirs()
|
|
1658
1664
|
.map(dir => join(dir, kind))
|
|
@@ -42,27 +42,34 @@ function compareNewest(a, b) {
|
|
|
42
42
|
return String(b?.time || '').localeCompare(String(a?.time || ''));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function
|
|
46
|
-
const groupsDir = join(dir, 'groups');
|
|
47
|
-
if (!existsSync(groupsDir)) return [];
|
|
48
|
-
|
|
45
|
+
function sessionConversationMessageDirs(dir) {
|
|
49
46
|
const dirs = [];
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
for (const rootName of ['sessions', 'groups']) {
|
|
49
|
+
const root = join(dir, rootName);
|
|
50
|
+
if (!existsSync(root)) continue;
|
|
51
|
+
for (const name of readdirSync(root)) {
|
|
52
|
+
const sessionDir = join(root, name);
|
|
53
|
+
try {
|
|
54
|
+
if (!statSync(sessionDir).isDirectory()) continue;
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
const conversationDir = join(sessionDir, 'conversation');
|
|
60
|
+
for (const kind of ['messages', 'cold']) {
|
|
61
|
+
const messagesDir = join(conversationDir, kind);
|
|
62
|
+
if (seen.has(messagesDir)) continue;
|
|
63
|
+
seen.add(messagesDir);
|
|
64
|
+
dirs.push(messagesDir);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
60
67
|
}
|
|
61
68
|
return dirs;
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
/**
|
|
65
|
-
* Search Yeaft history (chat + per-
|
|
72
|
+
* Search Yeaft history (chat + per-session + legacy conversation) for a keyword.
|
|
66
73
|
*
|
|
67
74
|
* @param {string} dir — Yeaft root directory (e.g. ~/.yeaft)
|
|
68
75
|
* @param {string} keyword — search term
|
|
@@ -75,8 +82,8 @@ export function searchMessages(dir, keyword, limit = 20) {
|
|
|
75
82
|
const dirs = [
|
|
76
83
|
join(dir, 'chat', 'messages'),
|
|
77
84
|
join(dir, 'chat', 'cold'),
|
|
78
|
-
...
|
|
79
|
-
// Compatibility for profiles created before chat/
|
|
85
|
+
...sessionConversationMessageDirs(dir),
|
|
86
|
+
// Compatibility for profiles created before chat/session split.
|
|
80
87
|
join(dir, 'conversation', 'messages'),
|
|
81
88
|
join(dir, 'conversation', 'cold'),
|
|
82
89
|
];
|
package/yeaft/dream/apply.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { promises as fsp } from 'fs';
|
|
22
22
|
import { join, dirname } from 'path';
|
|
23
|
+
import { inspect } from 'util';
|
|
23
24
|
|
|
24
25
|
import { writeMemory, writeSummary, readMemory, readSummary } from '../memory/store.js';
|
|
25
26
|
import { withDreamMarker } from './state.js';
|
|
@@ -28,6 +29,18 @@ import { snapshotScope } from './snapshot.js';
|
|
|
28
29
|
import { parseJsonSafe } from './triage.js';
|
|
29
30
|
import { render } from './prompts/index.js';
|
|
30
31
|
|
|
32
|
+
function malformedJsonError(message, raw) {
|
|
33
|
+
const err = new Error(message);
|
|
34
|
+
err.rawSnippet = rawResponseSnippet(raw);
|
|
35
|
+
return err;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rawResponseSnippet(raw) {
|
|
39
|
+
if (typeof raw === 'string') return raw.slice(0, 1000);
|
|
40
|
+
if (raw == null) return String(raw);
|
|
41
|
+
return inspect(raw, { depth: 2, maxArrayLength: 10, breakLength: 120 }).slice(0, 1000);
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
function applySystem(language) {
|
|
32
45
|
return String(language || '').toLowerCase().startsWith('zh')
|
|
33
46
|
? '你是梦境流水线的 Apply 阶段。你会根据最近的群组对话重写单个 scope 的 memory.md 和 summary.md。请只回复严格 JSON,不要输出说明文字或 markdown fence。memory_md 和 summary_md 的自然语言内容必须使用中文;JSON key、scope、schema 字段和代码标识符保持英文。'
|
|
@@ -204,7 +217,7 @@ export async function applyMergedTarget(merged, opts) {
|
|
|
204
217
|
const raw = await opts.llm({ pass: 'create', prompt, system: applySystem(opts.language) });
|
|
205
218
|
const parsed = parseJsonSafe(raw);
|
|
206
219
|
if (!parsed || typeof parsed.memory_md !== 'string') {
|
|
207
|
-
throw
|
|
220
|
+
throw malformedJsonError(`apply: CREATE returned malformed JSON for ${merged.target}`, raw);
|
|
208
221
|
}
|
|
209
222
|
memoryMd = parsed.memory_md;
|
|
210
223
|
summaryMd = typeof parsed.summary_md === 'string' ? parsed.summary_md : '';
|
|
@@ -233,7 +246,7 @@ export async function applyMergedTarget(merged, opts) {
|
|
|
233
246
|
const raw = await opts.llm({ pass: 'update', prompt, system: applySystem(opts.language) });
|
|
234
247
|
const parsed = parseJsonSafe(raw);
|
|
235
248
|
if (!parsed || typeof parsed.memory_md !== 'string') {
|
|
236
|
-
throw
|
|
249
|
+
throw malformedJsonError(`apply: UPDATE batch ${i} returned malformed JSON for ${merged.target}`, raw);
|
|
237
250
|
}
|
|
238
251
|
memoryMd = parsed.memory_md;
|
|
239
252
|
if (typeof parsed.summary_md === 'string') summaryMd = parsed.summary_md;
|
package/yeaft/dream/runner.js
CHANGED
|
@@ -176,6 +176,7 @@ export async function runDream(opts) {
|
|
|
176
176
|
phase: 'triage',
|
|
177
177
|
message: err.message,
|
|
178
178
|
stack: err.stack,
|
|
179
|
+
rawSnippet: err.rawSnippet,
|
|
179
180
|
});
|
|
180
181
|
continue;
|
|
181
182
|
}
|
|
@@ -227,6 +228,7 @@ export async function runDream(opts) {
|
|
|
227
228
|
phase: 'apply',
|
|
228
229
|
message: err.message,
|
|
229
230
|
stack: err.stack,
|
|
231
|
+
rawSnippet: err.rawSnippet,
|
|
230
232
|
});
|
|
231
233
|
}
|
|
232
234
|
}
|
|
@@ -38,10 +38,11 @@
|
|
|
38
38
|
* `sessionId` may be inherited via `stampDreamScope()` when a scope is active.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
41
42
|
import { join } from 'path';
|
|
42
43
|
import { runDream } from './runner.js';
|
|
43
44
|
import { createDreamScheduler } from './schedule.js';
|
|
44
|
-
import {
|
|
45
|
+
import { parseMessage, parseSeqFromId } from '../conversation/persist.js';
|
|
45
46
|
import { readSessionState } from './state.js';
|
|
46
47
|
import { DREAM_NUDGE_AFTER_MESSAGES, DREAM_INTERVAL_HOURS } from './limits.js';
|
|
47
48
|
|
|
@@ -56,48 +57,47 @@ import { DREAM_NUDGE_AFTER_MESSAGES, DREAM_INTERVAL_HOURS } from './limits.js';
|
|
|
56
57
|
export function buildRunDreamOpts(session, onProgress) {
|
|
57
58
|
const yeaftDir = session.yeaftDir;
|
|
58
59
|
const memoryRoot = join(yeaftDir, 'memory');
|
|
59
|
-
const
|
|
60
|
+
const sessionConversationsRoot = join(yeaftDir, 'sessions');
|
|
61
|
+
// Legacy disk fallback for pre-session transcript directories. New writes and
|
|
62
|
+
// Dream's primary source use `sessions/<sessionId>/conversation`.
|
|
63
|
+
const legacySessionConversationsRoot = join(yeaftDir, 'groups');
|
|
60
64
|
|
|
61
65
|
return {
|
|
62
66
|
root: memoryRoot,
|
|
63
67
|
language: session.config?.language || 'en',
|
|
64
68
|
llm: makeLlm(session),
|
|
65
69
|
listSessions: async () => {
|
|
66
|
-
try { return
|
|
70
|
+
try { return listConversationSessions([sessionConversationsRoot, legacySessionConversationsRoot]); }
|
|
67
71
|
catch { return []; }
|
|
68
72
|
},
|
|
69
|
-
countMessages: async (
|
|
70
|
-
try {
|
|
71
|
-
|
|
72
|
-
let n = 0;
|
|
73
|
-
for (const _m of h.streamMessages()) n += 1;
|
|
74
|
-
return n;
|
|
75
|
-
} catch { return 0; }
|
|
73
|
+
countMessages: async (sessionId) => {
|
|
74
|
+
try { return loadSessionConversationMessages([sessionConversationsRoot, legacySessionConversationsRoot], sessionId).length; }
|
|
75
|
+
catch { return 0; }
|
|
76
76
|
},
|
|
77
|
-
loadGroupDiff: async (
|
|
77
|
+
loadGroupDiff: async (sessionId, sinceId) => {
|
|
78
78
|
try {
|
|
79
|
-
const
|
|
79
|
+
const messages = loadSessionConversationMessages([sessionConversationsRoot, legacySessionConversationsRoot], sessionId);
|
|
80
80
|
const out = [];
|
|
81
81
|
let started = !sinceId;
|
|
82
|
-
for (const m of
|
|
82
|
+
for (const m of messages) {
|
|
83
83
|
if (!started) {
|
|
84
84
|
if (m.id === sinceId) started = true;
|
|
85
85
|
continue;
|
|
86
86
|
}
|
|
87
|
-
out.push(
|
|
87
|
+
out.push(translateSessionConversationMessage(m));
|
|
88
88
|
}
|
|
89
89
|
return out;
|
|
90
90
|
} catch { return []; }
|
|
91
91
|
},
|
|
92
|
-
loadOverlapPreamble: async (
|
|
92
|
+
loadOverlapPreamble: async (sessionId, beforeId, n) => {
|
|
93
93
|
try {
|
|
94
|
-
const
|
|
94
|
+
const messages = loadSessionConversationMessages([sessionConversationsRoot, legacySessionConversationsRoot], sessionId);
|
|
95
95
|
const buf = [];
|
|
96
|
-
for (const m of
|
|
96
|
+
for (const m of messages) {
|
|
97
97
|
if (m.id === beforeId) break;
|
|
98
98
|
buf.push(m);
|
|
99
99
|
}
|
|
100
|
-
return buf.slice(-n).map(
|
|
100
|
+
return buf.slice(-n).map(translateSessionConversationMessage);
|
|
101
101
|
} catch { return []; }
|
|
102
102
|
},
|
|
103
103
|
onProgress,
|
|
@@ -105,8 +105,8 @@ export function buildRunDreamOpts(session, onProgress) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
* Translate a
|
|
109
|
-
*
|
|
108
|
+
* Translate a persisted session conversation message into the shape runDream
|
|
109
|
+
* expects (id, role, body, vpId, author).
|
|
110
110
|
*
|
|
111
111
|
* (2026-05-13: legacy `m.meta.featureId` propagation was dropped along
|
|
112
112
|
* with the Feature system. Historical messages on disk may still carry
|
|
@@ -114,19 +114,107 @@ export function buildRunDreamOpts(session, onProgress) {
|
|
|
114
114
|
*
|
|
115
115
|
* @param {Object} m
|
|
116
116
|
*/
|
|
117
|
-
function
|
|
118
|
-
const role = m.role ||
|
|
117
|
+
function translateSessionConversationMessage(m) {
|
|
118
|
+
const role = m.role || 'assistant';
|
|
119
119
|
const out = {
|
|
120
120
|
id: m.id,
|
|
121
121
|
role,
|
|
122
|
-
body: m.text || '',
|
|
122
|
+
body: m.content || m.text || '',
|
|
123
123
|
};
|
|
124
|
-
if (role === 'assistant' && m.
|
|
125
|
-
out.vpId = m.
|
|
124
|
+
if (role === 'assistant' && m.speakerVpId) {
|
|
125
|
+
out.vpId = m.speakerVpId;
|
|
126
126
|
}
|
|
127
127
|
return out;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Enumerate session ids that have persisted conversation messages. `sessions/`
|
|
132
|
+
* is the primary layout; `groups/` is read-only legacy fallback.
|
|
133
|
+
*
|
|
134
|
+
* @param {string[]} roots session transcript roots in priority order
|
|
135
|
+
* @returns {string[]}
|
|
136
|
+
*/
|
|
137
|
+
function listConversationSessions(roots) {
|
|
138
|
+
const out = new Set();
|
|
139
|
+
for (const root of roots) {
|
|
140
|
+
if (!existsSync(root)) continue;
|
|
141
|
+
for (const name of readdirSync(root)) {
|
|
142
|
+
if (name.startsWith('.')) continue;
|
|
143
|
+
try {
|
|
144
|
+
const dir = sessionConversationDir(root, name);
|
|
145
|
+
if (!hasReadableMessages(dir)) continue;
|
|
146
|
+
out.add(name);
|
|
147
|
+
} catch {
|
|
148
|
+
// Ignore partial or old session directories. Dream only needs sessions
|
|
149
|
+
// with readable conversation messages.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return [...out].sort();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Load hot and cold conversation messages for one Yeaft Session. This mirrors
|
|
158
|
+
* ConversationStore's session history source: `conversation/cold` plus
|
|
159
|
+
* `conversation/messages`, deduped by id and sorted by message sequence.
|
|
160
|
+
*
|
|
161
|
+
* @param {string[]} roots session transcript roots in priority order
|
|
162
|
+
* @param {string} sessionId
|
|
163
|
+
* @returns {object[]}
|
|
164
|
+
*/
|
|
165
|
+
function loadSessionConversationMessages(roots, sessionId) {
|
|
166
|
+
const byId = new Map();
|
|
167
|
+
for (const root of roots) {
|
|
168
|
+
const dir = sessionConversationDir(root, sessionId);
|
|
169
|
+
for (const m of loadConversationDirMessages(dir)) {
|
|
170
|
+
if (!byId.has(m.id)) byId.set(m.id, m);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return [...byId.values()].sort(compareMessagesBySeq);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sessionConversationDir(root, sessionId) {
|
|
177
|
+
return join(root, safeDirComponent(sessionId), 'conversation');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasReadableMessages(conversationDir) {
|
|
181
|
+
return ['messages', 'cold'].some(kind => {
|
|
182
|
+
const dir = join(conversationDir, kind);
|
|
183
|
+
try {
|
|
184
|
+
return statSync(dir).isDirectory() && readdirSync(dir).some(f => f.endsWith('.md'));
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function loadConversationDirMessages(conversationDir) {
|
|
192
|
+
return ['cold', 'messages'].flatMap(kind => loadMessageDir(join(conversationDir, kind)));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function loadMessageDir(dir) {
|
|
196
|
+
if (!existsSync(dir)) return [];
|
|
197
|
+
return readdirSync(dir)
|
|
198
|
+
.filter(f => f.endsWith('.md'))
|
|
199
|
+
.map(file => {
|
|
200
|
+
try { return parseMessage(readFileSync(join(dir, file), 'utf8')); }
|
|
201
|
+
catch { return null; }
|
|
202
|
+
})
|
|
203
|
+
.filter(m => m && m.id);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function compareMessagesBySeq(a, b) {
|
|
207
|
+
const sa = parseSeqFromId(a?.id);
|
|
208
|
+
const sb = parseSeqFromId(b?.id);
|
|
209
|
+
if (Number.isFinite(sa) && Number.isFinite(sb) && sa !== sb) return sa - sb;
|
|
210
|
+
return String(a?.time || '').localeCompare(String(b?.time || ''));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function safeDirComponent(s) {
|
|
214
|
+
const safe = String(s).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 120).replace(/^\.+$/, '_');
|
|
215
|
+
return safe || '_';
|
|
216
|
+
}
|
|
217
|
+
|
|
130
218
|
/**
|
|
131
219
|
* Build the LLM callable that runDream's triage/apply prompts use.
|
|
132
220
|
*
|
package/yeaft/dream/state.js
CHANGED
|
@@ -178,6 +178,7 @@ export async function writeDreamError(root, scope, info) {
|
|
|
178
178
|
phase: String(info?.phase || 'unknown'),
|
|
179
179
|
message: String(info?.message || ''),
|
|
180
180
|
stack: stackLines.length > 0 ? stackLines.join('\n') : null,
|
|
181
|
+
rawSnippet: typeof info?.rawSnippet === 'string' ? info.rawSnippet.slice(0, 1000) : null,
|
|
181
182
|
}, null, 2) + '\n';
|
|
182
183
|
await atomicWrite(abs, body);
|
|
183
184
|
} catch {
|