@yeaft/webchat-agent 0.1.847 → 0.1.849

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.847",
3
+ "version": "0.1.849",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -228,7 +228,23 @@ export async function applyMergedTarget(merged, opts) {
228
228
  await writeMemory(scope, stamped, { root: opts.root });
229
229
  await writeSummary(scope, summaryMd || '', { root: opts.root, language: opts.language });
230
230
 
231
- if (opts.onProgress) opts.onProgress({ phase: 'apply', target: merged.target, status: 'done', batches: batchesUsed });
231
+ if (opts.onProgress) {
232
+ // feat-dream-debug-detail: surface a truncated copy of what was
233
+ // actually written so the debug panel can show "what segments were
234
+ // generated" instead of just "done". The full bytes are on disk
235
+ // anyway — this preview is for at-a-glance debugging.
236
+ opts.onProgress({
237
+ phase: 'apply',
238
+ target: merged.target,
239
+ status: 'done',
240
+ batches: batchesUsed,
241
+ kind: merged.kind,
242
+ memoryMdPreview: truncateForDebug(stamped),
243
+ summaryMdPreview: truncateForDebug(summaryMd || ''),
244
+ memoryMdLength: (stamped || '').length,
245
+ summaryMdLength: (summaryMd || '').length,
246
+ });
247
+ }
232
248
  return { target: merged.target, kind: merged.kind, batches: batchesUsed };
233
249
  }
234
250
 
@@ -246,3 +262,23 @@ function scopeRelDir(scope) {
246
262
  }
247
263
 
248
264
  function oneLine(s) { return String(s || '').replace(/\s+/g, ' ').trim().slice(0, 200); }
265
+
266
+ /**
267
+ * Per-field truncation cap for debug previews emitted on `apply/done`.
268
+ * Keep this small — the dream panel only needs a recognisable snippet.
269
+ * Total worst-case payload is `PREVIEW_MAX * 2 * targets_per_run` per
270
+ * dream pass; with N=50 targets that's ~200 KB. The full bytes are on
271
+ * disk under <root>/<scope>/{memory,summary}.md anyway — these previews
272
+ * are for at-a-glance debugging only.
273
+ */
274
+ const PREVIEW_MAX = 2048;
275
+
276
+ /**
277
+ * Truncate a markdown blob for inclusion in a debug-panel cell. Adds a
278
+ * "…(+N chars)" marker so the user knows it was cut.
279
+ */
280
+ function truncateForDebug(s, max = PREVIEW_MAX) {
281
+ const str = String(s || '');
282
+ if (str.length <= max) return str;
283
+ return str.slice(0, max) + `…(+${str.length - max} chars)`;
284
+ }
@@ -7,6 +7,35 @@
7
7
  *
8
8
  * Memory v2 is the only path. The legacy `config.memoryV2` opt-out flag was
9
9
  * retired (task-710) — the wiring is unconditional.
10
+ *
11
+ * ─────────────────────────────────────────────────────────────────────
12
+ * Wire contract — DreamEvent (consumed by web/components/UnifyDebugPanel.js)
13
+ * ─────────────────────────────────────────────────────────────────────
14
+ * The events persisted to trace_events with these event_type values are
15
+ * load-bearing for the debug panel. Renaming a field on this side
16
+ * silently degrades that UI to a generic JSON dump.
17
+ *
18
+ * dream_turn_open: { type:'turn_open', turnId, userPrompt, vpId, groupId, at }
19
+ * dream_loop: { type:'loop', turnId, loopNumber, pass, model,
20
+ * systemPrompt: string,
21
+ * messages: [{ role:'user', content:string }],
22
+ * response: string,
23
+ * toolCalls: [], usage: { inputTokens, outputTokens, totalTokens },
24
+ * latencyMs, ttfbMs, stopReason, rawRequest, rawResponse }
25
+ * dream_turn_close: { type:'turn_close', turnId, totalMs, totalTokens, loopCount,
26
+ * metrics: { llmCallCount, inputTokens, outputTokens,
27
+ * totalTokens, durationMs, passBreakdown:{[pass]:{
28
+ * llmCallCount, inputTokens, outputTokens,
29
+ * totalTokens, durationMs }} } }
30
+ * dream_run: { type:'dream_run', turnId, phase:'result',
31
+ * status:'done'|'error', metrics, resultSummary:{ groups,
32
+ * targets, error, skipped, skippedReason } }
33
+ * dream_progress: runner-emitted phase events (`start`/`load-diff`/`triage`/
34
+ * `merge`/`apply`/`done`). The `apply/done` variant carries
35
+ * `kind, memoryMdPreview, summaryMdPreview, memoryMdLength,
36
+ * summaryMdLength` (see apply.js).
37
+ *
38
+ * `groupId` may be inherited via `stampDreamScope()` when a scope is active.
10
39
  */
11
40
 
12
41
  import { join } from 'path';
@@ -23,7 +23,7 @@
23
23
  */
24
24
 
25
25
  import { runPreflow as runFtsPreflow } from '../memory/preflow.js';
26
- import { isMember, resolveFallbackVp } from './roster.js';
26
+ import { resolveFallbackVp, resolveMemberId } from './roster.js';
27
27
 
28
28
  /** Matches `@vp-id` where id is [A-Za-z0-9_-]+. Captures the id. */
29
29
  const MENTION_RE = /(^|\s)@([A-Za-z0-9_][A-Za-z0-9_-]*)/g;
@@ -115,15 +115,16 @@ export function selectRespondingVps(input) {
115
115
  const dispatched = [];
116
116
  const errors = [];
117
117
  for (const vpId of mentions) {
118
- if (!isMember(meta, vpId)) {
118
+ const canonicalVpId = resolveMemberId(meta, vpId);
119
+ if (!canonicalVpId) {
119
120
  errors.push({ vpId, error: 'not_in_roster' });
120
121
  continue;
121
122
  }
122
- if (taskMembers && !taskMembers.includes(vpId)) {
123
+ if (taskMembers && !taskMembers.includes(canonicalVpId)) {
123
124
  errors.push({ vpId, error: 'not_in_task_members' });
124
125
  continue;
125
126
  }
126
- dispatched.push(vpId);
127
+ if (!dispatched.includes(canonicalVpId)) dispatched.push(canonicalVpId);
127
128
  }
128
129
  return { dispatched, fallback: null, errors, reason: 'mention' };
129
130
  }
@@ -55,6 +55,33 @@ export function isMember(meta, vpId) {
55
55
  return meta.roster.includes(vpId);
56
56
  }
57
57
 
58
+ /**
59
+ * Resolve a user/tool supplied VP target to its canonical roster id.
60
+ *
61
+ * The persisted roster stores canonical VP ids such as `linus`, while UI
62
+ * mentions and route_forward callers may still send the display-style
63
+ * `vp-linus` alias. Prefer exact roster ids first so a real `vp-foo` member
64
+ * is never collapsed to `foo`.
65
+ *
66
+ * @param {object} meta
67
+ * @param {string} target
68
+ * @returns {string|null}
69
+ */
70
+ export function resolveMemberId(meta, target) {
71
+ if (!meta || !Array.isArray(meta.roster) || typeof target !== 'string') {
72
+ return null;
73
+ }
74
+ if (meta.roster.includes(target)) return target;
75
+
76
+ const prefix = 'vp-';
77
+ if (target.startsWith(prefix)) {
78
+ const unprefixed = target.slice(prefix.length);
79
+ if (meta.roster.includes(unprefixed)) return unprefixed;
80
+ }
81
+
82
+ return null;
83
+ }
84
+
58
85
  /**
59
86
  * Resolve which VP should answer a message with no explicit @-mention.
60
87
  * Per architecture G2: defaultVpId if set, else roster[0], else null.
@@ -25,7 +25,7 @@
25
25
  * (d) Tool schema uses defineTool (agent/unify/tools/types.js).
26
26
  */
27
27
 
28
- import { isMember } from '../groups/roster.js';
28
+ import { resolveMemberId } from '../groups/roster.js';
29
29
  import { createLoopGuard, extendCausedBy } from './loop-guard.js';
30
30
 
31
31
  /**
@@ -86,19 +86,20 @@ export function createRouter(deps = {}) {
86
86
  if (typeof text !== 'string' || text.length === 0) {
87
87
  return { ok: false, error: 'text_required' };
88
88
  }
89
- if (to === from) {
90
- return { ok: false, error: 'self_forward_rejected' };
91
- }
92
-
93
89
  const meta = coordinator.group.getMeta();
94
90
  if (!meta) return { ok: false, error: 'group_not_initialised' };
95
91
 
96
92
  // Roster membership — `all` is reserved broadcast sentinel handled by
97
- // coordinator; anything else must be a real member so we fail fast with
98
- // a VP-friendly error before hitting Coordinator.
99
- if (to !== 'all' && !isMember(meta, to)) {
93
+ // coordinator; anything else must resolve to a real member so we fail fast
94
+ // with a VP-friendly error before hitting Coordinator. `vp-<id>` is a
95
+ // tolerated UI/tool alias for canonical roster ids such as `linus`.
96
+ const targetVpId = to === 'all' ? 'all' : resolveMemberId(meta, to);
97
+ if (targetVpId !== 'all' && !targetVpId) {
100
98
  return { ok: false, error: 'target_not_in_roster' };
101
99
  }
100
+ if (targetVpId === from) {
101
+ return { ok: false, error: 'self_forward_rejected' };
102
+ }
102
103
 
103
104
  // Build the causedBy chain BEFORE constructing the synthetic user-like
104
105
  // message. We don't know the new msgId yet (coordinator mints it on
@@ -110,7 +111,7 @@ export function createRouter(deps = {}) {
110
111
  // Loop guard: for broadcast, use 'all' as the target key so one VP
111
112
  // spamming @all still gets throttled even if each cycle hits different
112
113
  // member inboxes.
113
- const guardKey = to === 'all' ? 'all' : to;
114
+ const guardKey = targetVpId;
114
115
  const verdict = guard.check({
115
116
  groupId: meta.id,
116
117
  targetVpId: guardKey,
@@ -131,9 +132,9 @@ export function createRouter(deps = {}) {
131
132
  // stamp + `synthetic` marker let Coordinator's `selectRespondingVps`
132
133
  // still treat this like a routed turn (target VPs need to respond) even
133
134
  // though role is now 'assistant'.
134
- const injectText = to === 'all'
135
+ const injectText = targetVpId === 'all'
135
136
  ? `@all ${text}`
136
- : `@${to} ${text}`;
137
+ : `@${targetVpId} ${text}`;
137
138
 
138
139
  const report = coordinator.ingest(
139
140
  {