@yeaft/webchat-agent 0.1.778 → 0.1.780
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 +28 -11
- package/unify/eval/runner.js +5 -1
- package/unify/memory/store-v2.js +28 -13
- package/unify/sub-agent/runner.js +6 -0
- package/unify/web-bridge.js +63 -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;
|
|
@@ -326,10 +330,12 @@ export class Engine {
|
|
|
326
330
|
* config: object,
|
|
327
331
|
* conversationStore?: import('./conversation/persist.js').ConversationStore,
|
|
328
332
|
* memoryIndex?: import('./memory/index-db.js').SegmentIndex,
|
|
333
|
+
* amsRegistry?: object,
|
|
329
334
|
* toolRegistry?: import('./tools/registry.js').ToolRegistry,
|
|
330
335
|
* skillManager?: import('./skills.js').SkillManager,
|
|
331
336
|
* mcpManager?: import('./mcp.js').MCPManager,
|
|
332
337
|
* yeaftDir?: string,
|
|
338
|
+
* toolStats?: import('./stats/tool-usage.js').ToolUsageStats,
|
|
333
339
|
* }} params
|
|
334
340
|
*/
|
|
335
341
|
constructor({ adapter, trace, config, conversationStore, memoryIndex, amsRegistry, toolRegistry, skillManager, mcpManager, yeaftDir, toolStats = null }) {
|
|
@@ -476,19 +482,19 @@ export class Engine {
|
|
|
476
482
|
* dream tick (Phase 6) is what populates these; on a fresh install they
|
|
477
483
|
* all return ''.
|
|
478
484
|
*
|
|
479
|
-
* @param {{groupId?: string, vpId?: string}} ctx
|
|
485
|
+
* @param {{groupId?: string, vpId?: string, language?: string}} ctx
|
|
480
486
|
* @returns {Promise<{user:string, group:string, vp:string}>}
|
|
481
487
|
*/
|
|
482
|
-
async #loadLayerASummaries({ groupId, vpId } = {}) {
|
|
488
|
+
async #loadLayerASummaries({ groupId, vpId, language } = {}) {
|
|
483
489
|
if (!this.#yeaftDir) return { user: '', group: '', vp: '' };
|
|
484
490
|
const memoryRoot = `${this.#yeaftDir}/memory`;
|
|
485
491
|
const tasks = [
|
|
486
|
-
readScopeSummary({ kind: 'user' }, { root: memoryRoot }).catch(() => ''),
|
|
492
|
+
readScopeSummary({ kind: 'user' }, { root: memoryRoot, language }).catch(() => ''),
|
|
487
493
|
groupId
|
|
488
|
-
? readScopeSummary({ kind: 'group', id: groupId }, { root: memoryRoot }).catch(() => '')
|
|
494
|
+
? readScopeSummary({ kind: 'group', id: groupId }, { root: memoryRoot, language }).catch(() => '')
|
|
489
495
|
: Promise.resolve(''),
|
|
490
496
|
vpId
|
|
491
|
-
? readScopeSummary({ kind: 'vp', id: vpId }, { root: memoryRoot }).catch(() => '')
|
|
497
|
+
? readScopeSummary({ kind: 'vp', id: vpId }, { root: memoryRoot, language }).catch(() => '')
|
|
492
498
|
: Promise.resolve(''),
|
|
493
499
|
];
|
|
494
500
|
const [user, group, vp] = await Promise.all(tasks);
|
|
@@ -542,7 +548,7 @@ export class Engine {
|
|
|
542
548
|
ams.setOnDemand(segs);
|
|
543
549
|
|
|
544
550
|
// (c) Snapshot — render the AMS layers as a single prompt block.
|
|
545
|
-
const snapshotBlock = this.#renderAmsSnapshot(ams);
|
|
551
|
+
const snapshotBlock = this.#renderAmsSnapshot(ams, this.#config.language || 'en');
|
|
546
552
|
|
|
547
553
|
const scopes = buildRelevantScopes({
|
|
548
554
|
groupId: args.groupId,
|
|
@@ -558,30 +564,35 @@ export class Engine {
|
|
|
558
564
|
* so the LLM sees a consistent layout.
|
|
559
565
|
*
|
|
560
566
|
* @param {import('./memory/ams.js').ActiveMemorySet} ams
|
|
567
|
+
* @param {string} [language]
|
|
561
568
|
* @returns {string}
|
|
562
569
|
*/
|
|
563
|
-
#renderAmsSnapshot(ams) {
|
|
570
|
+
#renderAmsSnapshot(ams, language = 'en') {
|
|
564
571
|
const snap = ams.snapshot();
|
|
565
572
|
if (!snap) return '';
|
|
566
573
|
const parts = [];
|
|
567
574
|
if (snap.resident.length === 0 && snap.recent.length === 0 && snap.onDemand.length === 0) {
|
|
568
575
|
return '';
|
|
569
576
|
}
|
|
570
|
-
|
|
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.');
|
|
571
582
|
if (snap.resident.length > 0) {
|
|
572
|
-
parts.push('### Resident');
|
|
583
|
+
parts.push(zh ? '### 常驻记忆' : '### Resident');
|
|
573
584
|
for (const r of snap.resident) {
|
|
574
585
|
parts.push(`- **${r.scope}**: ${r.summary}`);
|
|
575
586
|
}
|
|
576
587
|
}
|
|
577
588
|
if (snap.recent.length > 0) {
|
|
578
|
-
parts.push('### Recent');
|
|
589
|
+
parts.push(zh ? '### 最近记忆' : '### Recent');
|
|
579
590
|
for (const s of snap.recent) {
|
|
580
591
|
parts.push(`- (${s.scope}) ${(s.body || '').trim()}`);
|
|
581
592
|
}
|
|
582
593
|
}
|
|
583
594
|
if (snap.onDemand.length > 0) {
|
|
584
|
-
parts.push('### OnDemand');
|
|
595
|
+
parts.push(zh ? '### 按需记忆' : '### OnDemand');
|
|
585
596
|
for (const s of snap.onDemand) {
|
|
586
597
|
parts.push(`- (${s.scope}) ${(s.body || '').trim()}`);
|
|
587
598
|
}
|
|
@@ -772,6 +783,11 @@ export class Engine {
|
|
|
772
783
|
parentVpPersona: vpCtx?.vpPersona || null,
|
|
773
784
|
onEvent: this.#subAgentEventSink || null,
|
|
774
785
|
language: this.#config?.language || 'en',
|
|
786
|
+
// Forward the session-shared ToolUsageStats so sub-agent
|
|
787
|
+
// engines record tool calls into the same on-disk snapshot
|
|
788
|
+
// (~/.yeaft/stats/tool-usage.json) the parent engine writes
|
|
789
|
+
// to. Null when the parent has no stats wired (e.g. tests).
|
|
790
|
+
toolStats: this.#toolStats || null,
|
|
775
791
|
},
|
|
776
792
|
};
|
|
777
793
|
}
|
|
@@ -1133,6 +1149,7 @@ export class Engine {
|
|
|
1133
1149
|
vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
|
|
1134
1150
|
? vpPersona.vpId
|
|
1135
1151
|
: (typeof senderVpId === 'string' ? senderVpId : undefined),
|
|
1152
|
+
language: this.#config.language || 'en',
|
|
1136
1153
|
});
|
|
1137
1154
|
|
|
1138
1155
|
// ─── AMS: populate + snapshot ───────────────────────────────
|
package/unify/eval/runner.js
CHANGED
|
@@ -90,7 +90,11 @@ export async function runSingleEval(evalCase, { adapter, model, config = {} }) {
|
|
|
90
90
|
const trace = new NullTrace();
|
|
91
91
|
const engineConfig = { model, maxOutputTokens: 4096, ...config };
|
|
92
92
|
|
|
93
|
-
// Build engine — optionally with ToolRegistry
|
|
93
|
+
// Build engine — optionally with ToolRegistry. NOTE: intentionally
|
|
94
|
+
// no `toolStats` here. The eval runner is an offline scoring harness
|
|
95
|
+
// and must NOT pollute the user-facing `~/.yeaft/stats/tool-usage.json`
|
|
96
|
+
// snapshot (see PR #782 for the session-shared ToolUsageStats
|
|
97
|
+
// wiring on the runtime paths).
|
|
94
98
|
const engineOpts = { adapter, trace, config: engineConfig };
|
|
95
99
|
|
|
96
100
|
if (evalCase.registryTools) {
|
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`);
|
|
@@ -86,6 +86,7 @@ export function isRestrictedToolName(name) {
|
|
|
86
86
|
* parentName?: string,
|
|
87
87
|
* parentVpId?: string,
|
|
88
88
|
* parentVpPersona?: object,
|
|
89
|
+
* toolStats?: object,
|
|
89
90
|
* onEvent?: (agentId: string, evt: object) => void,
|
|
90
91
|
* language?: 'en'|'zh',
|
|
91
92
|
* }} deps
|
|
@@ -112,6 +113,11 @@ export function startSubAgent(agent, deps = {}) {
|
|
|
112
113
|
skillManager: deps.skillManager || null,
|
|
113
114
|
mcpManager: deps.mcpManager || null,
|
|
114
115
|
yeaftDir: deps.yeaftDir || null,
|
|
116
|
+
// Share the session-shared ToolUsageStats so sub-agent tool calls
|
|
117
|
+
// land in the same on-disk snapshot the parent records into. Sub-
|
|
118
|
+
// agents are often the heaviest tool users — leaving them out
|
|
119
|
+
// skewed `unify_fetch_tool_stats` output.
|
|
120
|
+
toolStats: deps.toolStats || null,
|
|
115
121
|
});
|
|
116
122
|
|
|
117
123
|
agent.subEngine = subEngine;
|
package/unify/web-bridge.js
CHANGED
|
@@ -499,6 +499,19 @@ export function __testGroupContextEntry(groupId) {
|
|
|
499
499
|
return groupContexts.get(groupId);
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Test-only: build (or return cached) per-VP Engine for a session that
|
|
504
|
+
* was wired via `__testSetSession`. Lets tests assert that the engine's
|
|
505
|
+
* dependencies (notably `toolStats`) come from the session reference —
|
|
506
|
+
* see `test/agent/web-bridge-vp-engine-tool-stats.test.js`.
|
|
507
|
+
*
|
|
508
|
+
* @param {string} groupId
|
|
509
|
+
* @param {string} vpId
|
|
510
|
+
*/
|
|
511
|
+
export function __testGetOrCreateVpEngine(groupId, vpId) {
|
|
512
|
+
return getOrCreateVpEngine(groupId, vpId);
|
|
513
|
+
}
|
|
514
|
+
|
|
502
515
|
/** Whether we've already sent a permission warning to the UI */
|
|
503
516
|
let _permissionDiagnosticSent = false;
|
|
504
517
|
|
|
@@ -540,6 +553,12 @@ function getOrCreateVpEngine(groupId, vpId) {
|
|
|
540
553
|
skillManager: session.skillManager,
|
|
541
554
|
mcpManager: session.mcpManager,
|
|
542
555
|
yeaftDir: session.yeaftDir,
|
|
556
|
+
// Share the session-shared ToolUsageStats so per-VP tool calls land
|
|
557
|
+
// in the same on-disk snapshot the `unify_fetch_tool_stats` handler
|
|
558
|
+
// reads. Without this, engine's record-on-tool-exec guard
|
|
559
|
+
// (`if (this.#toolStats && ...)`) is false and group VP tool calls
|
|
560
|
+
// are silently dropped.
|
|
561
|
+
toolStats: session.toolStats || null,
|
|
543
562
|
});
|
|
544
563
|
vpEngines.set(key, eng);
|
|
545
564
|
return eng;
|
|
@@ -2673,6 +2692,37 @@ export const __testRaceWithEscalation = raceWithEscalation;
|
|
|
2673
2692
|
* Backwards-compat: when neither field is set, defaults to `vpId='default'`
|
|
2674
2693
|
* which matches the pre-v0.1.754 behavior.
|
|
2675
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
|
+
|
|
2676
2726
|
export async function handleUnifyDreamTrigger(msg = {}) {
|
|
2677
2727
|
// Resolve tag up-front so EVERY outbound envelope (including the
|
|
2678
2728
|
// scheduler-uninitialised early-return below) carries `groupId` /
|
|
@@ -2685,11 +2735,11 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2685
2735
|
const tag = groupId ? { groupId } : { vpId };
|
|
2686
2736
|
|
|
2687
2737
|
if (!session?.dreamScheduler) {
|
|
2738
|
+
const error = 'Dream scheduler not initialized — session not loaded.';
|
|
2688
2739
|
sendToServer({
|
|
2689
2740
|
type: 'unify_dream_result',
|
|
2690
2741
|
...tag,
|
|
2691
|
-
|
|
2692
|
-
error: 'Dream scheduler not initialized — session not loaded.',
|
|
2742
|
+
...normalizeDreamResult({ error }),
|
|
2693
2743
|
});
|
|
2694
2744
|
return;
|
|
2695
2745
|
}
|
|
@@ -2705,11 +2755,11 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2705
2755
|
// dream-v2/schedule.js inflight reuse), so the user-facing semantics
|
|
2706
2756
|
// are unchanged ("you already asked").
|
|
2707
2757
|
if (groupId && inflightScopedDreamGroups.size > 0) {
|
|
2758
|
+
const error = 'A dream pass is already running.';
|
|
2708
2759
|
sendToServer({
|
|
2709
2760
|
type: 'unify_dream_result',
|
|
2710
2761
|
...tag,
|
|
2711
|
-
|
|
2712
|
-
error: 'A dream pass is already running.',
|
|
2762
|
+
...normalizeDreamResult({ error }),
|
|
2713
2763
|
});
|
|
2714
2764
|
return;
|
|
2715
2765
|
}
|
|
@@ -2745,17 +2795,12 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2745
2795
|
? await session.dreamScheduler.triggerDreamForScopes([`group/${groupId}`])
|
|
2746
2796
|
: await session.dreamScheduler.triggerDreamNow();
|
|
2747
2797
|
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
//
|
|
2751
|
-
//
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
const lastDreamAt = result?.startedAt || new Date().toISOString();
|
|
2755
|
-
const success = !result.error && !result.skipped;
|
|
2756
|
-
|
|
2757
|
-
// Spread `result` FIRST so derived fields (success, entriesCreated,
|
|
2758
|
-
// 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
|
|
2759
2804
|
// with the same name. Today there is no collision (runner.js returns
|
|
2760
2805
|
// { groups, targets, startedAt, error?, skipped? }) but the failure
|
|
2761
2806
|
// mode of the alternative ordering is silent — review feedback from
|
|
@@ -2772,16 +2817,14 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2772
2817
|
type: 'unify_dream_result',
|
|
2773
2818
|
...tag,
|
|
2774
2819
|
...result,
|
|
2775
|
-
|
|
2776
|
-
entriesCreated,
|
|
2777
|
-
lastDreamAt,
|
|
2820
|
+
...normalized,
|
|
2778
2821
|
});
|
|
2779
2822
|
} catch (err) {
|
|
2823
|
+
const error = err?.message || String(err);
|
|
2780
2824
|
sendToServer({
|
|
2781
2825
|
type: 'unify_dream_result',
|
|
2782
2826
|
...tag,
|
|
2783
|
-
|
|
2784
|
-
error: err?.message || String(err),
|
|
2827
|
+
...normalizeDreamResult({ error }),
|
|
2785
2828
|
});
|
|
2786
2829
|
} finally {
|
|
2787
2830
|
// Restore the original sink and release the per-group inflight lock.
|