@yeaft/webchat-agent 0.1.754 → 0.1.756
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/groups/coordinator.js +14 -1
- package/unify/routing/router.js +7 -12
- package/unify/web-bridge.js +49 -14
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
|
}
|
|
@@ -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;
|
|
@@ -2731,11 +2758,19 @@ export async function handleUnifyLoadHistory(msg) {
|
|
|
2731
2758
|
if (m.role === 'user') {
|
|
2732
2759
|
sendUnifyOutput({ type: 'user', message: { content: m.content } }, { groupId: m.groupId || null });
|
|
2733
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;
|
|
2734
2769
|
sendUnifyOutput({
|
|
2735
2770
|
type: 'assistant',
|
|
2736
2771
|
message: { content: [{ type: 'text', text: m.content }] },
|
|
2737
|
-
},
|
|
2738
|
-
sendUnifyOutput({ type: 'result', result_text: '' },
|
|
2772
|
+
}, envelopeOpts);
|
|
2773
|
+
sendUnifyOutput({ type: 'result', result_text: '' }, envelopeOpts);
|
|
2739
2774
|
}
|
|
2740
2775
|
}
|
|
2741
2776
|
|