@yeaft/webchat-agent 0.1.753 → 0.1.754

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.753",
3
+ "version": "0.1.754",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * dream-v2/state.js.
3
3
  *
4
- * Two pieces of state, tracked separately:
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
- * Both helpers are pure I/O; no LLM, no logic beyond parsing.
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
  /**
@@ -2418,28 +2418,55 @@ export function __testGetRegisteredThreadIds() {
2418
2418
  export const __testRaceWithEscalation = raceWithEscalation;
2419
2419
 
2420
2420
  /**
2421
- * Manual dream trigger from VP detail page.
2421
+ * Manual dream trigger.
2422
+ *
2423
+ * Two call shapes, both routed through this single handler:
2424
+ *
2425
+ * { type: 'unify_dream_trigger', vpId } — per-VP trigger (legacy
2426
+ * VP-detail page button). Fires an unscoped dream pass; the result
2427
+ * event is tagged with `vpId` so the per-VP store row updates.
2428
+ *
2429
+ * { type: 'unify_dream_trigger', groupId } — per-GROUP trigger (new
2430
+ * in v0.1.754 — added so users can manually kick dream for a group
2431
+ * after seeing the Resident layer stuck on the bootstrap seed).
2432
+ * Fires a scope-filtered pass via `triggerDreamForScopes(['group/X'])`
2433
+ * so unrelated groups don't get processed; the result event is
2434
+ * tagged with `groupId` for the per-group UI row.
2435
+ *
2436
+ * Backwards-compat: when neither field is set, defaults to `vpId='default'`
2437
+ * which matches the pre-v0.1.754 behavior.
2422
2438
  */
2423
2439
  export async function handleUnifyDreamTrigger(msg = {}) {
2440
+ // Resolve tag up-front so EVERY outbound envelope (including the
2441
+ // scheduler-uninitialised early-return below) carries `groupId` /
2442
+ // `vpId`. Without this the frontend's `applyDreamResult` couldn't
2443
+ // route the error event back to the right row and the per-group
2444
+ // "Run dream now" button would stay stuck on "Running…" forever
2445
+ // (review feedback from PR #757).
2446
+ const groupId = typeof msg.groupId === 'string' && msg.groupId ? msg.groupId : null;
2447
+ const vpId = !groupId ? (msg.vpId || 'default') : null;
2448
+ const tag = groupId ? { groupId } : { vpId };
2449
+
2424
2450
  if (!session?.dreamScheduler) {
2425
2451
  sendToServer({
2426
2452
  type: 'unify_dream_result',
2453
+ ...tag,
2427
2454
  success: false,
2428
2455
  error: 'Dream scheduler not initialized — session not loaded.',
2429
2456
  });
2430
2457
  return;
2431
2458
  }
2432
2459
 
2433
- const vpId = msg.vpId || 'default';
2434
-
2435
2460
  try {
2436
2461
  sendToServer({
2437
2462
  type: 'unify_dream_status',
2438
- vpId,
2463
+ ...tag,
2439
2464
  status: 'running',
2440
2465
  });
2441
2466
 
2442
- const result = await session.dreamScheduler.triggerDreamNow();
2467
+ const result = groupId
2468
+ ? await session.dreamScheduler.triggerDreamForScopes([`group/${groupId}`])
2469
+ : await session.dreamScheduler.triggerDreamNow();
2443
2470
 
2444
2471
  // fix/dream-cadence-and-ui-trigger: derive a single "entries
2445
2472
  // created" count for the UI bubble. The runner returns a richer
@@ -2457,7 +2484,7 @@ export async function handleUnifyDreamTrigger(msg = {}) {
2457
2484
  // PR #743.
2458
2485
  sendToServer({
2459
2486
  type: 'unify_dream_result',
2460
- vpId,
2487
+ ...tag,
2461
2488
  ...result,
2462
2489
  success: !result.error && !result.skipped,
2463
2490
  entriesCreated,
@@ -2466,7 +2493,7 @@ export async function handleUnifyDreamTrigger(msg = {}) {
2466
2493
  } catch (err) {
2467
2494
  sendToServer({
2468
2495
  type: 'unify_dream_result',
2469
- vpId,
2496
+ ...tag,
2470
2497
  success: false,
2471
2498
  error: err?.message || String(err),
2472
2499
  });