@yeaft/webchat-agent 0.1.753 → 0.1.755
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/conversation/persist.js +6 -0
- package/unify/dream-v2/runner.js +17 -1
- package/unify/dream-v2/state.js +103 -2
- package/unify/groups/coordinator.js +14 -1
- package/unify/routing/router.js +7 -12
- package/unify/web-bridge.js +83 -21
package/package.json
CHANGED
|
@@ -102,6 +102,11 @@ function serializeMessage(msg) {
|
|
|
102
102
|
// in the default group and switching back to the originating group
|
|
103
103
|
// shows an empty pane.
|
|
104
104
|
if (msg.groupId) fm.push(`groupId: ${msg.groupId}`);
|
|
105
|
+
// Group-chat attribution: when a VP authors an assistant turn (either
|
|
106
|
+
// its own reply or a route_forward injection from another VP), stamp
|
|
107
|
+
// the speaker so the UI can render the message on the correct VP track.
|
|
108
|
+
// For real user messages this is unset.
|
|
109
|
+
if (msg.speakerVpId) fm.push(`speakerVpId: ${msg.speakerVpId}`);
|
|
105
110
|
|
|
106
111
|
// Token estimate
|
|
107
112
|
const content = msg.content || '';
|
|
@@ -173,6 +178,7 @@ export function parseMessage(raw) {
|
|
|
173
178
|
case 'threadId': msg.threadId = value; break;
|
|
174
179
|
case 'sourceThreadId': msg.sourceThreadId = value; break;
|
|
175
180
|
case 'groupId': msg.groupId = value; break;
|
|
181
|
+
case 'speakerVpId': msg.speakerVpId = value; break;
|
|
176
182
|
// toolCalls are multi-line YAML — handled separately below
|
|
177
183
|
}
|
|
178
184
|
}
|
package/unify/dream-v2/runner.js
CHANGED
|
@@ -44,7 +44,7 @@ import { listScopes, readSummary } from '../memory/store-v2.js';
|
|
|
44
44
|
import {
|
|
45
45
|
DEFAULT_LIMITS,
|
|
46
46
|
} from './limits.js';
|
|
47
|
-
import { readGroupState, writeGroupState } from './state.js';
|
|
47
|
+
import { readGroupState, writeGroupState, writeDreamError } from './state.js';
|
|
48
48
|
import { segmentDiff, truncateMessage, estimateMessagesTokens } from './segment.js';
|
|
49
49
|
import { triageGroupSegments } from './triage.js';
|
|
50
50
|
import { mergeByTarget } from './merge.js';
|
|
@@ -148,6 +148,14 @@ export async function runDream(opts) {
|
|
|
148
148
|
} catch (err) {
|
|
149
149
|
groupsReport.push({ groupId, new: newCount, status: 'error', error: err.message });
|
|
150
150
|
onProgress({ phase: 'triage', groupId, status: 'error', error: err.message });
|
|
151
|
+
// Journal the failure on disk so operators can see WHY dream is
|
|
152
|
+
// not advancing without having to enable `config.debug`. Best-
|
|
153
|
+
// effort — `writeDreamError` swallows its own I/O errors.
|
|
154
|
+
await writeDreamError(opts.root, `group/${groupId}`, {
|
|
155
|
+
phase: 'triage',
|
|
156
|
+
message: err.message,
|
|
157
|
+
stack: err.stack,
|
|
158
|
+
});
|
|
151
159
|
continue;
|
|
152
160
|
}
|
|
153
161
|
|
|
@@ -191,6 +199,14 @@ export async function runDream(opts) {
|
|
|
191
199
|
error: err.message,
|
|
192
200
|
});
|
|
193
201
|
onProgress({ phase: 'apply', target: merged.target, status: 'error', error: err.message });
|
|
202
|
+
// Journal apply-stage failures into the target scope's directory
|
|
203
|
+
// (`<root>/<merged.target>/.dream-last-error.json`). Same rationale
|
|
204
|
+
// as the triage catch above.
|
|
205
|
+
await writeDreamError(opts.root, merged.target, {
|
|
206
|
+
phase: 'apply',
|
|
207
|
+
message: err.message,
|
|
208
|
+
stack: err.stack,
|
|
209
|
+
});
|
|
194
210
|
}
|
|
195
211
|
}
|
|
196
212
|
|
package/unify/dream-v2/state.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dream-v2/state.js.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Three pieces of state, tracked separately:
|
|
5
5
|
*
|
|
6
6
|
* 1. Per-group control state (used to decide whether a group enters
|
|
7
7
|
* triage and how far to advance the cursor):
|
|
@@ -31,13 +31,25 @@
|
|
|
31
31
|
* control-flow decision. We update it by replacing the existing
|
|
32
32
|
* block (if any) or appending a new one to the end of the file.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
34
|
+
* 3. Per-scope dream-error sink (added v0.1.754):
|
|
35
|
+
*
|
|
36
|
+
* ~/.yeaft/memory/<scope>/.dream-last-error.json
|
|
37
|
+
*
|
|
38
|
+
* Most-recent-wins JSON written unconditionally on every triage
|
|
39
|
+
* or apply failure (best-effort — never throws even when the I/O
|
|
40
|
+
* itself fails). The runner used to swallow these exceptions and
|
|
41
|
+
* the only sink was a `config.debug`-gated console.log; this file
|
|
42
|
+
* gives operators on-disk evidence regardless of debug. See
|
|
43
|
+
* `writeDreamError` / `readDreamError` below for the contract.
|
|
44
|
+
*
|
|
45
|
+
* All helpers are pure I/O; no LLM, no logic beyond parsing.
|
|
35
46
|
*/
|
|
36
47
|
|
|
37
48
|
import { promises as fsp, existsSync } from 'fs';
|
|
38
49
|
import { join, dirname } from 'path';
|
|
39
50
|
|
|
40
51
|
const STATE_FILE = '.dream-state';
|
|
52
|
+
const ERROR_FILE = '.dream-last-error.json';
|
|
41
53
|
const DREAM_BLOCK_OPEN = '<!-- dream-state -->';
|
|
42
54
|
const DREAM_BLOCK_CLOSE = '<!-- /dream-state -->';
|
|
43
55
|
|
|
@@ -101,6 +113,95 @@ function parseGroupState(raw) {
|
|
|
101
113
|
return out;
|
|
102
114
|
}
|
|
103
115
|
|
|
116
|
+
// ─── per-scope dream error sink ────────────────────────────────
|
|
117
|
+
//
|
|
118
|
+
// Why: dream-v2 silently swallowed exceptions at the triage / apply
|
|
119
|
+
// catch sites — the only sink was `trace.event('dream_progress', evt)`
|
|
120
|
+
// and a `config.debug`-gated `console.log` in `session-wiring.js`. With
|
|
121
|
+
// `debug=false` (the default), there was no on-disk evidence that a
|
|
122
|
+
// dream pass had ever failed: no `.dream-state` (because we only write
|
|
123
|
+
// it on success), no log file, nothing. The Resident layer's continued
|
|
124
|
+
// regurgitation of the bootstrap seed was the only symptom.
|
|
125
|
+
//
|
|
126
|
+
// `writeDreamError` writes `<memoryRoot>/<scope>/.dream-last-error.json`
|
|
127
|
+
// unconditionally on every catch (best-effort — write failures must not
|
|
128
|
+
// shadow the original error). Operators can then `ls ~/.yeaft/memory/
|
|
129
|
+
// group/<id>/` and see what blew up, without having to re-enable debug.
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve a memoryRoot + scope-string to the scope directory.
|
|
133
|
+
* The scope string is the same shape dream-v2 already uses internally:
|
|
134
|
+
* `'user'`, `'vp/<vpId>'`, `'group/<groupId>'`, `'feature/<id>'`, etc.
|
|
135
|
+
*
|
|
136
|
+
* Pure path-join; does NOT create the directory. The writer creates it.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} root
|
|
139
|
+
* @param {string} scope
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
export function scopeDirFor(root, scope) {
|
|
143
|
+
// Defensive: trim leading/trailing slashes so callers can pass either
|
|
144
|
+
// `'group/grp_fun'` or `/group/grp_fun/` — both land on the same dir.
|
|
145
|
+
const clean = String(scope || '').replace(/^\/+|\/+$/g, '');
|
|
146
|
+
return join(root, clean);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Best-effort write of the dream-error sink. Never throws — a failed
|
|
151
|
+
* write is silently swallowed because the caller is already in an
|
|
152
|
+
* error-handling path and we must not mask the original failure.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} root — memory root, e.g. ~/.yeaft/memory
|
|
155
|
+
* @param {string} scope — `'group/<id>'` for triage failures,
|
|
156
|
+
* `merged.target` for apply failures.
|
|
157
|
+
* @param {{ phase: string, message: string, stack?: string|null, at?: string }} info
|
|
158
|
+
* @returns {Promise<void>}
|
|
159
|
+
*/
|
|
160
|
+
export async function writeDreamError(root, scope, info) {
|
|
161
|
+
try {
|
|
162
|
+
const dir = scopeDirFor(root, scope);
|
|
163
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
164
|
+
const abs = join(dir, ERROR_FILE);
|
|
165
|
+
const at = (info && info.at) || new Date().toISOString();
|
|
166
|
+
// Trim stack to the first 5 frames — enough for diagnosis, small
|
|
167
|
+
// enough that the artifact stays human-readable. Missing/empty
|
|
168
|
+
// stack collapses to `null` rather than `""` so the artifact is
|
|
169
|
+
// cleaner for operators.
|
|
170
|
+
const rawStack = info && typeof info.stack === 'string' ? info.stack : '';
|
|
171
|
+
const stackLines = rawStack ? rawStack.split('\n').slice(0, 5) : [];
|
|
172
|
+
const body = JSON.stringify({
|
|
173
|
+
at,
|
|
174
|
+
scope,
|
|
175
|
+
phase: String(info?.phase || 'unknown'),
|
|
176
|
+
message: String(info?.message || ''),
|
|
177
|
+
stack: stackLines.length > 0 ? stackLines.join('\n') : null,
|
|
178
|
+
}, null, 2) + '\n';
|
|
179
|
+
await atomicWrite(abs, body);
|
|
180
|
+
} catch {
|
|
181
|
+
// Best-effort: swallow. The caller is already handling the real
|
|
182
|
+
// error; an inability to journal it must not shadow that.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read the last dream error JSON for a scope, or null if absent. Used
|
|
188
|
+
* by the debug panel and by tests. Tolerates a malformed file by
|
|
189
|
+
* returning `{ raw: <body>, parseError: <message> }` instead of
|
|
190
|
+
* throwing.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} root
|
|
193
|
+
* @param {string} scope
|
|
194
|
+
* @returns {Promise<object|null>}
|
|
195
|
+
*/
|
|
196
|
+
export async function readDreamError(root, scope) {
|
|
197
|
+
const abs = join(scopeDirFor(root, scope), ERROR_FILE);
|
|
198
|
+
let raw;
|
|
199
|
+
try { raw = await fsp.readFile(abs, 'utf8'); }
|
|
200
|
+
catch (err) { if (err && err.code === 'ENOENT') return null; throw err; }
|
|
201
|
+
try { return JSON.parse(raw); }
|
|
202
|
+
catch (e) { return { raw, parseError: e.message }; }
|
|
203
|
+
}
|
|
204
|
+
|
|
104
205
|
// ─── per-scope marker (memory.md tail block) ───────────────────
|
|
105
206
|
|
|
106
207
|
/**
|
|
@@ -64,7 +64,20 @@ export function createCoordinator(group, options = {}) {
|
|
|
64
64
|
const meta = group.getMeta();
|
|
65
65
|
if (!meta) throw new Error('group not initialised (call createGroup first)');
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
// `fromUser` drives `selectRespondingVps` — when true, the @-mention
|
|
68
|
+
// matrix runs (mention/broadcast/fallback). When false, VPs cannot
|
|
69
|
+
// text-@-route (VP-authored free text is surface noise per arch §6).
|
|
70
|
+
//
|
|
71
|
+
// route_forward injection is a special case: the message is VP-authored
|
|
72
|
+
// (role='assistant') but it MUST trigger dispatch (target VP needs to
|
|
73
|
+
// run). We detect it via `meta.injectedBy === 'route_forward'` and
|
|
74
|
+
// treat it as "user-like" for dispatch purposes only. Persistence still
|
|
75
|
+
// honours the caller's `role` field so the on-disk record correctly
|
|
76
|
+
// attributes the turn to the sending VP, not to the user.
|
|
77
|
+
const isRouteForwardInjection = input?.meta?.injectedBy === 'route_forward';
|
|
78
|
+
const fromUser = input.from === 'user'
|
|
79
|
+
|| input.role === 'user'
|
|
80
|
+
|| isRouteForwardInjection;
|
|
68
81
|
const mentions = parseMentions(input.text);
|
|
69
82
|
|
|
70
83
|
// Persist first — audit log / replay works even if dispatch has bugs.
|
package/unify/routing/router.js
CHANGED
|
@@ -125,17 +125,12 @@ export function createRouter(deps = {}) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// Synthesize an injection message — coordinator's `ingest` expects the
|
|
128
|
-
// {from, role, text} shape.
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// stamp
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
// Why not add a third role to Coordinator? Scope discipline: Coordinator
|
|
135
|
-
// (334b) owns user vs VP branching. A third branch would force edits
|
|
136
|
-
// across both 334b and 334d for one hop. Setting role='user' with
|
|
137
|
-
// synthetic meta keeps the Coordinator API frozen — and `from` still
|
|
138
|
-
// reflects the real author, which is what the guard keys on anyway.
|
|
128
|
+
// {from, role, text} shape. The forwarded message is semantically the
|
|
129
|
+
// SENDER VP speaking (just delivered to a different VP's inbox), so it
|
|
130
|
+
// persists as role='assistant' attributed to `from`. The `meta.injectedBy`
|
|
131
|
+
// stamp + `synthetic` marker let Coordinator's `selectRespondingVps`
|
|
132
|
+
// still treat this like a routed turn (target VPs need to respond) even
|
|
133
|
+
// though role is now 'assistant'.
|
|
139
134
|
const injectText = to === 'all'
|
|
140
135
|
? `@all ${text}`
|
|
141
136
|
: `@${to} ${text}`;
|
|
@@ -143,7 +138,7 @@ export function createRouter(deps = {}) {
|
|
|
143
138
|
const report = coordinator.ingest(
|
|
144
139
|
{
|
|
145
140
|
from, // real VP id — preserved for provenance
|
|
146
|
-
role: '
|
|
141
|
+
role: 'assistant', // VP-authored — persists as assistant turn
|
|
147
142
|
text: injectText,
|
|
148
143
|
taskId: args.taskId ?? null,
|
|
149
144
|
meta: {
|
package/unify/web-bridge.js
CHANGED
|
@@ -617,10 +617,22 @@ function ensureDriverRunning(groupId, vpId) {
|
|
|
617
617
|
try {
|
|
618
618
|
const envMsgId = envelope?.msg?.id;
|
|
619
619
|
if (envMsgId && text) {
|
|
620
|
-
|
|
620
|
+
// route_forward injection: the envelope text was authored by the
|
|
621
|
+
// sending VP, not the user. Persist as an assistant row attributed
|
|
622
|
+
// to the sender so history replay puts it on the VP track, not on
|
|
623
|
+
// the human "user" track. Non-forward envelopes (real user input)
|
|
624
|
+
// persist as the user row exactly like before.
|
|
625
|
+
const meta = envelope?.msg?.meta || {};
|
|
626
|
+
const isForward = meta.injectedBy === 'route_forward';
|
|
627
|
+
const senderVpId = isForward
|
|
628
|
+
? (meta.senderVpId || envelope?.msg?.from || null)
|
|
629
|
+
: null;
|
|
630
|
+
persistInboundMessageOnceByMsgId({
|
|
621
631
|
msgId: envMsgId,
|
|
622
632
|
text,
|
|
623
633
|
groupId,
|
|
634
|
+
role: isForward ? 'assistant' : 'user',
|
|
635
|
+
speakerVpId: senderVpId,
|
|
624
636
|
});
|
|
625
637
|
}
|
|
626
638
|
} catch { /* never crash WS pipeline */ }
|
|
@@ -1685,7 +1697,7 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
1685
1697
|
try {
|
|
1686
1698
|
const persistedMsgId = report?.message?.id;
|
|
1687
1699
|
if (persistedMsgId) {
|
|
1688
|
-
|
|
1700
|
+
persistInboundMessageOnceByMsgId({
|
|
1689
1701
|
msgId: persistedMsgId,
|
|
1690
1702
|
text,
|
|
1691
1703
|
groupId,
|
|
@@ -1693,7 +1705,7 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
1693
1705
|
}
|
|
1694
1706
|
} catch (err) {
|
|
1695
1707
|
console.warn(
|
|
1696
|
-
'[Unify] unify_group_chat:
|
|
1708
|
+
'[Unify] unify_group_chat: persistInboundMessageOnceByMsgId failed',
|
|
1697
1709
|
err?.message || err,
|
|
1698
1710
|
);
|
|
1699
1711
|
}
|
|
@@ -2210,11 +2222,13 @@ function appendTurnToGroupHistory(groupId, prompt, assistantTextParts, toolCalls
|
|
|
2210
2222
|
}
|
|
2211
2223
|
|
|
2212
2224
|
/**
|
|
2213
|
-
* Persist
|
|
2214
|
-
* keyed by the coordinator-assigned `msgId`.
|
|
2215
|
-
* (real user input
|
|
2216
|
-
*
|
|
2217
|
-
*
|
|
2225
|
+
* Persist an inbound message row to disk EXACTLY ONCE per
|
|
2226
|
+
* coordinator-ingest call, keyed by the coordinator-assigned `msgId`.
|
|
2227
|
+
* Both `handleUnifyGroupChat` (real user input, persists as
|
|
2228
|
+
* role='user') and `enqueueForVp`'s driver loop (route_forward
|
|
2229
|
+
* synthetic injections, persists as role='assistant' attributed via
|
|
2230
|
+
* `speakerVpId`) call this — the Set guard makes either path the
|
|
2231
|
+
* writer, whichever runs first, while the other becomes a no-op.
|
|
2218
2232
|
*
|
|
2219
2233
|
* Without this dedup, a 2-VP group prompt produces TWO `m{NNNN}.md`
|
|
2220
2234
|
* user rows (one per engine) — `handleUnifyLoadHistory` then replays
|
|
@@ -2232,11 +2246,11 @@ function appendTurnToGroupHistory(groupId, prompt, assistantTextParts, toolCalls
|
|
|
2232
2246
|
* schema today (the engine never wrote them either) — they live on the
|
|
2233
2247
|
* coordinator's jsonl-log under group meta.
|
|
2234
2248
|
*
|
|
2235
|
-
* @param {{ msgId:string, text:string, groupId:string }} args
|
|
2249
|
+
* @param {{ msgId:string, text:string, groupId:string, role?:string, speakerVpId?:string|null }} args
|
|
2236
2250
|
* @returns {boolean} true if this call wrote the row, false if a prior
|
|
2237
2251
|
* call already wrote it (dedup hit).
|
|
2238
2252
|
*/
|
|
2239
|
-
function
|
|
2253
|
+
function persistInboundMessageOnceByMsgId({ msgId, text, groupId, role, speakerVpId }) {
|
|
2240
2254
|
if (!session?.conversationStore) return false;
|
|
2241
2255
|
// No msgId means no dedup key — caller is responsible for guarding.
|
|
2242
2256
|
// Both call sites already do (`if (envMsgId && text)` and
|
|
@@ -2267,17 +2281,30 @@ function persistUserMessageOnceByMsgId({ msgId, text, groupId }) {
|
|
|
2267
2281
|
}
|
|
2268
2282
|
}
|
|
2269
2283
|
try {
|
|
2284
|
+
// role defaults to 'user' for back-compat: handleUnifyGroupChat's
|
|
2285
|
+
// real-user call site passes no role and gets a user row. The driver
|
|
2286
|
+
// loop passes role='assistant' + speakerVpId for route_forward
|
|
2287
|
+
// injections so the on-disk record correctly attributes the text to
|
|
2288
|
+
// the sending VP.
|
|
2289
|
+
const persistRole = role === 'assistant' ? 'assistant' : 'user';
|
|
2270
2290
|
const record = {
|
|
2271
|
-
role:
|
|
2291
|
+
role: persistRole,
|
|
2272
2292
|
content: text,
|
|
2273
2293
|
threadId: 'main',
|
|
2274
2294
|
};
|
|
2275
2295
|
if (groupId) record.groupId = groupId;
|
|
2296
|
+
// Stamp speakerVpId so the UI's loadHistory replay can route the row
|
|
2297
|
+
// to the correct VP block. Only meaningful when role='assistant'; for
|
|
2298
|
+
// a real user message we leave it unset (the UI's user track is
|
|
2299
|
+
// unattributed).
|
|
2300
|
+
if (persistRole === 'assistant' && speakerVpId && typeof speakerVpId === 'string') {
|
|
2301
|
+
record.speakerVpId = speakerVpId;
|
|
2302
|
+
}
|
|
2276
2303
|
session.conversationStore.append(record);
|
|
2277
2304
|
return true;
|
|
2278
2305
|
} catch (err) {
|
|
2279
2306
|
console.warn(
|
|
2280
|
-
'[Unify]
|
|
2307
|
+
'[Unify] persistInboundMessageOnceByMsgId failed (non-fatal):',
|
|
2281
2308
|
err?.message || err,
|
|
2282
2309
|
);
|
|
2283
2310
|
return false;
|
|
@@ -2418,28 +2445,55 @@ export function __testGetRegisteredThreadIds() {
|
|
|
2418
2445
|
export const __testRaceWithEscalation = raceWithEscalation;
|
|
2419
2446
|
|
|
2420
2447
|
/**
|
|
2421
|
-
* Manual dream trigger
|
|
2448
|
+
* Manual dream trigger.
|
|
2449
|
+
*
|
|
2450
|
+
* Two call shapes, both routed through this single handler:
|
|
2451
|
+
*
|
|
2452
|
+
* { type: 'unify_dream_trigger', vpId } — per-VP trigger (legacy
|
|
2453
|
+
* VP-detail page button). Fires an unscoped dream pass; the result
|
|
2454
|
+
* event is tagged with `vpId` so the per-VP store row updates.
|
|
2455
|
+
*
|
|
2456
|
+
* { type: 'unify_dream_trigger', groupId } — per-GROUP trigger (new
|
|
2457
|
+
* in v0.1.754 — added so users can manually kick dream for a group
|
|
2458
|
+
* after seeing the Resident layer stuck on the bootstrap seed).
|
|
2459
|
+
* Fires a scope-filtered pass via `triggerDreamForScopes(['group/X'])`
|
|
2460
|
+
* so unrelated groups don't get processed; the result event is
|
|
2461
|
+
* tagged with `groupId` for the per-group UI row.
|
|
2462
|
+
*
|
|
2463
|
+
* Backwards-compat: when neither field is set, defaults to `vpId='default'`
|
|
2464
|
+
* which matches the pre-v0.1.754 behavior.
|
|
2422
2465
|
*/
|
|
2423
2466
|
export async function handleUnifyDreamTrigger(msg = {}) {
|
|
2467
|
+
// Resolve tag up-front so EVERY outbound envelope (including the
|
|
2468
|
+
// scheduler-uninitialised early-return below) carries `groupId` /
|
|
2469
|
+
// `vpId`. Without this the frontend's `applyDreamResult` couldn't
|
|
2470
|
+
// route the error event back to the right row and the per-group
|
|
2471
|
+
// "Run dream now" button would stay stuck on "Running…" forever
|
|
2472
|
+
// (review feedback from PR #757).
|
|
2473
|
+
const groupId = typeof msg.groupId === 'string' && msg.groupId ? msg.groupId : null;
|
|
2474
|
+
const vpId = !groupId ? (msg.vpId || 'default') : null;
|
|
2475
|
+
const tag = groupId ? { groupId } : { vpId };
|
|
2476
|
+
|
|
2424
2477
|
if (!session?.dreamScheduler) {
|
|
2425
2478
|
sendToServer({
|
|
2426
2479
|
type: 'unify_dream_result',
|
|
2480
|
+
...tag,
|
|
2427
2481
|
success: false,
|
|
2428
2482
|
error: 'Dream scheduler not initialized — session not loaded.',
|
|
2429
2483
|
});
|
|
2430
2484
|
return;
|
|
2431
2485
|
}
|
|
2432
2486
|
|
|
2433
|
-
const vpId = msg.vpId || 'default';
|
|
2434
|
-
|
|
2435
2487
|
try {
|
|
2436
2488
|
sendToServer({
|
|
2437
2489
|
type: 'unify_dream_status',
|
|
2438
|
-
|
|
2490
|
+
...tag,
|
|
2439
2491
|
status: 'running',
|
|
2440
2492
|
});
|
|
2441
2493
|
|
|
2442
|
-
const result =
|
|
2494
|
+
const result = groupId
|
|
2495
|
+
? await session.dreamScheduler.triggerDreamForScopes([`group/${groupId}`])
|
|
2496
|
+
: await session.dreamScheduler.triggerDreamNow();
|
|
2443
2497
|
|
|
2444
2498
|
// fix/dream-cadence-and-ui-trigger: derive a single "entries
|
|
2445
2499
|
// created" count for the UI bubble. The runner returns a richer
|
|
@@ -2457,7 +2511,7 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2457
2511
|
// PR #743.
|
|
2458
2512
|
sendToServer({
|
|
2459
2513
|
type: 'unify_dream_result',
|
|
2460
|
-
|
|
2514
|
+
...tag,
|
|
2461
2515
|
...result,
|
|
2462
2516
|
success: !result.error && !result.skipped,
|
|
2463
2517
|
entriesCreated,
|
|
@@ -2466,7 +2520,7 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2466
2520
|
} catch (err) {
|
|
2467
2521
|
sendToServer({
|
|
2468
2522
|
type: 'unify_dream_result',
|
|
2469
|
-
|
|
2523
|
+
...tag,
|
|
2470
2524
|
success: false,
|
|
2471
2525
|
error: err?.message || String(err),
|
|
2472
2526
|
});
|
|
@@ -2704,11 +2758,19 @@ export async function handleUnifyLoadHistory(msg) {
|
|
|
2704
2758
|
if (m.role === 'user') {
|
|
2705
2759
|
sendUnifyOutput({ type: 'user', message: { content: m.content } }, { groupId: m.groupId || null });
|
|
2706
2760
|
} else if (m.role === 'assistant') {
|
|
2761
|
+
// speakerVpId rides on the envelope so the frontend can route this
|
|
2762
|
+
// replayed assistant text to the correct VP track. Without it, the
|
|
2763
|
+
// history replay would merge replies from different VPs onto one
|
|
2764
|
+
// anonymous assistant turn.
|
|
2765
|
+
const envelopeOpts = {
|
|
2766
|
+
groupId: m.groupId || null,
|
|
2767
|
+
};
|
|
2768
|
+
if (m.speakerVpId) envelopeOpts.vpId = m.speakerVpId;
|
|
2707
2769
|
sendUnifyOutput({
|
|
2708
2770
|
type: 'assistant',
|
|
2709
2771
|
message: { content: [{ type: 'text', text: m.content }] },
|
|
2710
|
-
},
|
|
2711
|
-
sendUnifyOutput({ type: 'result', result_text: '' },
|
|
2772
|
+
}, envelopeOpts);
|
|
2773
|
+
sendUnifyOutput({ type: 'result', result_text: '' }, envelopeOpts);
|
|
2712
2774
|
}
|
|
2713
2775
|
}
|
|
2714
2776
|
|