@yeaft/webchat-agent 0.1.941 → 0.1.943
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/debug-trace.js +1 -1
- package/yeaft/dream/apply.js +15 -19
- package/yeaft/dream/output-snapshot.js +3 -6
- package/yeaft/dream/prompts/extract-session.md +2 -2
- package/yeaft/dream/prompts/extract-topic.md +1 -1
- package/yeaft/dream/prompts/index.js +4 -6
- package/yeaft/dream/prompts/triage-pass1.md +1 -1
- package/yeaft/dream/runner.js +44 -44
- package/yeaft/dream/session-wiring.js +5 -5
- package/yeaft/dream/state.js +20 -25
- package/yeaft/dream/triage.js +12 -12
- package/yeaft/engine.js +28 -31
- package/yeaft/memory/store.js +61 -6
- package/yeaft/sessions/seed-default.js +1 -1
- package/yeaft/sessions/session-crud.js +1 -5
- package/yeaft/web-bridge.js +29 -38
package/package.json
CHANGED
package/yeaft/debug-trace.js
CHANGED
|
@@ -529,7 +529,7 @@ export class DebugTrace {
|
|
|
529
529
|
const target = typeof data.target === 'string' ? data.target : '';
|
|
530
530
|
if (sessionId) {
|
|
531
531
|
const isBroadcast = !evtGroupId && !target;
|
|
532
|
-
const isThisGroup = evtGroupId === sessionId || target === `
|
|
532
|
+
const isThisGroup = evtGroupId === sessionId || target === `sessions/${sessionId}`;
|
|
533
533
|
if (!isBroadcast && !isThisGroup) continue;
|
|
534
534
|
}
|
|
535
535
|
dreamEvents.push({
|
package/yeaft/dream/apply.js
CHANGED
|
@@ -99,7 +99,7 @@ function renderSourceBlocks(sources, language) {
|
|
|
99
99
|
const out = [];
|
|
100
100
|
for (const src of (sources || [])) {
|
|
101
101
|
out.push('');
|
|
102
|
-
out.push(`[
|
|
102
|
+
out.push(`[sessions/${src.sessionId}]`);
|
|
103
103
|
for (const m of (src.diff || [])) {
|
|
104
104
|
const head = `[${m.role || 'message'}${m.kind === 'overlap' ? (String(language || '').toLowerCase().startsWith('zh') ? '(已处理)' : ' (already processed)') : ''}]`;
|
|
105
105
|
out.push(head);
|
|
@@ -110,7 +110,7 @@ function renderSourceBlocks(sources, language) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
* Translate `target` like '
|
|
113
|
+
* Translate `target` like 'sessions/g-eng' to a Scope
|
|
114
114
|
* understood by store. Throws if the path is malformed.
|
|
115
115
|
*
|
|
116
116
|
* @param {string} target
|
|
@@ -121,26 +121,22 @@ export function targetToScope(target) {
|
|
|
121
121
|
if (target === 'user') return { kind: 'user' };
|
|
122
122
|
const segs = target.split('/').filter(Boolean);
|
|
123
123
|
// Legacy scopes — explicitly rejected. Old data lives under .legacy/.
|
|
124
|
-
if (segs[0] === 'vp' || segs[0] === 'feature' || segs[0] === 'topic') {
|
|
125
|
-
throw new Error(`apply.targetToScope: legacy root scope ${JSON.stringify(target)} rejected — use
|
|
124
|
+
if (segs[0] === 'group' || segs[0] === 'vp' || segs[0] === 'feature' || segs[0] === 'topic') {
|
|
125
|
+
throw new Error(`apply.targetToScope: legacy root scope ${JSON.stringify(target)} rejected — use sessions/<sessionId>`);
|
|
126
126
|
}
|
|
127
|
-
if (segs[0] === '
|
|
128
|
-
if (segs.length === 2) return { kind: '
|
|
129
|
-
// group/<g>/user
|
|
127
|
+
if (segs[0] === 'sessions') {
|
|
128
|
+
if (segs.length === 2) return { kind: 'session', id: segs[1] };
|
|
130
129
|
if (segs.length === 3 && segs[2] === 'user') {
|
|
131
|
-
return { kind: '
|
|
130
|
+
return { kind: 'session-user', sessionId: segs[1] };
|
|
132
131
|
}
|
|
133
|
-
// group/<g>/vp/<v>
|
|
134
132
|
if (segs.length === 4 && segs[2] === 'vp') {
|
|
135
|
-
return { kind: '
|
|
133
|
+
return { kind: 'session-vp', sessionId: segs[1], id: segs[3] };
|
|
136
134
|
}
|
|
137
|
-
// group/<g>/feature/<f>
|
|
138
135
|
if (segs.length === 4 && segs[2] === 'feature') {
|
|
139
|
-
return { kind: '
|
|
136
|
+
return { kind: 'session-feature', sessionId: segs[1], id: segs[3] };
|
|
140
137
|
}
|
|
141
|
-
// group/<g>/topic/<l1>[/<l2>]
|
|
142
138
|
if (segs[2] === 'topic' && (segs.length === 4 || segs.length === 5)) {
|
|
143
|
-
return { kind: '
|
|
139
|
+
return { kind: 'session-topic', sessionId: segs[1], path: segs.slice(3) };
|
|
144
140
|
}
|
|
145
141
|
}
|
|
146
142
|
if (segs[0] === 'chat') {
|
|
@@ -275,11 +271,11 @@ export async function applyMergedTarget(merged, opts) {
|
|
|
275
271
|
function scopeRelDir(scope) {
|
|
276
272
|
switch (scope.kind) {
|
|
277
273
|
case 'user': return 'user';
|
|
278
|
-
case '
|
|
279
|
-
case '
|
|
280
|
-
case '
|
|
281
|
-
case '
|
|
282
|
-
case '
|
|
274
|
+
case 'session': return `sessions/${scope.id}`;
|
|
275
|
+
case 'session-user': return `sessions/${scope.sessionId}/user`;
|
|
276
|
+
case 'session-vp': return `sessions/${scope.sessionId}/vp/${scope.id}`;
|
|
277
|
+
case 'session-feature': return `sessions/${scope.sessionId}/feature/${scope.id}`;
|
|
278
|
+
case 'session-topic': return `sessions/${scope.sessionId}/topic/${scope.path.join('/')}`;
|
|
283
279
|
case 'chat': return `chat/${scope.id}`;
|
|
284
280
|
case 'chat-vp': return `chat/${scope.chatId}/vp/${scope.id}`;
|
|
285
281
|
default: throw new Error(`apply.scopeRelDir: unknown kind ${scope.kind}`);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
|
|
12
12
|
import { readMemory, readSummary } from '../memory/store.js';
|
|
13
|
-
import {
|
|
13
|
+
import { readSessionState } from './state.js';
|
|
14
14
|
|
|
15
15
|
export const DREAM_SNAPSHOT_TEXT_LIMIT = 6000;
|
|
16
16
|
|
|
@@ -30,15 +30,12 @@ export function truncateDreamText(value, limit = DREAM_SNAPSHOT_TEXT_LIMIT) {
|
|
|
30
30
|
export async function buildDreamOutputSnapshot(sessionLike, sessionId) {
|
|
31
31
|
if (!sessionId || !sessionLike?.yeaftDir) return null;
|
|
32
32
|
const scope = `sessions/${sessionId}`;
|
|
33
|
-
|
|
34
|
-
// the historical kind:'group' store path. The snapshot's public scope label
|
|
35
|
-
// is still sessions/<id>.
|
|
36
|
-
const memoryScope = { kind: 'group', id: sessionId };
|
|
33
|
+
const memoryScope = { kind: 'session', id: sessionId };
|
|
37
34
|
const root = join(sessionLike.yeaftDir, 'memory');
|
|
38
35
|
const [memoryRaw, summaryRaw, state] = await Promise.all([
|
|
39
36
|
readMemory(memoryScope, { root }).catch(() => ''),
|
|
40
37
|
readSummary(memoryScope, { root }).catch(() => ''),
|
|
41
|
-
|
|
38
|
+
readSessionState(root, sessionId).catch(() => ({
|
|
42
39
|
lastDreamMessageId: null,
|
|
43
40
|
lastDreamAt: null,
|
|
44
41
|
messageCount: 0,
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
You are extracting **memory segments** from a conversation between the
|
|
4
4
|
user and a Yeaft AI companion. This pass focuses on a specific
|
|
5
|
-
**`
|
|
5
|
+
**`sessions/<id>` scope**: long-lived facts about one collaboration session
|
|
6
6
|
(a project team, a study cohort, a working set of people/agents).
|
|
7
7
|
|
|
8
8
|
The target session id is provided as `{{sessionId}}`.
|
|
9
9
|
|
|
10
|
-
## What to extract for `
|
|
10
|
+
## What to extract for `sessions/<id>` scope
|
|
11
11
|
|
|
12
12
|
- **purpose** — what this session exists to do, its charter / mission
|
|
13
13
|
- **members** — people, VPs, and roles in the session, and what each is
|
|
@@ -26,7 +26,7 @@ The target topic id is provided as `{{topicId}}`.
|
|
|
26
26
|
|
|
27
27
|
- Facts about the user as a person — `user` scope.
|
|
28
28
|
- Facts specific to one feature implementation — `feature/<id>`.
|
|
29
|
-
- Group conventions — `
|
|
29
|
+
- Group conventions — `sessions/<id>`.
|
|
30
30
|
- Generic encyclopedia facts the assistant already knows — only the
|
|
31
31
|
user's *durable views and confirmed knowledge* about the topic.
|
|
32
32
|
|
|
@@ -41,12 +41,10 @@ const FILES = {
|
|
|
41
41
|
export function extractTemplateForScope(scope) {
|
|
42
42
|
if (!scope || typeof scope !== 'string') return 'extractTopic';
|
|
43
43
|
if (scope === 'user') return 'extractUser';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (/^
|
|
47
|
-
if (
|
|
48
|
-
if (/^group\/[^/]+\/user(?:\/|$)/.test(scope)) return 'extractUser';
|
|
49
|
-
if (scope.startsWith('group/')) return 'extractSession';
|
|
44
|
+
if (/^sessions\/[^/]+\/vp\//.test(scope)) return 'extractVp';
|
|
45
|
+
if (/^sessions\/[^/]+\/topic\//.test(scope)) return 'extractTopic';
|
|
46
|
+
if (/^sessions\/[^/]+\/user(?:\/|$)/.test(scope)) return 'extractUser';
|
|
47
|
+
if (scope.startsWith('sessions/')) return 'extractSession';
|
|
50
48
|
// Chat-isolated scopes: same template family as groups.
|
|
51
49
|
if (/^chat\/[^/]+\/vp\//.test(scope)) return 'extractVp';
|
|
52
50
|
if (scope.startsWith('chat/')) return 'extractSession';
|
|
@@ -2,7 +2,7 @@ You are deciding whether a recent group conversation carries:
|
|
|
2
2
|
- signals that should update the USER profile, and/or
|
|
3
3
|
- signals that should update one or more TOPIC scopes.
|
|
4
4
|
|
|
5
|
-
Do NOT mention vp/, group/, or
|
|
5
|
+
Do NOT mention vp/, group/, feature/, or topic/ scopes — those are handled by hard rules.
|
|
6
6
|
|
|
7
7
|
Session: {{sessionId}}
|
|
8
8
|
|
package/yeaft/dream/runner.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
*
|
|
6
6
|
* trigger
|
|
7
7
|
* ↓
|
|
8
|
-
*
|
|
8
|
+
* enumerateSessions() via opts.listSessions()
|
|
9
9
|
* ↓
|
|
10
|
-
* for each
|
|
10
|
+
* for each session with newCount ≥ MIN_NEW_PER_GROUP (auto)
|
|
11
11
|
* or > 0 (manual)
|
|
12
|
-
* or prior messages in a scoped manual
|
|
12
|
+
* or prior messages in a scoped manual session rerun:
|
|
13
13
|
* loadDiff() via opts.loadGroupDiff(sessionId, sinceId)
|
|
14
14
|
* applyOverlap() via opts.loadOverlapPreamble(...)
|
|
15
15
|
* segment() segmentDiff(...)
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*
|
|
22
22
|
* bookkeep:
|
|
23
23
|
* for each processed group:
|
|
24
|
-
*
|
|
24
|
+
* session .dream-state ←
|
|
25
25
|
* { lastDreamMessageId: tail of real diff,
|
|
26
26
|
* lastDreamAt: nowIso,
|
|
27
27
|
* messageCount: <after> }
|
|
@@ -45,7 +45,7 @@ import { listScopes, readSummary } from '../memory/store.js';
|
|
|
45
45
|
import {
|
|
46
46
|
DEFAULT_LIMITS,
|
|
47
47
|
} from './limits.js';
|
|
48
|
-
import {
|
|
48
|
+
import { readSessionState, writeSessionState, writeDreamError } from './state.js';
|
|
49
49
|
import { segmentDiff, truncateMessage, estimateMessagesTokens } from './segment.js';
|
|
50
50
|
import { triageGroupSegments } from './triage.js';
|
|
51
51
|
import { mergeByTarget } from './merge.js';
|
|
@@ -56,9 +56,9 @@ import { tsForBackup, pruneOldSnapshots } from './snapshot.js';
|
|
|
56
56
|
* @typedef {Object} RunDreamOpts
|
|
57
57
|
* @property {string} root — memory root, e.g. ~/.yeaft/memory
|
|
58
58
|
* @property {boolean} [manual=false] — manual trigger overrides newCount<20 skip
|
|
59
|
-
* @property {string[]} [scopeFilter] — optional: only dream these targets; scoped manual
|
|
59
|
+
* @property {string[]} [scopeFilter] — optional: only dream these targets; scoped manual session triggers rerun the current session when there are prior messages but no new cursor delta ('*' allowed)
|
|
60
60
|
* @property {(req: {pass:string, prompt:string, system:string}) => Promise<string>} llm
|
|
61
|
-
* @property {() => Promise<Array<string>>} listSessions — return all
|
|
61
|
+
* @property {() => Promise<Array<string>>} listSessions — return all session ids (incl. '_no-group')
|
|
62
62
|
* @property {(sessionId: string) => Promise<number>} countMessages — total message count for a group
|
|
63
63
|
* @property {(sessionId: string, sinceMessageId: string|null) => Promise<Array<object>>} loadGroupDiff
|
|
64
64
|
* @property {(sessionId: string, beforeMessageId: string|null, count: number) => Promise<Array<object>>} loadOverlapPreamble
|
|
@@ -89,14 +89,14 @@ export async function runDream(opts) {
|
|
|
89
89
|
// 1. enumerate groups
|
|
90
90
|
const sessionIds = await safeCall(opts.listSessions, []);
|
|
91
91
|
const filter = Array.isArray(opts.scopeFilter) ? new Set(opts.scopeFilter) : null;
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
92
|
+
const sessionFilter = deriveSessionFilter(filter);
|
|
93
|
+
const sessionsReport = [];
|
|
94
|
+
const sessionTriages = [];
|
|
95
|
+
const processedSessions = [];
|
|
96
96
|
|
|
97
|
-
// 2. per-
|
|
98
|
-
// Topic summaries are
|
|
99
|
-
// them inside the per-
|
|
97
|
+
// 2. per-session: skip / segment / triage
|
|
98
|
+
// Topic summaries are resolved inside the per-session loop
|
|
99
|
+
// them inside the per-session loop instead of once up front.
|
|
100
100
|
const resolveTopicSummaries = async (sessionId) => {
|
|
101
101
|
if (opts.listTopicSummaries) {
|
|
102
102
|
return await safeCall(() => opts.listTopicSummaries(sessionId), []);
|
|
@@ -105,31 +105,31 @@ export async function runDream(opts) {
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
for (const sessionId of sessionIds) {
|
|
108
|
-
// Current-
|
|
108
|
+
// Current-session manual dream passes are the one case where scopeFilter
|
|
109
109
|
// must constrain enumeration too: clicking the conversation header means
|
|
110
|
-
// "dream this
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
if (
|
|
114
|
-
|
|
110
|
+
// "dream this session now", not "triage every session and then only apply
|
|
111
|
+
// sessions/<id>". Pure target filters such as ['user'] still triage every
|
|
112
|
+
// session so their hard-rule actions can contribute to the requested scope.
|
|
113
|
+
if (sessionFilter && !sessionFilter.has(sessionId)) {
|
|
114
|
+
sessionsReport.push({ sessionId, new: 0, status: 'skipped', reason: 'scope-filtered' });
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
|
-
const state = await
|
|
117
|
+
const state = await readSessionState(opts.root, sessionId);
|
|
118
118
|
const beforeCount = await safeCall(() => opts.countMessages(sessionId), 0);
|
|
119
119
|
const newCount = Math.max(0, beforeCount - (state.messageCount || 0));
|
|
120
120
|
|
|
121
121
|
const rerunScopedManual = !!opts.manual
|
|
122
|
-
&&
|
|
123
|
-
&&
|
|
122
|
+
&& sessionFilter
|
|
123
|
+
&& sessionFilter.has(sessionId)
|
|
124
124
|
&& newCount === 0
|
|
125
125
|
&& beforeCount > 0;
|
|
126
126
|
|
|
127
127
|
if (newCount === 0 && !rerunScopedManual) {
|
|
128
|
-
|
|
128
|
+
sessionsReport.push({ sessionId, new: 0, status: 'skipped', reason: 'no-new-messages' });
|
|
129
129
|
continue;
|
|
130
130
|
}
|
|
131
131
|
if (!opts.manual && newCount < limits.MIN_NEW_PER_GROUP) {
|
|
132
|
-
|
|
132
|
+
sessionsReport.push({ sessionId, new: newCount, status: 'skipped', reason: 'below-threshold' });
|
|
133
133
|
continue;
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -137,7 +137,7 @@ export async function runDream(opts) {
|
|
|
137
137
|
const diffCursor = rerunScopedManual ? null : state.lastDreamMessageId;
|
|
138
138
|
const diffNew = await safeCall(() => opts.loadGroupDiff(sessionId, diffCursor), []);
|
|
139
139
|
if (!diffNew || diffNew.length === 0) {
|
|
140
|
-
|
|
140
|
+
sessionsReport.push({ sessionId, new: newCount, status: 'skipped', reason: 'empty-diff' });
|
|
141
141
|
continue;
|
|
142
142
|
}
|
|
143
143
|
const overlapMessages = state.lastDreamMessageId && !rerunScopedManual
|
|
@@ -167,12 +167,12 @@ export async function runDream(opts) {
|
|
|
167
167
|
language: opts.language,
|
|
168
168
|
});
|
|
169
169
|
} catch (err) {
|
|
170
|
-
|
|
170
|
+
sessionsReport.push({ sessionId, new: newCount, status: 'error', error: err.message });
|
|
171
171
|
onProgress({ phase: 'triage', sessionId, status: 'error', error: err.message });
|
|
172
172
|
// Journal the failure on disk so operators can see WHY dream is
|
|
173
173
|
// not advancing without having to enable `config.debug`. Best-
|
|
174
174
|
// effort — `writeDreamError` swallows its own I/O errors.
|
|
175
|
-
await writeDreamError(opts.root, `
|
|
175
|
+
await writeDreamError(opts.root, `sessions/${sessionId}`, {
|
|
176
176
|
phase: 'triage',
|
|
177
177
|
message: err.message,
|
|
178
178
|
stack: err.stack,
|
|
@@ -181,17 +181,17 @@ export async function runDream(opts) {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
onProgress({ phase: 'triage', sessionId, status: 'done', actions: actions.length });
|
|
184
|
-
|
|
184
|
+
sessionTriages.push({ sessionId, diff: fullDiff, actions });
|
|
185
185
|
|
|
186
186
|
const tailId = lastMessageId(diffNew);
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
processedSessions.push({ sessionId, tailId, beforeCount, newCount, segments: segments.length, actions: actions.length });
|
|
188
|
+
sessionsReport.push({ sessionId, new: newCount, segments: segments.length, actions: actions.length, status: 'triaged', rerun: rerunScopedManual || undefined });
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
// 3. merge
|
|
192
|
-
const mergedTargets = mergeByTarget(
|
|
192
|
+
const mergedTargets = mergeByTarget(sessionTriages);
|
|
193
193
|
const targetsToApply = filter && filter.size > 0 && !filter.has('*')
|
|
194
|
-
? mergedTargets.filter(t => filter.has(t.target) || filter.has(`
|
|
194
|
+
? mergedTargets.filter(t => filter.has(t.target) || filter.has(`sessions/${sourceSessionId(t)}`))
|
|
195
195
|
: mergedTargets;
|
|
196
196
|
|
|
197
197
|
onProgress({ phase: 'merge', targets: targetsToApply.length });
|
|
@@ -237,13 +237,13 @@ export async function runDream(opts) {
|
|
|
237
237
|
// cursor. (If everything errored, we keep the cursor so next run
|
|
238
238
|
// retries.)
|
|
239
239
|
const successfulTargets = new Set(targetsReport.filter(r => r.status === 'done').map(r => r.target));
|
|
240
|
-
for (const pg of
|
|
241
|
-
const contributed = (
|
|
240
|
+
for (const pg of processedSessions) {
|
|
241
|
+
const contributed = (sessionTriages.find(g => g.sessionId === pg.sessionId) || { actions: [] })
|
|
242
242
|
.actions.map(a => a.scope);
|
|
243
243
|
const anySuccess = contributed.some(t => successfulTargets.has(t));
|
|
244
244
|
if (!anySuccess) continue;
|
|
245
245
|
if (pg.tailId) {
|
|
246
|
-
await
|
|
246
|
+
await writeSessionState(opts.root, pg.sessionId, {
|
|
247
247
|
lastDreamMessageId: pg.tailId,
|
|
248
248
|
lastDreamAt: nowIso,
|
|
249
249
|
messageCount: pg.beforeCount,
|
|
@@ -257,7 +257,7 @@ export async function runDream(opts) {
|
|
|
257
257
|
const duration = Date.now() - startedAt;
|
|
258
258
|
onProgress({
|
|
259
259
|
phase: 'done',
|
|
260
|
-
|
|
260
|
+
sessions: processedSessions.length,
|
|
261
261
|
targets: targetsReport.length,
|
|
262
262
|
duration,
|
|
263
263
|
backupsKept: pruned.kept.length,
|
|
@@ -267,7 +267,7 @@ export async function runDream(opts) {
|
|
|
267
267
|
return {
|
|
268
268
|
startedAt: nowIso,
|
|
269
269
|
durationMs: duration,
|
|
270
|
-
|
|
270
|
+
sessions: sessionsReport,
|
|
271
271
|
targets: targetsReport,
|
|
272
272
|
backups: pruned,
|
|
273
273
|
ts,
|
|
@@ -284,18 +284,18 @@ function lastMessageId(messages) {
|
|
|
284
284
|
return null;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
function
|
|
287
|
+
function deriveSessionFilter(filter) {
|
|
288
288
|
if (!filter || filter.size === 0 || filter.has('*')) return null;
|
|
289
|
-
const
|
|
289
|
+
const sessions = [];
|
|
290
290
|
for (const scope of filter) {
|
|
291
291
|
if (typeof scope !== 'string') continue;
|
|
292
|
-
const m = /^
|
|
293
|
-
if (m && m[1])
|
|
292
|
+
const m = /^sessions\/([^/]+)$/.exec(scope);
|
|
293
|
+
if (m && m[1]) sessions.push(m[1]);
|
|
294
294
|
}
|
|
295
|
-
return
|
|
295
|
+
return sessions.length > 0 ? new Set(sessions) : null;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
function
|
|
298
|
+
function sourceSessionId(mergedTarget) {
|
|
299
299
|
const src = Array.isArray(mergedTarget?.sources) ? mergedTarget.sources[0] : null;
|
|
300
300
|
return src && typeof src.sessionId === 'string' ? src.sessionId : '';
|
|
301
301
|
}
|
|
@@ -42,7 +42,7 @@ import { join } from 'path';
|
|
|
42
42
|
import { runDream } from './runner.js';
|
|
43
43
|
import { createDreamScheduler } from './schedule.js';
|
|
44
44
|
import { listSessions, openSession } from '../sessions/session-store.js';
|
|
45
|
-
import {
|
|
45
|
+
import { readSessionState } from './state.js';
|
|
46
46
|
import { DREAM_NUDGE_AFTER_MESSAGES, DREAM_INTERVAL_HOURS } from './limits.js';
|
|
47
47
|
|
|
48
48
|
/**
|
|
@@ -262,7 +262,7 @@ function finalizeDreamMetrics(metrics, durationMs) {
|
|
|
262
262
|
|
|
263
263
|
function summarizeDreamResult(result = {}) {
|
|
264
264
|
return {
|
|
265
|
-
|
|
265
|
+
sessions: Array.isArray(result.sessions) ? result.sessions.length : 0,
|
|
266
266
|
targets: Array.isArray(result.targets) ? result.targets.length : 0,
|
|
267
267
|
error: result.error || null,
|
|
268
268
|
skipped: !!result.skipped,
|
|
@@ -461,7 +461,7 @@ export async function bootInitEmptyGroups(args) {
|
|
|
461
461
|
const empty = [];
|
|
462
462
|
for (const gid of ids) {
|
|
463
463
|
let segCount;
|
|
464
|
-
try { segCount = args.memoryIndex.listByScope(`
|
|
464
|
+
try { segCount = args.memoryIndex.listByScope(`sessions/${gid}`).length; }
|
|
465
465
|
catch { continue; }
|
|
466
466
|
if (segCount > 0) continue;
|
|
467
467
|
let hasMessages = false;
|
|
@@ -473,7 +473,7 @@ export async function bootInitEmptyGroups(args) {
|
|
|
473
473
|
hasMessages = !first.done;
|
|
474
474
|
} catch { continue; }
|
|
475
475
|
if (!hasMessages) continue;
|
|
476
|
-
empty.push(`
|
|
476
|
+
empty.push(`sessions/${gid}`);
|
|
477
477
|
}
|
|
478
478
|
if (empty.length === 0) return out;
|
|
479
479
|
if (args.config?.debug) {
|
|
@@ -534,7 +534,7 @@ export async function bootCatchUpStaleDream(args) {
|
|
|
534
534
|
let anyTraffic = false;
|
|
535
535
|
for (const gid of sessionIds) {
|
|
536
536
|
let st;
|
|
537
|
-
try { st = await
|
|
537
|
+
try { st = await readSessionState(memoryRoot, gid); }
|
|
538
538
|
catch { continue; }
|
|
539
539
|
if (st.lastDreamAt) {
|
|
540
540
|
const t = Date.parse(st.lastDreamAt);
|
package/yeaft/dream/state.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Three pieces of state, tracked separately:
|
|
5
5
|
*
|
|
6
|
-
* 1. Per-
|
|
6
|
+
* 1. Per-session control state (used to decide whether a session enters
|
|
7
7
|
* triage and how far to advance the cursor):
|
|
8
8
|
*
|
|
9
|
-
* ~/.yeaft/memory/
|
|
9
|
+
* ~/.yeaft/memory/sessions/<id>/.dream-state
|
|
10
10
|
*
|
|
11
11
|
* A 3-line text file:
|
|
12
12
|
*
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* Fields are independent of each other; missing fields default to
|
|
18
18
|
* empty / null / 0. The file is rewritten atomically every dream.
|
|
19
19
|
*
|
|
20
|
-
* The virtual `_no-
|
|
21
|
-
* (`
|
|
20
|
+
* The virtual `_no-session/` session lives at the same path layout
|
|
21
|
+
* (`sessions/_no-session/.dream-state`) and uses the same accessor.
|
|
22
22
|
*
|
|
23
23
|
* 2. Per-scope observability marker, embedded inside the scope's
|
|
24
24
|
* `memory.md` between two HTML comments at the file's tail:
|
|
@@ -53,42 +53,37 @@ const ERROR_FILE = '.dream-last-error.json';
|
|
|
53
53
|
const DREAM_BLOCK_OPEN = '<!-- dream-state -->';
|
|
54
54
|
const DREAM_BLOCK_CLOSE = '<!-- /dream-state -->';
|
|
55
55
|
|
|
56
|
-
// ─── per-
|
|
56
|
+
// ─── per-session ────────────────────────────────────────────────
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Read a
|
|
59
|
+
* Read a session's .dream-state. Missing file → defaults.
|
|
60
60
|
*
|
|
61
61
|
* @param {string} root — memory root, e.g. ~/.yeaft/memory
|
|
62
62
|
* @param {string} sessionId
|
|
63
63
|
* @returns {Promise<{ lastDreamMessageId: string|null, lastDreamAt: string|null, messageCount: number }>}
|
|
64
64
|
*/
|
|
65
|
-
export async function
|
|
66
|
-
const abs = join(root, '
|
|
67
|
-
const legacyAbs = join(root, 'group', sessionId, STATE_FILE);
|
|
65
|
+
export async function readSessionState(root, sessionId) {
|
|
66
|
+
const abs = join(root, 'sessions', sessionId, STATE_FILE);
|
|
68
67
|
const empty = { lastDreamMessageId: null, lastDreamAt: null, messageCount: 0 };
|
|
69
68
|
let raw;
|
|
70
69
|
try { raw = await fsp.readFile(abs, 'utf8'); }
|
|
71
70
|
catch (err) {
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
catch (legacyErr) {
|
|
75
|
-
if (legacyErr && legacyErr.code === 'ENOENT') return empty;
|
|
76
|
-
throw legacyErr;
|
|
77
|
-
}
|
|
71
|
+
if (err && err.code === 'ENOENT') return empty;
|
|
72
|
+
throw err;
|
|
78
73
|
}
|
|
79
|
-
return
|
|
74
|
+
return parseSessionState(raw);
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
/**
|
|
83
|
-
* Atomically rewrite a
|
|
78
|
+
* Atomically rewrite a session's .dream-state. Creates the session dir if
|
|
84
79
|
* absent. Unknown fields are ignored.
|
|
85
80
|
*
|
|
86
81
|
* @param {string} root
|
|
87
82
|
* @param {string} sessionId
|
|
88
83
|
* @param {{ lastDreamMessageId?: string|null, lastDreamAt?: string|null, messageCount?: number }} state
|
|
89
84
|
*/
|
|
90
|
-
export async function
|
|
91
|
-
const dir = join(root, '
|
|
85
|
+
export async function writeSessionState(root, sessionId, state) {
|
|
86
|
+
const dir = join(root, 'sessions', sessionId);
|
|
92
87
|
await fsp.mkdir(dir, { recursive: true });
|
|
93
88
|
const abs = join(dir, STATE_FILE);
|
|
94
89
|
const body =
|
|
@@ -103,7 +98,7 @@ export async function writeGroupState(root, sessionId, state) {
|
|
|
103
98
|
* empty values.
|
|
104
99
|
* @param {string} raw
|
|
105
100
|
*/
|
|
106
|
-
function
|
|
101
|
+
function parseSessionState(raw) {
|
|
107
102
|
const out = { lastDreamMessageId: null, lastDreamAt: null, messageCount: 0 };
|
|
108
103
|
const lines = String(raw || '').split(/\r?\n/);
|
|
109
104
|
for (const ln of lines) {
|
|
@@ -134,12 +129,12 @@ function parseGroupState(raw) {
|
|
|
134
129
|
// `writeDreamError` writes `<memoryRoot>/<scope>/.dream-last-error.json`
|
|
135
130
|
// unconditionally on every catch (best-effort — write failures must not
|
|
136
131
|
// shadow the original error). Operators can then `ls ~/.yeaft/memory/
|
|
137
|
-
//
|
|
132
|
+
// sessions/<id>/` and see what blew up, without having to re-enable debug.
|
|
138
133
|
|
|
139
134
|
/**
|
|
140
135
|
* Resolve a memoryRoot + scope-string to the scope directory.
|
|
141
136
|
* The scope string is the same shape dream already uses internally:
|
|
142
|
-
* `'user'`, `'
|
|
137
|
+
* `'user'`, `'sessions/<sessionId>'`, `'sessions/<sessionId>/vp/<vpId>'`, etc.
|
|
143
138
|
*
|
|
144
139
|
* Pure path-join; does NOT create the directory. The writer creates it.
|
|
145
140
|
*
|
|
@@ -149,7 +144,7 @@ function parseGroupState(raw) {
|
|
|
149
144
|
*/
|
|
150
145
|
export function scopeDirFor(root, scope) {
|
|
151
146
|
// Defensive: trim leading/trailing slashes so callers can pass either
|
|
152
|
-
// `'
|
|
147
|
+
// `'sessions/grp_fun'` or `/sessions/grp_fun/` — both land on the same dir.
|
|
153
148
|
const clean = String(scope || '').replace(/^\/+|\/+$/g, '');
|
|
154
149
|
return join(root, clean);
|
|
155
150
|
}
|
|
@@ -160,7 +155,7 @@ export function scopeDirFor(root, scope) {
|
|
|
160
155
|
* error-handling path and we must not mask the original failure.
|
|
161
156
|
*
|
|
162
157
|
* @param {string} root — memory root, e.g. ~/.yeaft/memory
|
|
163
|
-
* @param {string} scope — `'
|
|
158
|
+
* @param {string} scope — `'sessions/<id>'` for triage failures,
|
|
164
159
|
* `merged.target` for apply failures.
|
|
165
160
|
* @param {{ phase: string, message: string, stack?: string|null, at?: string }} info
|
|
166
161
|
* @returns {Promise<void>}
|
|
@@ -283,4 +278,4 @@ async function atomicWrite(absPath, content) {
|
|
|
283
278
|
}
|
|
284
279
|
|
|
285
280
|
// re-exported for tests
|
|
286
|
-
export const _internals = {
|
|
281
|
+
export const _internals = { parseSessionState, extractDreamBlock };
|
package/yeaft/dream/triage.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isValidTopic } from '../memory/store.js';
|
|
1
2
|
/**
|
|
2
3
|
* dream/triage.js.
|
|
3
4
|
*
|
|
@@ -5,7 +6,7 @@
|
|
|
5
6
|
* The decision is two-staged on purpose:
|
|
6
7
|
*
|
|
7
8
|
* 1. **Hard rules** (this module, no LLM): everything we can determine
|
|
8
|
-
* from message metadata. Always include the active
|
|
9
|
+
* from message metadata. Always include the active session, every VP
|
|
9
10
|
* that spoke as an assistant in the diff, and `user` (so painted-over
|
|
10
11
|
* user-profile signals can't be missed). (Feature scope was dropped
|
|
11
12
|
* 2026-05-13 along with the rest of the Feature system.)
|
|
@@ -35,7 +36,6 @@
|
|
|
35
36
|
* below.)
|
|
36
37
|
*/
|
|
37
38
|
|
|
38
|
-
import { isValidTopic } from '../memory/store.js';
|
|
39
39
|
import { truncateMessage } from './segment.js';
|
|
40
40
|
import { render } from './prompts/index.js';
|
|
41
41
|
|
|
@@ -50,8 +50,8 @@ function triageSystem(language) {
|
|
|
50
50
|
* structure of the diff.
|
|
51
51
|
*
|
|
52
52
|
* Inputs:
|
|
53
|
-
* - sessionId: the active
|
|
54
|
-
* `
|
|
53
|
+
* - sessionId: the active session ('_no-session' is allowed and skips the
|
|
54
|
+
* `sessions/<id>` entry — by convention the virtual session has no scope
|
|
55
55
|
* of its own).
|
|
56
56
|
* - messages: the diff (already overlap-prefixed if applicable).
|
|
57
57
|
*
|
|
@@ -65,7 +65,7 @@ export function applyHardRules({ sessionId, chatId, messages }) {
|
|
|
65
65
|
// global user is always in.
|
|
66
66
|
add('user');
|
|
67
67
|
|
|
68
|
-
// chat path takes precedence: chat sessions have no
|
|
68
|
+
// chat path takes precedence: chat sessions have no collaborative session context.
|
|
69
69
|
if (chatId) {
|
|
70
70
|
add(`chat/${chatId}`);
|
|
71
71
|
for (const m of (messages || [])) {
|
|
@@ -80,19 +80,19 @@ export function applyHardRules({ sessionId, chatId, messages }) {
|
|
|
80
80
|
return Array.from(out.values());
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
// active
|
|
83
|
+
// active collaborative session, except the virtual _no-session bucket.
|
|
84
84
|
if (sessionId && sessionId !== '_no-session') {
|
|
85
|
-
add(`
|
|
86
|
-
add(`
|
|
85
|
+
add(`sessions/${sessionId}`);
|
|
86
|
+
add(`sessions/${sessionId}/user`);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
for (const m of (messages || [])) {
|
|
90
90
|
if (!m || typeof m !== 'object') continue;
|
|
91
|
-
// Active VP: any assistant message's vpId
|
|
91
|
+
// Active VP: any assistant message's vpId, isolated inside this session.
|
|
92
92
|
if (m.role === 'assistant') {
|
|
93
93
|
const vp = m.vpId || (m.author && /^vp:(.+)$/.exec(m.author)?.[1]);
|
|
94
94
|
if (vp && /^[A-Za-z0-9_\-.一-鿿]+$/.test(vp) && sessionId && sessionId !== '_no-session') {
|
|
95
|
-
add(`
|
|
95
|
+
add(`sessions/${sessionId}/vp/${vp}`);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
}
|
|
@@ -181,8 +181,8 @@ export async function classifySoft({ sessionId, messages, topicSummaries, llm, l
|
|
|
181
181
|
if (!path) continue;
|
|
182
182
|
const segs = path.split('/').filter(Boolean);
|
|
183
183
|
if (!sessionId || sessionId === '_no-session') continue;
|
|
184
|
-
if (!isValidTopic({ kind: '
|
|
185
|
-
const scope = `
|
|
184
|
+
if (!isValidTopic({ kind: 'session-topic', sessionId, path: segs })) continue;
|
|
185
|
+
const scope = `sessions/${sessionId}/topic/${segs.join('/')}`;
|
|
186
186
|
if (pass2.decision === 'match') {
|
|
187
187
|
out.push({ kind: 'update', scope });
|
|
188
188
|
} else if (pass2.decision === 'new') {
|
package/yeaft/engine.js
CHANGED
|
@@ -251,7 +251,7 @@ export function shouldAllowGroupReflection({
|
|
|
251
251
|
* @param {{
|
|
252
252
|
* sessionId?: string|null,
|
|
253
253
|
* ownVpId?: string|null,
|
|
254
|
-
* summaries: { user?: string,
|
|
254
|
+
* summaries: { user?: string, session?: string, vp?: string }
|
|
255
255
|
* }} args
|
|
256
256
|
* @returns {Array<{scope: string, summary: string}>}
|
|
257
257
|
*/
|
|
@@ -259,15 +259,12 @@ export function buildResidentEntries(args) {
|
|
|
259
259
|
const summaries = (args && args.summaries) || {};
|
|
260
260
|
const out = [];
|
|
261
261
|
if (summaries.user) out.push({ scope: 'user', summary: summaries.user });
|
|
262
|
-
if (args.sessionId && summaries.
|
|
263
|
-
|
|
264
|
-
// though the current disk compatibility path still reads kind:'group'.
|
|
265
|
-
out.push({ scope: `sessions/${args.sessionId}`, summary: summaries.group });
|
|
262
|
+
if (args.sessionId && summaries.session) {
|
|
263
|
+
out.push({ scope: `sessions/${args.sessionId}`, summary: summaries.session });
|
|
266
264
|
}
|
|
267
265
|
// VP per-session isolation (2026-06-09): the VP summary scope MUST be
|
|
268
266
|
// session-qualified. The legacy bare `vp/<id>` scope was a structural
|
|
269
|
-
|
|
270
|
-
// (see #loadLayerASummaries, kind:'group-vp'), so labelling it `vp/<id>`
|
|
267
|
+
// (see #loadLayerASummaries, kind:'group-vp'), so labelling it `vp/<id>`
|
|
271
268
|
// in the Resident layer (a) collides with the ACL regex in store
|
|
272
269
|
// (which only recognises `<root>/<sid>/vp/...`) and (b) makes the same
|
|
273
270
|
// VP persona leak across DIFFERENT sessions whenever the AMS rehydrates
|
|
@@ -394,7 +391,7 @@ export class Engine {
|
|
|
394
391
|
* subsequent turns only run on budget pressure or new memory.
|
|
395
392
|
* @type {Map<string, boolean>}
|
|
396
393
|
*/
|
|
397
|
-
#
|
|
394
|
+
#adjustRanBySession = new Map();
|
|
398
395
|
|
|
399
396
|
/** @type {string|null} */
|
|
400
397
|
#abortReason = null;
|
|
@@ -572,30 +569,30 @@ export class Engine {
|
|
|
572
569
|
*
|
|
573
570
|
* Scopes:
|
|
574
571
|
* - user → `user/summary.md` (always attempted)
|
|
575
|
-
* -
|
|
576
|
-
* - vp
|
|
572
|
+
* - session <sid> → `sessions/<sid>/summary.md` (if sessionId)
|
|
573
|
+
* - session-vp → `sessions/<sid>/vp/<vpId>/summary.md` (if vpId)
|
|
577
574
|
*
|
|
578
575
|
* Each fetch is best-effort — missing files / read errors return ''. The
|
|
579
576
|
* dream tick (Phase 6) is what populates these; on a fresh install they
|
|
580
577
|
* all return ''.
|
|
581
578
|
*
|
|
582
579
|
* @param {{sessionId?: string, vpId?: string, language?: string}} ctx
|
|
583
|
-
* @returns {Promise<{user:string,
|
|
580
|
+
* @returns {Promise<{user:string, session:string, vp:string}>}
|
|
584
581
|
*/
|
|
585
582
|
async #loadLayerASummaries({ sessionId, vpId, language } = {}) {
|
|
586
|
-
if (!this.#yeaftDir) return { user: '',
|
|
583
|
+
if (!this.#yeaftDir) return { user: '', session: '', vp: '' };
|
|
587
584
|
const memoryRoot = `${this.#yeaftDir}/memory`;
|
|
588
585
|
const tasks = [
|
|
589
586
|
readScopeSummary({ kind: 'user' }, { root: memoryRoot, language }).catch(() => ''),
|
|
590
587
|
sessionId
|
|
591
|
-
? readScopeSummary({ kind: '
|
|
588
|
+
? readScopeSummary({ kind: 'session', id: sessionId }, { root: memoryRoot, language }).catch(() => '')
|
|
592
589
|
: Promise.resolve(''),
|
|
593
590
|
vpId && sessionId
|
|
594
|
-
? readScopeSummary({ kind: '
|
|
591
|
+
? readScopeSummary({ kind: 'session-vp', sessionId, id: vpId }, { root: memoryRoot, language }).catch(() => '')
|
|
595
592
|
: Promise.resolve(''),
|
|
596
593
|
];
|
|
597
|
-
const [user,
|
|
598
|
-
return { user: user || '',
|
|
594
|
+
const [user, session, vp] = await Promise.all(tasks);
|
|
595
|
+
return { user: user || '', session: session || '', vp: vp || '' };
|
|
599
596
|
}
|
|
600
597
|
|
|
601
598
|
/**
|
|
@@ -605,12 +602,12 @@ export class Engine {
|
|
|
605
602
|
* @param {{
|
|
606
603
|
* sessionId?: string,
|
|
607
604
|
* ownVpId?: string|null,
|
|
608
|
-
* summaries: { user?: string,
|
|
605
|
+
* summaries: { user?: string, session?: string, vp?: string },
|
|
609
606
|
* recallEntries: object[],
|
|
610
607
|
* }} args
|
|
611
608
|
* @returns {{
|
|
612
609
|
* ams: import('./memory/ams.js').ActiveMemorySet,
|
|
613
|
-
*
|
|
610
|
+
* sessionKey: string,
|
|
614
611
|
* ownVpId: string|null,
|
|
615
612
|
* scopes: string[],
|
|
616
613
|
* snapshotBlock: string,
|
|
@@ -619,17 +616,17 @@ export class Engine {
|
|
|
619
616
|
*/
|
|
620
617
|
#prepareAms(args) {
|
|
621
618
|
if (!this.#amsRegistry) return null;
|
|
622
|
-
const
|
|
619
|
+
const sessionKey = args.sessionId || 'default';
|
|
623
620
|
const ownVpId = args.ownVpId || null;
|
|
624
|
-
const ams = this.#amsRegistry.getOrCreate(
|
|
621
|
+
const ams = this.#amsRegistry.getOrCreate(sessionKey, { ownVpId });
|
|
625
622
|
|
|
626
|
-
// Prime #
|
|
623
|
+
// Prime #adjustRanBySession from disk-hydrated state on first access:
|
|
627
624
|
// a reactivated group resumes with whatever adjustRanThisSession bit
|
|
628
625
|
// it had on disconnect, so we don't burn a fresh adjust on every
|
|
629
626
|
// reload. Once set true in this session we never clear it.
|
|
630
|
-
if (!this.#
|
|
631
|
-
&& this.#amsRegistry.adjustRanThisSession(
|
|
632
|
-
this.#
|
|
627
|
+
if (!this.#adjustRanBySession.has(sessionKey)
|
|
628
|
+
&& this.#amsRegistry.adjustRanThisSession(sessionKey)) {
|
|
629
|
+
this.#adjustRanBySession.set(sessionKey, true);
|
|
633
630
|
}
|
|
634
631
|
|
|
635
632
|
// (a) Resident: rebuild from the same scope summaries the worker
|
|
@@ -653,7 +650,7 @@ export class Engine {
|
|
|
653
650
|
vpId: ownVpId,
|
|
654
651
|
});
|
|
655
652
|
|
|
656
|
-
return { ams,
|
|
653
|
+
return { ams, sessionKey, ownVpId, scopes, snapshotBlock, residentEntries };
|
|
657
654
|
}
|
|
658
655
|
|
|
659
656
|
/**
|
|
@@ -708,7 +705,7 @@ export class Engine {
|
|
|
708
705
|
* surface as a turn failure.
|
|
709
706
|
*
|
|
710
707
|
* @param {{
|
|
711
|
-
* amsContext: { ams: import('./memory/ams.js').ActiveMemorySet,
|
|
708
|
+
* amsContext: { ams: import('./memory/ams.js').ActiveMemorySet, sessionKey: string, ownVpId: string|null, scopes: string[] }|null,
|
|
712
709
|
* userMsg: string,
|
|
713
710
|
* assistantReply: string,
|
|
714
711
|
* turnTokenUsage: number,
|
|
@@ -721,7 +718,7 @@ export class Engine {
|
|
|
721
718
|
const totalBudget = ctx.ams.budget?.total || 0;
|
|
722
719
|
if (!totalBudget) return null;
|
|
723
720
|
|
|
724
|
-
const adjustRanThisSession = this.#
|
|
721
|
+
const adjustRanThisSession = this.#adjustRanBySession.get(ctx.sessionKey) === true;
|
|
725
722
|
try {
|
|
726
723
|
const result = await runAdjust({
|
|
727
724
|
trigger: {
|
|
@@ -748,12 +745,12 @@ export class Engine {
|
|
|
748
745
|
},
|
|
749
746
|
});
|
|
750
747
|
if (result?.ran) {
|
|
751
|
-
this.#
|
|
748
|
+
this.#adjustRanBySession.set(ctx.sessionKey, true);
|
|
752
749
|
// Always persist when we ran — even with no membership change,
|
|
753
750
|
// the adjustRanThisSession bit is part of the on-disk state we
|
|
754
751
|
// want to preserve.
|
|
755
|
-
this.#amsRegistry.markDirty(ctx.
|
|
756
|
-
this.#amsRegistry.persist(ctx.
|
|
752
|
+
this.#amsRegistry.markDirty(ctx.sessionKey);
|
|
753
|
+
this.#amsRegistry.persist(ctx.sessionKey, {
|
|
757
754
|
adjustRanThisSession: true,
|
|
758
755
|
});
|
|
759
756
|
}
|
|
@@ -2262,7 +2259,7 @@ export class Engine {
|
|
|
2262
2259
|
type: 'memory_adjust',
|
|
2263
2260
|
turnId: queryTurnId,
|
|
2264
2261
|
threadId,
|
|
2265
|
-
|
|
2262
|
+
sessionKey: amsContext.sessionKey,
|
|
2266
2263
|
added: adjustResult.added,
|
|
2267
2264
|
evicted: adjustResult.evicted,
|
|
2268
2265
|
skipped: adjustResult.skipped || 0,
|
package/yeaft/memory/store.js
CHANGED
|
@@ -65,10 +65,13 @@ export const SCOPE_KINDS = Object.freeze([
|
|
|
65
65
|
'chat',
|
|
66
66
|
'chat-vp',
|
|
67
67
|
'session',
|
|
68
|
+
'session-user',
|
|
68
69
|
'session-vp',
|
|
70
|
+
'session-feature',
|
|
71
|
+
'session-topic',
|
|
69
72
|
]);
|
|
70
73
|
|
|
71
|
-
/** @typedef {'user'|'group'|'group-user'|'group-vp'|'group-feature'|'group-topic'|'chat'|'chat-vp'|'session'|'session-vp'} ScopeKind */
|
|
74
|
+
/** @typedef {'user'|'group'|'group-user'|'group-vp'|'group-feature'|'group-topic'|'chat'|'chat-vp'|'session'|'session-user'|'session-vp'|'session-feature'|'session-topic'} ScopeKind */
|
|
72
75
|
|
|
73
76
|
/**
|
|
74
77
|
* @typedef {Object} Scope
|
|
@@ -141,14 +144,36 @@ export function scopeDir(scope) {
|
|
|
141
144
|
case 'session': {
|
|
142
145
|
if (!scope.id) throw new Error('scopeDir: session scope requires id');
|
|
143
146
|
assertSafeSegment(scope.id, 'session.id');
|
|
144
|
-
return `
|
|
147
|
+
return `sessions/${scope.id}`;
|
|
148
|
+
}
|
|
149
|
+
case 'session-user': {
|
|
150
|
+
if (!scope.sessionId) throw new Error('scopeDir: session-user scope requires sessionId');
|
|
151
|
+
assertSafeSegment(scope.sessionId, 'session-user.sessionId');
|
|
152
|
+
return `sessions/${scope.sessionId}/user`;
|
|
145
153
|
}
|
|
146
154
|
case 'session-vp': {
|
|
147
155
|
if (!scope.sessionId) throw new Error('scopeDir: session-vp scope requires sessionId');
|
|
148
156
|
if (!scope.id) throw new Error('scopeDir: session-vp scope requires id');
|
|
149
157
|
assertSafeSegment(scope.sessionId, 'session-vp.sessionId');
|
|
150
158
|
assertSafeSegment(scope.id, 'session-vp.id');
|
|
151
|
-
return `
|
|
159
|
+
return `sessions/${scope.sessionId}/vp/${scope.id}`;
|
|
160
|
+
}
|
|
161
|
+
case 'session-feature': {
|
|
162
|
+
if (!scope.sessionId) throw new Error('scopeDir: session-feature scope requires sessionId');
|
|
163
|
+
if (!scope.id) throw new Error('scopeDir: session-feature scope requires id');
|
|
164
|
+
assertSafeSegment(scope.sessionId, 'session-feature.sessionId');
|
|
165
|
+
assertSafeSegment(scope.id, 'session-feature.id');
|
|
166
|
+
return `sessions/${scope.sessionId}/feature/${scope.id}`;
|
|
167
|
+
}
|
|
168
|
+
case 'session-topic': {
|
|
169
|
+
if (!scope.sessionId) throw new Error('scopeDir: session-topic scope requires sessionId');
|
|
170
|
+
assertSafeSegment(scope.sessionId, 'session-topic.sessionId');
|
|
171
|
+
const segs = Array.isArray(scope.path) ? scope.path : [];
|
|
172
|
+
if (segs.length === 0 || segs.length > 2) {
|
|
173
|
+
throw new Error('scopeDir: session-topic.path must have 1 or 2 segments');
|
|
174
|
+
}
|
|
175
|
+
for (const seg of segs) assertSafeSegment(seg, 'session-topic.path');
|
|
176
|
+
return `sessions/${scope.sessionId}/topic/${segs.join('/')}`;
|
|
152
177
|
}
|
|
153
178
|
default:
|
|
154
179
|
throw new Error(`scopeDir: unknown kind ${JSON.stringify(scope.kind)}`);
|
|
@@ -189,7 +214,7 @@ function assertSafeSegment(s, ctx) {
|
|
|
189
214
|
* @returns {boolean}
|
|
190
215
|
*/
|
|
191
216
|
export function isValidTopic(scope) {
|
|
192
|
-
if (!scope || scope.kind !== 'group-topic') return false;
|
|
217
|
+
if (!scope || (scope.kind !== 'group-topic' && scope.kind !== 'session-topic')) return false;
|
|
193
218
|
if (!scope.sessionId || typeof scope.sessionId !== 'string') return false;
|
|
194
219
|
if (!Array.isArray(scope.path)) return false;
|
|
195
220
|
if (scope.path.length < 1 || scope.path.length > 2) return false;
|
|
@@ -578,8 +603,8 @@ export async function listScopes(opts = {}) {
|
|
|
578
603
|
}
|
|
579
604
|
}
|
|
580
605
|
|
|
581
|
-
//
|
|
582
|
-
const sessionRoot = join(root, '
|
|
606
|
+
// sessions/<s>/ and sessions/<s>/vp/<v>/
|
|
607
|
+
const sessionRoot = join(root, 'sessions');
|
|
583
608
|
let sessions;
|
|
584
609
|
try { sessions = await fsp.readdir(sessionRoot, { withFileTypes: true }); }
|
|
585
610
|
catch (err) {
|
|
@@ -592,6 +617,8 @@ export async function listScopes(opts = {}) {
|
|
|
592
617
|
if (!isSafeId(sent.name)) continue;
|
|
593
618
|
const s = sent.name;
|
|
594
619
|
out.push({ kind: 'session', id: s });
|
|
620
|
+
if (existsSync(join(sessionRoot, s, 'user'))) out.push({ kind: 'session-user', sessionId: s });
|
|
621
|
+
|
|
595
622
|
const vpDir = join(sessionRoot, s, 'vp');
|
|
596
623
|
let vps;
|
|
597
624
|
try { vps = await fsp.readdir(vpDir, { withFileTypes: true }); }
|
|
@@ -601,6 +628,34 @@ export async function listScopes(opts = {}) {
|
|
|
601
628
|
if (!isSafeId(vent.name)) continue;
|
|
602
629
|
out.push({ kind: 'session-vp', sessionId: s, id: vent.name });
|
|
603
630
|
}
|
|
631
|
+
|
|
632
|
+
const featureDir = join(sessionRoot, s, 'feature');
|
|
633
|
+
let features;
|
|
634
|
+
try { features = await fsp.readdir(featureDir, { withFileTypes: true }); }
|
|
635
|
+
catch { features = []; }
|
|
636
|
+
for (const fent of features) {
|
|
637
|
+
if (!fent.isDirectory()) continue;
|
|
638
|
+
if (!isSafeId(fent.name)) continue;
|
|
639
|
+
out.push({ kind: 'session-feature', sessionId: s, id: fent.name });
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const topicDir = join(sessionRoot, s, 'topic');
|
|
643
|
+
let topics;
|
|
644
|
+
try { topics = await fsp.readdir(topicDir, { withFileTypes: true }); }
|
|
645
|
+
catch { topics = []; }
|
|
646
|
+
for (const tent of topics) {
|
|
647
|
+
if (!tent.isDirectory()) continue;
|
|
648
|
+
if (!isSafeId(tent.name)) continue;
|
|
649
|
+
out.push({ kind: 'session-topic', sessionId: s, path: [tent.name] });
|
|
650
|
+
let subTopics;
|
|
651
|
+
try { subTopics = await fsp.readdir(join(topicDir, tent.name), { withFileTypes: true }); }
|
|
652
|
+
catch { subTopics = []; }
|
|
653
|
+
for (const sub of subTopics) {
|
|
654
|
+
if (!sub.isDirectory()) continue;
|
|
655
|
+
if (!isSafeId(sub.name)) continue;
|
|
656
|
+
out.push({ kind: 'session-topic', sessionId: s, path: [tent.name, sub.name] });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
604
659
|
}
|
|
605
660
|
|
|
606
661
|
return out;
|
|
@@ -83,7 +83,7 @@ export function seedDefaultSession(yeaftDir, spec = {}) {
|
|
|
83
83
|
// root permission failure must NOT break the bootstrap flow.
|
|
84
84
|
try {
|
|
85
85
|
seedSummaryIfMissingSync(
|
|
86
|
-
{ kind: '
|
|
86
|
+
{ kind: 'session', id: DEFAULT_SESSION_ID },
|
|
87
87
|
buildDefaultSessionSeedSummary({ name, roster, defaultVpId }),
|
|
88
88
|
{ root: memoryRoot },
|
|
89
89
|
);
|
|
@@ -497,14 +497,10 @@ export function deleteSession(yeaftDir, sessionId, options = {}) {
|
|
|
497
497
|
rmSync(dir, { recursive: true, force: true });
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
-
// Cascade: drop the
|
|
500
|
+
// Cascade: drop the session memory scope so a recreate with the same id
|
|
501
501
|
// starts clean. Best-effort — never let memory cleanup fail the CRUD op.
|
|
502
|
-
// Runs unconditionally so the idempotent path also clears stale memory.
|
|
503
502
|
try {
|
|
504
503
|
removeScopeDirSync({ kind: 'session', id: sessionId }, { root: memoryRoot });
|
|
505
|
-
// Legacy pre-session memory scopes used memory/group/<id>. Delete both so
|
|
506
|
-
// idempotent removal clears stale summaries for old grp_* sessions too.
|
|
507
|
-
removeScopeDirSync({ kind: 'group', id: sessionId }, { root: memoryRoot });
|
|
508
504
|
} catch (err) {
|
|
509
505
|
console.warn(`[session-crud] failed to remove memory dir for ${sessionId}:`, err?.message || err);
|
|
510
506
|
}
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -205,11 +205,11 @@ function getVpStatusBroker() {
|
|
|
205
205
|
* the target VP's driver instead of being dropped.
|
|
206
206
|
* 2. Each VP thread gets its own Engine (via `vpEngines`) so private state
|
|
207
207
|
* (`#currentAbortCtrl`, `#__queryCounter`, `#pendingT2`,
|
|
208
|
-
* `#abortReason`, `#
|
|
208
|
+
* `#abortReason`, `#adjustRanBySession`, `#execLog`, `#currentThreadId`)
|
|
209
209
|
* does not collide across concurrent VP turns. Engines are keyed by
|
|
210
210
|
* `${sessionId}::${vpId}::${threadId}` rather than vpId alone because
|
|
211
211
|
* Engine cannot serve two concurrent queries safely — even if AMS state
|
|
212
|
-
* partitions correctly by
|
|
212
|
+
* partitions correctly by sessionKey, the non-session-keyed private state
|
|
213
213
|
* would collide if the same VP ran turns in two groups or two threads
|
|
214
214
|
* in parallel.
|
|
215
215
|
*/
|
|
@@ -859,7 +859,7 @@ function isPermissionErrorMsg(msg) {
|
|
|
859
859
|
/**
|
|
860
860
|
* Get-or-create the per-VP Engine. Each VP owns its own Engine instance
|
|
861
861
|
* so private state (`#currentAbortCtrl`, `#__queryCounter`, `#pendingT2`,
|
|
862
|
-
* `#abortReason`, `#
|
|
862
|
+
* `#abortReason`, `#adjustRanBySession`, `#execLog`) doesn't collide when
|
|
863
863
|
* VP-A and VP-B run concurrent turns. All engines share the session's
|
|
864
864
|
* adapter / trace / config / stores so memory recall, conversation
|
|
865
865
|
* persistence, and tool registry remain consistent.
|
|
@@ -1599,16 +1599,7 @@ export function handleYeaftRestoreSession(msg) {
|
|
|
1599
1599
|
|
|
1600
1600
|
export function handleYeaftRenameSession(msg) {
|
|
1601
1601
|
const requestId = msg && msg.requestId;
|
|
1602
|
-
|
|
1603
|
-
// addition to `sessionId`. The contract documented in
|
|
1604
|
-
// `web/stores/sessions.js` header ("Inbound payloads may carry
|
|
1605
|
-
// either sessionId (new) or groupId (legacy); both are accepted,
|
|
1606
|
-
// prefer sessionId") was only honored on web-side reads; the agent
|
|
1607
|
-
// handlers were silently rejecting the older wire shape that today's
|
|
1608
|
-
// SessionSettingsModal still sends. That's what made delete/rename/
|
|
1609
|
-
// archive/update_config/add_member/remove_member/set_default_vp
|
|
1610
|
-
// all throw `not_found` on undefined ids.
|
|
1611
|
-
const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
|
|
1602
|
+
const sessionId = (msg && msg.sessionId) || null;
|
|
1612
1603
|
const name = msg && msg.name;
|
|
1613
1604
|
try {
|
|
1614
1605
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
@@ -1638,7 +1629,7 @@ export function handleYeaftRenameSession(msg) {
|
|
|
1638
1629
|
export function handleYeaftUpdateSession(msg) {
|
|
1639
1630
|
const requestId = msg && msg.requestId;
|
|
1640
1631
|
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1641
|
-
const sessionId = (msg &&
|
|
1632
|
+
const sessionId = (msg && msg.sessionId) || null;
|
|
1642
1633
|
const patch = (msg && msg.patch && typeof msg.patch === 'object') ? msg.patch : null;
|
|
1643
1634
|
try {
|
|
1644
1635
|
const hasName = patch && typeof patch.name === 'string' && patch.name.trim().length > 0;
|
|
@@ -1673,7 +1664,7 @@ export function handleYeaftUpdateSession(msg) {
|
|
|
1673
1664
|
export function handleYeaftUpdateSessionConfig(msg) {
|
|
1674
1665
|
const requestId = msg && msg.requestId;
|
|
1675
1666
|
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1676
|
-
const sessionId = (msg &&
|
|
1667
|
+
const sessionId = (msg && msg.sessionId) || null;
|
|
1677
1668
|
const partial = (msg && msg.config && typeof msg.config === 'object') ? msg.config : null;
|
|
1678
1669
|
try {
|
|
1679
1670
|
if (!sessionId) throw new SessionConfigError('missing_group_id', 'sessionId required');
|
|
@@ -1696,7 +1687,7 @@ export function handleYeaftUpdateSessionConfig(msg) {
|
|
|
1696
1687
|
export function handleYeaftArchiveSession(msg) {
|
|
1697
1688
|
const requestId = msg && msg.requestId;
|
|
1698
1689
|
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1699
|
-
const sessionId = (msg &&
|
|
1690
|
+
const sessionId = (msg && msg.sessionId) || null;
|
|
1700
1691
|
try {
|
|
1701
1692
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1702
1693
|
const result = archiveSession(yeaftDir, sessionId);
|
|
@@ -1717,7 +1708,7 @@ export function handleYeaftArchiveSession(msg) {
|
|
|
1717
1708
|
export function handleYeaftDeleteSession(msg) {
|
|
1718
1709
|
const requestId = msg && msg.requestId;
|
|
1719
1710
|
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1720
|
-
const sessionId = (msg &&
|
|
1711
|
+
const sessionId = (msg && msg.sessionId) || null;
|
|
1721
1712
|
try {
|
|
1722
1713
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1723
1714
|
const result = deleteSession(yeaftDir, sessionId);
|
|
@@ -1759,7 +1750,7 @@ export function handleYeaftDeleteSession(msg) {
|
|
|
1759
1750
|
export function handleYeaftSessionAddMember(msg) {
|
|
1760
1751
|
const requestId = msg && msg.requestId;
|
|
1761
1752
|
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1762
|
-
const sessionId = (msg &&
|
|
1753
|
+
const sessionId = (msg && msg.sessionId) || null;
|
|
1763
1754
|
const vpId = msg && msg.vpId;
|
|
1764
1755
|
try {
|
|
1765
1756
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
@@ -1775,7 +1766,7 @@ export function handleYeaftSessionAddMember(msg) {
|
|
|
1775
1766
|
export function handleYeaftSessionRemoveMember(msg) {
|
|
1776
1767
|
const requestId = msg && msg.requestId;
|
|
1777
1768
|
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1778
|
-
const sessionId = (msg &&
|
|
1769
|
+
const sessionId = (msg && msg.sessionId) || null;
|
|
1779
1770
|
const vpId = msg && msg.vpId;
|
|
1780
1771
|
try {
|
|
1781
1772
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
@@ -1797,7 +1788,7 @@ export function handleYeaftSessionRemoveMember(msg) {
|
|
|
1797
1788
|
export function handleYeaftSessionSetDefaultVp(msg) {
|
|
1798
1789
|
const requestId = msg && msg.requestId;
|
|
1799
1790
|
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1800
|
-
const sessionId = (msg &&
|
|
1791
|
+
const sessionId = (msg && msg.sessionId) || null;
|
|
1801
1792
|
const vpId = msg && msg.vpId;
|
|
1802
1793
|
try {
|
|
1803
1794
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
@@ -1885,11 +1876,11 @@ export function installYeaftRuntimeBridge(s) {
|
|
|
1885
1876
|
s._dreamResultSink = async (result = {}) => {
|
|
1886
1877
|
if (result?.trigger !== 'auto') return;
|
|
1887
1878
|
const normalized = normalizeDreamResult(result);
|
|
1888
|
-
const processed = Array.isArray(result.
|
|
1889
|
-
? result.
|
|
1879
|
+
const processed = Array.isArray(result.sessions)
|
|
1880
|
+
? result.sessions.filter(row => row && row.status === 'triaged' && row.sessionId)
|
|
1890
1881
|
: [];
|
|
1891
|
-
for (const
|
|
1892
|
-
const sessionId =
|
|
1882
|
+
for (const sessionRow of processed) {
|
|
1883
|
+
const sessionId = sessionRow.sessionId;
|
|
1893
1884
|
const snapshot = await buildDreamOutputSnapshot(session, sessionId).catch(() => null);
|
|
1894
1885
|
sendToServer({
|
|
1895
1886
|
type: 'yeaft_dream_result',
|
|
@@ -2230,7 +2221,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
2230
2221
|
sendYeaftEvent({
|
|
2231
2222
|
type: 'memory_adjust',
|
|
2232
2223
|
turnId: event.turnId,
|
|
2233
|
-
|
|
2224
|
+
sessionKey: event.sessionKey,
|
|
2234
2225
|
added: event.added,
|
|
2235
2226
|
evicted: event.evicted,
|
|
2236
2227
|
skipped: event.skipped,
|
|
@@ -2902,7 +2893,7 @@ async function raceWithEscalation(inner, { deadlineMs, onEscalate }) {
|
|
|
2902
2893
|
* itself; the persistent coord lives in `sessionContexts[sessionId]`. Uses
|
|
2903
2894
|
* `getOrCreateVpEngine(sessionId, vpId)` so each VP runs against its own
|
|
2904
2895
|
* Engine instance — private state (`#currentAbortCtrl`, `#__queryCounter`,
|
|
2905
|
-
* `#pendingT2`, `#abortReason`, `#
|
|
2896
|
+
* `#pendingT2`, `#abortReason`, `#adjustRanBySession`, `#execLog`) does not
|
|
2906
2897
|
* collide when VP-A and VP-B run concurrent turns.
|
|
2907
2898
|
*
|
|
2908
2899
|
* @param {{ prompt: string, sessionId: string, vpId: string, turnId: string, envelope: object, vpAbort: AbortController, baseSnapshot: Array }} args
|
|
@@ -3523,28 +3514,28 @@ export function __testAppendTurnToSessionHistory(...args) {
|
|
|
3523
3514
|
* { type: 'yeaft_dream_trigger', sessionId } — per-GROUP trigger (new
|
|
3524
3515
|
* in v0.1.754 — added so users can manually kick dream for a group
|
|
3525
3516
|
* after seeing the Resident layer stuck on the bootstrap seed).
|
|
3526
|
-
* Fires a scope-filtered pass via `triggerDreamForScopes(['
|
|
3517
|
+
* Fires a scope-filtered pass via `triggerDreamForScopes(['sessions/X'])`
|
|
3527
3518
|
* so unrelated groups don't get processed; the result event is
|
|
3528
|
-
* tagged with `sessionId` for the per-
|
|
3519
|
+
* tagged with `sessionId` for the per-session UI row.
|
|
3529
3520
|
*
|
|
3530
3521
|
* Backwards-compat: when neither field is set, defaults to `vpId='default'`
|
|
3531
3522
|
* which matches the pre-v0.1.754 behavior.
|
|
3532
3523
|
*/
|
|
3533
3524
|
export function normalizeDreamResult(result) {
|
|
3534
|
-
const
|
|
3525
|
+
const sessions = Array.isArray(result?.sessions) ? result.sessions : [];
|
|
3535
3526
|
const targets = Array.isArray(result?.targets) ? result.targets : [];
|
|
3536
|
-
const
|
|
3537
|
-
const
|
|
3538
|
-
const
|
|
3527
|
+
const sessionsProcessed = sessions.filter(g => g && g.status === 'triaged').length;
|
|
3528
|
+
const skippedSessions = sessions.filter(g => g && g.status === 'skipped');
|
|
3529
|
+
const sessionsSkipped = skippedSessions.length;
|
|
3539
3530
|
const targetsApplied = targets.filter(t => t && t.status === 'done').length;
|
|
3540
3531
|
const targetErrors = targets
|
|
3541
3532
|
.filter(t => t && t.status === 'error')
|
|
3542
3533
|
.map(t => ({ target: t.target || null, error: t.error || 'unknown' }));
|
|
3543
3534
|
const hardError = result?.error || null;
|
|
3544
3535
|
const explicitSkipped = result?.skipped === true;
|
|
3545
|
-
const skipped = !hardError && (explicitSkipped || (
|
|
3536
|
+
const skipped = !hardError && (explicitSkipped || (sessionsProcessed === 0 && targetsApplied === 0));
|
|
3546
3537
|
const skippedReason = skipped
|
|
3547
|
-
? (result?.skippedReason ||
|
|
3538
|
+
? (result?.skippedReason || skippedSessions[0]?.reason || 'no-targets-applied')
|
|
3548
3539
|
: null;
|
|
3549
3540
|
const trigger = result?.trigger || null;
|
|
3550
3541
|
const success = !hardError && targetErrors.length === 0 && !skipped && targetsApplied > 0;
|
|
@@ -3560,8 +3551,8 @@ export function normalizeDreamResult(result) {
|
|
|
3560
3551
|
passBreakdown: result?.passBreakdown || result?.metrics?.passBreakdown || null,
|
|
3561
3552
|
skipped,
|
|
3562
3553
|
skippedReason,
|
|
3563
|
-
|
|
3564
|
-
|
|
3554
|
+
sessionsProcessed,
|
|
3555
|
+
sessionsSkipped,
|
|
3565
3556
|
targetsApplied,
|
|
3566
3557
|
targetErrors,
|
|
3567
3558
|
entriesCreated: targetsApplied,
|
|
@@ -3648,7 +3639,7 @@ export async function handleYeaftDreamTrigger(msg = {}) {
|
|
|
3648
3639
|
});
|
|
3649
3640
|
|
|
3650
3641
|
const result = sessionId
|
|
3651
|
-
? await session.dreamScheduler.triggerDreamForScopes([`
|
|
3642
|
+
? await session.dreamScheduler.triggerDreamForScopes([`sessions/${sessionId}`])
|
|
3652
3643
|
: await session.dreamScheduler.triggerDreamNow();
|
|
3653
3644
|
|
|
3654
3645
|
const normalized = normalizeDreamResult(result);
|
|
@@ -3657,7 +3648,7 @@ export async function handleYeaftDreamTrigger(msg = {}) {
|
|
|
3657
3648
|
: null;
|
|
3658
3649
|
|
|
3659
3650
|
// Spread `result` FIRST so normalized fields (success, skipped,
|
|
3660
|
-
// skippedReason,
|
|
3651
|
+
// skippedReason, sessionsProcessed, sessionsSkipped, targetsApplied,
|
|
3661
3652
|
// targetErrors, entriesCreated, lastDreamAt) authoritatively shadow
|
|
3662
3653
|
// anything the runner might grow
|
|
3663
3654
|
// with the same name. Today there is no collision (runner.js returns
|