@yeaft/webchat-agent 0.1.779 → 0.1.781
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/unify/dream-v2/apply.js +2 -2
- package/unify/dream-v2/runner.js +30 -7
- package/unify/engine.js +21 -11
- package/unify/memory/store-v2.js +28 -13
- package/unify/session.js +45 -2
- package/unify/web-bridge.js +44 -20
package/package.json
CHANGED
package/unify/dream-v2/apply.js
CHANGED
|
@@ -163,7 +163,7 @@ export async function applyMergedTarget(merged, opts) {
|
|
|
163
163
|
await snapFn(opts.root, ts, scopeDirRel);
|
|
164
164
|
|
|
165
165
|
let memoryMd = await readMemory(scope, { root: opts.root });
|
|
166
|
-
let summaryMd = await readSummary(scope, { root: opts.root });
|
|
166
|
+
let summaryMd = await readSummary(scope, { root: opts.root, language: opts.language });
|
|
167
167
|
|
|
168
168
|
if (merged.kind === 'create' && (memoryMd || summaryMd)) {
|
|
169
169
|
// Race / partial state: the scope already exists. Treat as update —
|
|
@@ -226,7 +226,7 @@ export async function applyMergedTarget(merged, opts) {
|
|
|
226
226
|
// Stamp the per-scope dream marker, then atomically write both files.
|
|
227
227
|
const stamped = withDreamMarker(memoryMd, { lastDreamAt: nowIso });
|
|
228
228
|
await writeMemory(scope, stamped, { root: opts.root });
|
|
229
|
-
await writeSummary(scope, summaryMd || '', { root: opts.root });
|
|
229
|
+
await writeSummary(scope, summaryMd || '', { root: opts.root, language: opts.language });
|
|
230
230
|
|
|
231
231
|
if (opts.onProgress) opts.onProgress({ phase: 'apply', target: merged.target, status: 'done', batches: batchesUsed });
|
|
232
232
|
return { target: merged.target, kind: merged.kind, batches: batchesUsed };
|
package/unify/dream-v2/runner.js
CHANGED
|
@@ -88,6 +88,7 @@ export async function runDream(opts) {
|
|
|
88
88
|
// 1. enumerate groups
|
|
89
89
|
const groupIds = await safeCall(opts.listGroups, []);
|
|
90
90
|
const filter = Array.isArray(opts.scopeFilter) ? new Set(opts.scopeFilter) : null;
|
|
91
|
+
const groupFilter = deriveGroupFilter(filter);
|
|
91
92
|
const groupsReport = [];
|
|
92
93
|
const groupTriages = [];
|
|
93
94
|
const processedGroups = [];
|
|
@@ -95,12 +96,18 @@ export async function runDream(opts) {
|
|
|
95
96
|
// 2. per-group: skip / segment / triage
|
|
96
97
|
const topicSummaries = opts.listTopicSummaries
|
|
97
98
|
? await safeCall(opts.listTopicSummaries, [])
|
|
98
|
-
: await defaultListTopicSummaries(opts.root).catch(() => []);
|
|
99
|
+
: await defaultListTopicSummaries(opts.root, opts.language).catch(() => []);
|
|
99
100
|
|
|
100
101
|
for (const groupId of groupIds) {
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
102
|
+
// Current-group manual dream passes are the one case where scopeFilter
|
|
103
|
+
// must constrain enumeration too: clicking the conversation header means
|
|
104
|
+
// "dream this group now", not "triage every group and then only apply
|
|
105
|
+
// group/<id>". Pure target filters such as ['user'] still triage every
|
|
106
|
+
// group so their hard-rule actions can contribute to the requested scope.
|
|
107
|
+
if (groupFilter && !groupFilter.has(groupId)) {
|
|
108
|
+
groupsReport.push({ groupId, new: 0, status: 'skipped', reason: 'scope-filtered' });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
104
111
|
const state = await readGroupState(opts.root, groupId);
|
|
105
112
|
const beforeCount = await safeCall(() => opts.countMessages(groupId), 0);
|
|
106
113
|
const newCount = Math.max(0, beforeCount - (state.messageCount || 0));
|
|
@@ -170,7 +177,7 @@ export async function runDream(opts) {
|
|
|
170
177
|
// 3. merge
|
|
171
178
|
const mergedTargets = mergeByTarget(groupTriages);
|
|
172
179
|
const targetsToApply = filter && filter.size > 0 && !filter.has('*')
|
|
173
|
-
? mergedTargets.filter(t => filter.has(t.target))
|
|
180
|
+
? mergedTargets.filter(t => filter.has(t.target) || filter.has(`group/${sourceGroupId(t)}`))
|
|
174
181
|
: mergedTargets;
|
|
175
182
|
|
|
176
183
|
onProgress({ phase: 'merge', targets: targetsToApply.length });
|
|
@@ -263,6 +270,22 @@ function lastMessageId(messages) {
|
|
|
263
270
|
return null;
|
|
264
271
|
}
|
|
265
272
|
|
|
273
|
+
function deriveGroupFilter(filter) {
|
|
274
|
+
if (!filter || filter.size === 0 || filter.has('*')) return null;
|
|
275
|
+
const groups = [];
|
|
276
|
+
for (const scope of filter) {
|
|
277
|
+
if (typeof scope !== 'string') continue;
|
|
278
|
+
const m = /^group\/([^/]+)$/.exec(scope);
|
|
279
|
+
if (m && m[1]) groups.push(m[1]);
|
|
280
|
+
}
|
|
281
|
+
return groups.length > 0 ? new Set(groups) : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function sourceGroupId(mergedTarget) {
|
|
285
|
+
const src = Array.isArray(mergedTarget?.sources) ? mergedTarget.sources[0] : null;
|
|
286
|
+
return src && typeof src.groupId === 'string' ? src.groupId : '';
|
|
287
|
+
}
|
|
288
|
+
|
|
266
289
|
async function safeCall(fn, fallback) {
|
|
267
290
|
try {
|
|
268
291
|
if (typeof fn !== 'function') return fallback;
|
|
@@ -273,12 +296,12 @@ async function safeCall(fn, fallback) {
|
|
|
273
296
|
}
|
|
274
297
|
}
|
|
275
298
|
|
|
276
|
-
async function defaultListTopicSummaries(root) {
|
|
299
|
+
async function defaultListTopicSummaries(root, language) {
|
|
277
300
|
const all = await listScopes({ root });
|
|
278
301
|
const out = [];
|
|
279
302
|
for (const sc of all) {
|
|
280
303
|
if (sc.kind !== 'topic') continue;
|
|
281
|
-
const summary = await readSummary(sc, { root });
|
|
304
|
+
const summary = await readSummary(sc, { root, language });
|
|
282
305
|
out.push({ path: sc.path.join('/'), summary });
|
|
283
306
|
}
|
|
284
307
|
return out;
|
package/unify/engine.js
CHANGED
|
@@ -216,6 +216,10 @@ export function buildResidentEntries(args) {
|
|
|
216
216
|
return out;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
function isZhRuntimeLanguage(language) {
|
|
220
|
+
return String(language || '').toLowerCase().startsWith('zh');
|
|
221
|
+
}
|
|
222
|
+
|
|
219
223
|
export class Engine {
|
|
220
224
|
/** @type {import('./llm/adapter.js').LLMAdapter} */
|
|
221
225
|
#adapter;
|
|
@@ -478,19 +482,19 @@ export class Engine {
|
|
|
478
482
|
* dream tick (Phase 6) is what populates these; on a fresh install they
|
|
479
483
|
* all return ''.
|
|
480
484
|
*
|
|
481
|
-
* @param {{groupId?: string, vpId?: string}} ctx
|
|
485
|
+
* @param {{groupId?: string, vpId?: string, language?: string}} ctx
|
|
482
486
|
* @returns {Promise<{user:string, group:string, vp:string}>}
|
|
483
487
|
*/
|
|
484
|
-
async #loadLayerASummaries({ groupId, vpId } = {}) {
|
|
488
|
+
async #loadLayerASummaries({ groupId, vpId, language } = {}) {
|
|
485
489
|
if (!this.#yeaftDir) return { user: '', group: '', vp: '' };
|
|
486
490
|
const memoryRoot = `${this.#yeaftDir}/memory`;
|
|
487
491
|
const tasks = [
|
|
488
|
-
readScopeSummary({ kind: 'user' }, { root: memoryRoot }).catch(() => ''),
|
|
492
|
+
readScopeSummary({ kind: 'user' }, { root: memoryRoot, language }).catch(() => ''),
|
|
489
493
|
groupId
|
|
490
|
-
? readScopeSummary({ kind: 'group', id: groupId }, { root: memoryRoot }).catch(() => '')
|
|
494
|
+
? readScopeSummary({ kind: 'group', id: groupId }, { root: memoryRoot, language }).catch(() => '')
|
|
491
495
|
: Promise.resolve(''),
|
|
492
496
|
vpId
|
|
493
|
-
? readScopeSummary({ kind: 'vp', id: vpId }, { root: memoryRoot }).catch(() => '')
|
|
497
|
+
? readScopeSummary({ kind: 'vp', id: vpId }, { root: memoryRoot, language }).catch(() => '')
|
|
494
498
|
: Promise.resolve(''),
|
|
495
499
|
];
|
|
496
500
|
const [user, group, vp] = await Promise.all(tasks);
|
|
@@ -544,7 +548,7 @@ export class Engine {
|
|
|
544
548
|
ams.setOnDemand(segs);
|
|
545
549
|
|
|
546
550
|
// (c) Snapshot — render the AMS layers as a single prompt block.
|
|
547
|
-
const snapshotBlock = this.#renderAmsSnapshot(ams);
|
|
551
|
+
const snapshotBlock = this.#renderAmsSnapshot(ams, this.#config.language || 'en');
|
|
548
552
|
|
|
549
553
|
const scopes = buildRelevantScopes({
|
|
550
554
|
groupId: args.groupId,
|
|
@@ -560,30 +564,35 @@ export class Engine {
|
|
|
560
564
|
* so the LLM sees a consistent layout.
|
|
561
565
|
*
|
|
562
566
|
* @param {import('./memory/ams.js').ActiveMemorySet} ams
|
|
567
|
+
* @param {string} [language]
|
|
563
568
|
* @returns {string}
|
|
564
569
|
*/
|
|
565
|
-
#renderAmsSnapshot(ams) {
|
|
570
|
+
#renderAmsSnapshot(ams, language = 'en') {
|
|
566
571
|
const snap = ams.snapshot();
|
|
567
572
|
if (!snap) return '';
|
|
568
573
|
const parts = [];
|
|
569
574
|
if (snap.resident.length === 0 && snap.recent.length === 0 && snap.onDemand.length === 0) {
|
|
570
575
|
return '';
|
|
571
576
|
}
|
|
572
|
-
|
|
577
|
+
const zh = isZhRuntimeLanguage(language);
|
|
578
|
+
parts.push(zh ? '## 活跃记忆集' : '## Active Memory Set');
|
|
579
|
+
parts.push(zh
|
|
580
|
+
? '以下记忆按当前用户语言呈现;如果个别历史摘要仍是其他语言,请只把它当作事实来源,回答和新增记忆应使用中文。'
|
|
581
|
+
: 'Memory is presented for the current user language; if an older summary is in another language, treat it as factual context and continue in English.');
|
|
573
582
|
if (snap.resident.length > 0) {
|
|
574
|
-
parts.push('### Resident');
|
|
583
|
+
parts.push(zh ? '### 常驻记忆' : '### Resident');
|
|
575
584
|
for (const r of snap.resident) {
|
|
576
585
|
parts.push(`- **${r.scope}**: ${r.summary}`);
|
|
577
586
|
}
|
|
578
587
|
}
|
|
579
588
|
if (snap.recent.length > 0) {
|
|
580
|
-
parts.push('### Recent');
|
|
589
|
+
parts.push(zh ? '### 最近记忆' : '### Recent');
|
|
581
590
|
for (const s of snap.recent) {
|
|
582
591
|
parts.push(`- (${s.scope}) ${(s.body || '').trim()}`);
|
|
583
592
|
}
|
|
584
593
|
}
|
|
585
594
|
if (snap.onDemand.length > 0) {
|
|
586
|
-
parts.push('### OnDemand');
|
|
595
|
+
parts.push(zh ? '### 按需记忆' : '### OnDemand');
|
|
587
596
|
for (const s of snap.onDemand) {
|
|
588
597
|
parts.push(`- (${s.scope}) ${(s.body || '').trim()}`);
|
|
589
598
|
}
|
|
@@ -1140,6 +1149,7 @@ export class Engine {
|
|
|
1140
1149
|
vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
|
|
1141
1150
|
? vpPersona.vpId
|
|
1142
1151
|
: (typeof senderVpId === 'string' ? senderVpId : undefined),
|
|
1152
|
+
language: this.#config.language || 'en',
|
|
1143
1153
|
});
|
|
1144
1154
|
|
|
1145
1155
|
// ─── AMS: populate + snapshot ───────────────────────────────
|
package/unify/memory/store-v2.js
CHANGED
|
@@ -197,7 +197,7 @@ async function atomicWrite(absPath, content) {
|
|
|
197
197
|
* Read a scope's memory.md. Missing → empty string.
|
|
198
198
|
*
|
|
199
199
|
* @param {Scope} scope
|
|
200
|
-
* @param {{ root?: string, currentVpId?: string }} [opts]
|
|
200
|
+
* @param {{ root?: string, currentVpId?: string, language?: string }} [opts]
|
|
201
201
|
* @returns {Promise<string>}
|
|
202
202
|
*/
|
|
203
203
|
export async function readMemory(scope, opts = {}) {
|
|
@@ -254,23 +254,38 @@ export async function appendMemory(scope, chunk, opts = {}) {
|
|
|
254
254
|
|
|
255
255
|
// ─── summary.md ────────────────────────────────────────────────
|
|
256
256
|
|
|
257
|
+
|
|
258
|
+
function summaryFileName(language) {
|
|
259
|
+
const normalized = String(language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
260
|
+
return normalized === 'zh' ? 'summary.zh.md' : 'summary.md';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function summaryCandidateRels(scope, language) {
|
|
264
|
+
const dir = scopeDir(scope);
|
|
265
|
+
const primary = `${dir}/${summaryFileName(language)}`;
|
|
266
|
+
const fallback = `${dir}/summary.md`;
|
|
267
|
+
return primary === fallback ? [fallback] : [primary, fallback];
|
|
268
|
+
}
|
|
269
|
+
|
|
257
270
|
/**
|
|
258
271
|
* Read a scope's summary.md (trimmed). Missing → empty string.
|
|
259
272
|
*
|
|
260
273
|
* @param {Scope} scope
|
|
261
|
-
* @param {{ root?: string, currentVpId?: string }} [opts]
|
|
274
|
+
* @param {{ root?: string, currentVpId?: string, language?: string }} [opts]
|
|
262
275
|
* @returns {Promise<string>}
|
|
263
276
|
*/
|
|
264
277
|
export async function readSummary(scope, opts = {}) {
|
|
265
|
-
const { root = DEFAULT_MEMORY_ROOT, currentVpId } = opts;
|
|
266
|
-
const rel
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
278
|
+
const { root = DEFAULT_MEMORY_ROOT, currentVpId, language } = opts;
|
|
279
|
+
for (const rel of summaryCandidateRels(scope, language)) {
|
|
280
|
+
enforceVpAcl(rel, currentVpId);
|
|
281
|
+
const abs = join(root, rel);
|
|
282
|
+
try { return (await fsp.readFile(abs, 'utf8')).trim(); }
|
|
283
|
+
catch (err) {
|
|
284
|
+
if (err && err.code === 'ENOENT') continue;
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
273
287
|
}
|
|
288
|
+
return '';
|
|
274
289
|
}
|
|
275
290
|
|
|
276
291
|
/**
|
|
@@ -278,11 +293,11 @@ export async function readSummary(scope, opts = {}) {
|
|
|
278
293
|
*
|
|
279
294
|
* @param {Scope} scope
|
|
280
295
|
* @param {string} body
|
|
281
|
-
* @param {{ root?: string, currentVpId?: string }} [opts]
|
|
296
|
+
* @param {{ root?: string, currentVpId?: string, language?: string }} [opts]
|
|
282
297
|
*/
|
|
283
298
|
export async function writeSummary(scope, body, opts = {}) {
|
|
284
|
-
const { root = DEFAULT_MEMORY_ROOT, currentVpId } = opts;
|
|
285
|
-
const rel = `${scopeDir(scope)}
|
|
299
|
+
const { root = DEFAULT_MEMORY_ROOT, currentVpId, language } = opts;
|
|
300
|
+
const rel = `${scopeDir(scope)}/${summaryFileName(language)}`;
|
|
286
301
|
enforceVpAcl(rel, currentVpId);
|
|
287
302
|
const abs = join(root, rel);
|
|
288
303
|
await atomicWrite(abs, `${(body || '').trim()}\n`);
|
package/unify/session.js
CHANGED
|
@@ -50,7 +50,7 @@ import { openSegmentIndex } from './memory/index-db.js';
|
|
|
50
50
|
import { syncAll as syncSegmentIndex } from './memory/segment-sync.js';
|
|
51
51
|
import { openAmsRegistry } from './memory/ams-registry.js';
|
|
52
52
|
import { join } from 'path';
|
|
53
|
-
import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe } from 'fs';
|
|
53
|
+
import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe, mkdirSync as mkdirSyncSafe } from 'fs';
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* @typedef {Object} SessionOptions
|
|
@@ -80,6 +80,33 @@ import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe } from '
|
|
|
80
80
|
* @property {() => Promise<void>} shutdown — Graceful shutdown
|
|
81
81
|
*/
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Eagerly create `<yeaftDir>/stats/` and surface failures as a warn.
|
|
85
|
+
*
|
|
86
|
+
* Returns the resolved path either way — the caller can still pass it
|
|
87
|
+
* to `ToolUsageStats`, which keeps an in-memory counter path even when
|
|
88
|
+
* the disk is read-only.
|
|
89
|
+
*
|
|
90
|
+
* NOTE: this knowledge ("stats lives under `stats/`") belongs inside
|
|
91
|
+
* `ToolUsageStats.init()`. Once that exists, delete this helper and
|
|
92
|
+
* the call site collapses to `await toolStats.init(yeaftDir)`.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} yeaftDir
|
|
95
|
+
* @returns {string} statsDir
|
|
96
|
+
*/
|
|
97
|
+
function prepareToolStatsDir(yeaftDir) {
|
|
98
|
+
const statsDir = join(yeaftDir, 'stats');
|
|
99
|
+
try {
|
|
100
|
+
mkdirSyncSafe(statsDir, { recursive: true });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.warn(
|
|
103
|
+
`[Unify] Could not create stats dir ${statsDir}: ${err?.message || err}. ` +
|
|
104
|
+
`Tool-usage counters will live in memory only.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return statsDir;
|
|
108
|
+
}
|
|
109
|
+
|
|
83
110
|
/**
|
|
84
111
|
* Load (or initialize) a Yeaft session.
|
|
85
112
|
*
|
|
@@ -320,8 +347,24 @@ export async function loadSession(options = {}) {
|
|
|
320
347
|
// Tool-call usage statistics: persisted to <yeaftDir>/stats/tool-usage.json.
|
|
321
348
|
// Loaded synchronously at boot so the first turn already sees prior counts.
|
|
322
349
|
// Threaded into the engine so it can `record` each tool_exec event.
|
|
350
|
+
//
|
|
351
|
+
// 2026-05-16: eagerly create the `stats/` directory at boot. The
|
|
352
|
+
// ToolUsageStats writer does `fsp.mkdir(..., {recursive:true})` lazily
|
|
353
|
+
// inside `#doFlush()` and swallows any mkdir error, which meant an
|
|
354
|
+
// unwritable parent (perm denied, ENOSPC) was silently invisible
|
|
355
|
+
// until the user filed a support ticket. Doing it here surfaces the
|
|
356
|
+
// failure as a console.warn while still leaving the in-memory
|
|
357
|
+
// counter path functional — the engine keeps recording even if the
|
|
358
|
+
// disk is read-only.
|
|
359
|
+
//
|
|
360
|
+
// FOLLOW-UP: this leaks `ToolUsageStats`'s storage layout (its
|
|
361
|
+
// directory name) into the session orchestrator. The right home is
|
|
362
|
+
// a `ToolUsageStats.init()` that owns the mkdir + the warn + a
|
|
363
|
+
// `writesDisabled` flag. Tracking as future work; for now the helper
|
|
364
|
+
// below visually quarantines the leak so the migration is one delete.
|
|
365
|
+
const statsDir = prepareToolStatsDir(yeaftDir);
|
|
323
366
|
const toolStats = new ToolUsageStats({
|
|
324
|
-
path: join(
|
|
367
|
+
path: join(statsDir, 'tool-usage.json'),
|
|
325
368
|
});
|
|
326
369
|
toolStats.loadSync();
|
|
327
370
|
const engine = new Engine({
|
package/unify/web-bridge.js
CHANGED
|
@@ -2692,6 +2692,37 @@ export const __testRaceWithEscalation = raceWithEscalation;
|
|
|
2692
2692
|
* Backwards-compat: when neither field is set, defaults to `vpId='default'`
|
|
2693
2693
|
* which matches the pre-v0.1.754 behavior.
|
|
2694
2694
|
*/
|
|
2695
|
+
export function normalizeDreamResult(result) {
|
|
2696
|
+
const groups = Array.isArray(result?.groups) ? result.groups : [];
|
|
2697
|
+
const targets = Array.isArray(result?.targets) ? result.targets : [];
|
|
2698
|
+
const groupsProcessed = groups.filter(g => g && g.status === 'triaged').length;
|
|
2699
|
+
const skippedGroups = groups.filter(g => g && g.status === 'skipped');
|
|
2700
|
+
const groupsSkipped = skippedGroups.length;
|
|
2701
|
+
const targetsApplied = targets.filter(t => t && t.status === 'done').length;
|
|
2702
|
+
const targetErrors = targets
|
|
2703
|
+
.filter(t => t && t.status === 'error')
|
|
2704
|
+
.map(t => ({ target: t.target || null, error: t.error || 'unknown' }));
|
|
2705
|
+
const hardError = result?.error || null;
|
|
2706
|
+
const skipped = !hardError && groupsProcessed === 0 && targetsApplied === 0;
|
|
2707
|
+
const skippedReason = skipped
|
|
2708
|
+
? (skippedGroups[0]?.reason || 'no-targets-applied')
|
|
2709
|
+
: null;
|
|
2710
|
+
const success = !hardError && targetErrors.length === 0 && !skipped && targetsApplied > 0;
|
|
2711
|
+
|
|
2712
|
+
return {
|
|
2713
|
+
success,
|
|
2714
|
+
skipped,
|
|
2715
|
+
skippedReason,
|
|
2716
|
+
groupsProcessed,
|
|
2717
|
+
groupsSkipped,
|
|
2718
|
+
targetsApplied,
|
|
2719
|
+
targetErrors,
|
|
2720
|
+
entriesCreated: targetsApplied,
|
|
2721
|
+
lastDreamAt: result?.startedAt || new Date().toISOString(),
|
|
2722
|
+
error: hardError || (targetErrors[0]?.error || null),
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2695
2726
|
export async function handleUnifyDreamTrigger(msg = {}) {
|
|
2696
2727
|
// Resolve tag up-front so EVERY outbound envelope (including the
|
|
2697
2728
|
// scheduler-uninitialised early-return below) carries `groupId` /
|
|
@@ -2704,11 +2735,11 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2704
2735
|
const tag = groupId ? { groupId } : { vpId };
|
|
2705
2736
|
|
|
2706
2737
|
if (!session?.dreamScheduler) {
|
|
2738
|
+
const error = 'Dream scheduler not initialized — session not loaded.';
|
|
2707
2739
|
sendToServer({
|
|
2708
2740
|
type: 'unify_dream_result',
|
|
2709
2741
|
...tag,
|
|
2710
|
-
|
|
2711
|
-
error: 'Dream scheduler not initialized — session not loaded.',
|
|
2742
|
+
...normalizeDreamResult({ error }),
|
|
2712
2743
|
});
|
|
2713
2744
|
return;
|
|
2714
2745
|
}
|
|
@@ -2724,11 +2755,11 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2724
2755
|
// dream-v2/schedule.js inflight reuse), so the user-facing semantics
|
|
2725
2756
|
// are unchanged ("you already asked").
|
|
2726
2757
|
if (groupId && inflightScopedDreamGroups.size > 0) {
|
|
2758
|
+
const error = 'A dream pass is already running.';
|
|
2727
2759
|
sendToServer({
|
|
2728
2760
|
type: 'unify_dream_result',
|
|
2729
2761
|
...tag,
|
|
2730
|
-
|
|
2731
|
-
error: 'A dream pass is already running.',
|
|
2762
|
+
...normalizeDreamResult({ error }),
|
|
2732
2763
|
});
|
|
2733
2764
|
return;
|
|
2734
2765
|
}
|
|
@@ -2764,17 +2795,12 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2764
2795
|
? await session.dreamScheduler.triggerDreamForScopes([`group/${groupId}`])
|
|
2765
2796
|
: await session.dreamScheduler.triggerDreamNow();
|
|
2766
2797
|
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
//
|
|
2770
|
-
//
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
const lastDreamAt = result?.startedAt || new Date().toISOString();
|
|
2774
|
-
const success = !result.error && !result.skipped;
|
|
2775
|
-
|
|
2776
|
-
// Spread `result` FIRST so derived fields (success, entriesCreated,
|
|
2777
|
-
// lastDreamAt) authoritatively shadow anything the runner might grow
|
|
2798
|
+
const normalized = normalizeDreamResult(result);
|
|
2799
|
+
|
|
2800
|
+
// Spread `result` FIRST so normalized fields (success, skipped,
|
|
2801
|
+
// skippedReason, groupsProcessed, groupsSkipped, targetsApplied,
|
|
2802
|
+
// targetErrors, entriesCreated, lastDreamAt) authoritatively shadow
|
|
2803
|
+
// anything the runner might grow
|
|
2778
2804
|
// with the same name. Today there is no collision (runner.js returns
|
|
2779
2805
|
// { groups, targets, startedAt, error?, skipped? }) but the failure
|
|
2780
2806
|
// mode of the alternative ordering is silent — review feedback from
|
|
@@ -2791,16 +2817,14 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2791
2817
|
type: 'unify_dream_result',
|
|
2792
2818
|
...tag,
|
|
2793
2819
|
...result,
|
|
2794
|
-
|
|
2795
|
-
entriesCreated,
|
|
2796
|
-
lastDreamAt,
|
|
2820
|
+
...normalized,
|
|
2797
2821
|
});
|
|
2798
2822
|
} catch (err) {
|
|
2823
|
+
const error = err?.message || String(err);
|
|
2799
2824
|
sendToServer({
|
|
2800
2825
|
type: 'unify_dream_result',
|
|
2801
2826
|
...tag,
|
|
2802
|
-
|
|
2803
|
-
error: err?.message || String(err),
|
|
2827
|
+
...normalizeDreamResult({ error }),
|
|
2804
2828
|
});
|
|
2805
2829
|
} finally {
|
|
2806
2830
|
// Restore the original sink and release the per-group inflight lock.
|