@yeaft/webchat-agent 0.1.711 → 0.1.712

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.711",
3
+ "version": "0.1.712",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/unify/config.js CHANGED
@@ -217,8 +217,6 @@ function loadLegacyConfig(dir, overrides) {
217
217
  maxContinueTurns: overrides.maxContinueTurns ?? fileConfig.maxContinueTurns ?? DEFAULTS.maxContinueTurns,
218
218
  // task-318: legacy path never had the `unify` section — defaults.
219
219
  unify: normaliseUnifySection(null),
220
- // H2-AMS feature flag. Default true. Override wins.
221
- memoryV2: overrides.memoryV2 !== undefined ? !!overrides.memoryV2 : true,
222
220
  providers: null,
223
221
  primaryModel: null,
224
222
  fastModel: null,
@@ -315,16 +313,6 @@ export function loadConfig(overrides = {}) {
315
313
  // don't pollute the flat config namespace used by chat/crew code.
316
314
  unify: normaliseUnifySection(jsonConfig.unify),
317
315
 
318
- // H2-AMS feature flag. When true the session opens the FTS5
319
- // SegmentIndex (used by groups/pre-flow.js → memory/preflow.js
320
- // for pre-turn recall) and wires the v2 dream pipeline
321
- // (dream-v2/runner.js). When false both are skipped — no recall,
322
- // no dream — turns still work but without memory injection. The
323
- // legacy R6 recall + dream-scheduler paths have been deleted, so
324
- // `false` is now a "memory off" kill switch rather than a fallback.
325
- memoryV2: overrides.memoryV2 !== undefined ? !!overrides.memoryV2
326
- : (jsonConfig.memoryV2 !== undefined ? !!jsonConfig.memoryV2 : true),
327
-
328
316
  // Legacy fields (null when using config.json)
329
317
  apiKey: overrides.apiKey || null,
330
318
  openaiApiKey: null,
@@ -10,7 +10,7 @@
10
10
  * malformed values fall back to default.
11
11
  */
12
12
 
13
- export const DREAM_INTERVAL_HOURS = 12;
13
+ export const DREAM_INTERVAL_HOURS = 1;
14
14
  export const DREAM_OVERLAP = 3;
15
15
  export const MIN_NEW_PER_GROUP = 20;
16
16
  export const MAX_SINGLE_MESSAGE_CHARS = 8000;
@@ -18,6 +18,11 @@ export const MAX_DIFF_TOKENS_PER_TRIAGE = 60000;
18
18
  export const MAX_APPLY_TOKENS = 80000;
19
19
  export const DREAM_BACKUP_KEEP = 7;
20
20
 
21
+ // task-710: nudge dream off the 1h timer when a group has accumulated
22
+ // this many user messages since the last successful pass. Keeps memory
23
+ // fresh during heavy chat windows without rivalling user latency.
24
+ export const DREAM_NUDGE_AFTER_MESSAGES = 50;
25
+
21
26
  export const DEFAULT_LIMITS = Object.freeze({
22
27
  DREAM_INTERVAL_HOURS,
23
28
  DREAM_OVERLAP,
@@ -26,6 +31,7 @@ export const DEFAULT_LIMITS = Object.freeze({
26
31
  MAX_DIFF_TOKENS_PER_TRIAGE,
27
32
  MAX_APPLY_TOKENS,
28
33
  DREAM_BACKUP_KEEP,
34
+ DREAM_NUDGE_AFTER_MESSAGES,
29
35
  });
30
36
 
31
37
  /**
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * dream-v2/schedule.js.
3
3
  *
4
- * Two trigger paths:
4
+ * Three trigger paths:
5
5
  *
6
- * 1. 12-hour interval timer.
6
+ * 1. Interval timer (default 1 hour, see DREAM_INTERVAL_HOURS in limits.js).
7
7
  * 2. Manual trigger (UI button or `/dream` command), routed in via
8
8
  * `triggerNow()` — sets `manual: true` so the per-group threshold
9
9
  * is bypassed.
10
+ * 3. Nudge (task-710), routed in via `nudge()` — non-manual, so
11
+ * MIN_NEW_PER_GROUP still applies. Used by session-wiring when
12
+ * user-message traffic crosses DREAM_NUDGE_AFTER_MESSAGES.
10
13
  *
11
14
  * The scheduler is a thin wrapper around `runDream()` that prevents
12
15
  * concurrent passes (a second tick while the previous is still running
@@ -66,6 +69,14 @@ export function createDreamScheduler({ run, intervalMs = DEFAULT_INTERVAL_MS, lo
66
69
  triggerNow(scopeFilter) {
67
70
  return fire({ manual: true, scopeFilter });
68
71
  },
72
+ /**
73
+ * task-710: non-manual fire driven by user-message traffic. Unlike
74
+ * `triggerNow`, MIN_NEW_PER_GROUP still applies — groups below
75
+ * threshold are skipped exactly as on the timer path.
76
+ */
77
+ nudge() {
78
+ return fire({ manual: false });
79
+ },
69
80
  isRunning() { return !!inflight; },
70
81
  /** Test hook: fires once without scheduling a timer. */
71
82
  _fire: fire,
@@ -5,14 +5,15 @@
5
5
  * to the live yeaft session: groups store, conversation log, LLM adapter,
6
6
  * and the engine's progress event sink.
7
7
  *
8
- * The dream pipeline only activates when `config.memoryV2 === true`. When
9
- * the flag is off, this module is a no-op.
8
+ * Memory v2 is the only path. The legacy `config.memoryV2` opt-out flag was
9
+ * retired (task-710) the wiring is unconditional.
10
10
  */
11
11
 
12
12
  import { join } from 'path';
13
13
  import { runDream } from './runner.js';
14
14
  import { createDreamScheduler } from './schedule.js';
15
15
  import { listGroups, openGroup } from '../groups/group-store.js';
16
+ import { DREAM_NUDGE_AFTER_MESSAGES } from './limits.js';
16
17
 
17
18
  /**
18
19
  * Build the per-call options for runDream. Pure: takes a session and returns
@@ -157,14 +158,91 @@ export function createV2DreamScheduler(session) {
157
158
  });
158
159
  // Auto-start the timer.
159
160
  v2.start();
161
+
162
+ // task-710: wire `noteUserMessage` to a real per-session message
163
+ // counter. When the count crosses DREAM_NUDGE_AFTER_MESSAGES (default
164
+ // 50) we kick off a non-manual dream pass and reset the counter.
165
+ // Non-manual still respects MIN_NEW_PER_GROUP per-group, so groups
166
+ // below threshold are still skipped — the nudge just frees us from
167
+ // waiting for the 1h timer when traffic is high.
168
+ //
169
+ // Counter resets on fire-attempt (not completion). If a pass is
170
+ // already in flight when we hit threshold, we CLAMP at threshold
171
+ // rather than letting the counter accumulate unbounded — otherwise
172
+ // the first message after the in-flight pass settles would fire
173
+ // immediately, defeating the 50-message guarantee.
174
+ let messagesSinceLastNudgeFire = 0;
175
+ function nudgeOnUserMessage() {
176
+ messagesSinceLastNudgeFire += 1;
177
+ if (messagesSinceLastNudgeFire < DREAM_NUDGE_AFTER_MESSAGES) return;
178
+ if (v2.isRunning()) {
179
+ // Clamp; don't accumulate. We want the next fire to wait another
180
+ // full DREAM_NUDGE_AFTER_MESSAGES once the in-flight pass clears.
181
+ messagesSinceLastNudgeFire = DREAM_NUDGE_AFTER_MESSAGES;
182
+ return;
183
+ }
184
+ messagesSinceLastNudgeFire = 0;
185
+ // Fire-and-forget; failure flows through the scheduler's catch.
186
+ v2.nudge().catch(() => {});
187
+ }
188
+
160
189
  // Adapter shim: legacy callers (web-bridge) call .noteUserMessage() and
161
190
  // .triggerDreamNow() / .shutdown(). Map them onto the v2 API.
162
191
  return {
163
- noteUserMessage() { /* v2 doesn't gate on user-message count */ },
192
+ noteUserMessage: nudgeOnUserMessage,
164
193
  triggerDreamNow() { return v2.triggerNow(); },
194
+ triggerDreamForScopes(scopeFilter) { return v2.triggerNow(scopeFilter); },
165
195
  shutdown() { v2.stop(); },
166
196
  get isRunning() { return v2.isRunning(); },
167
197
  // Preserve direct access for tests.
168
198
  _v2: v2,
169
199
  };
170
200
  }
201
+
202
+ /**
203
+ * task-710: walk every group on disk, find the ones that have user messages
204
+ * but zero memory segments in the FTS index, and trigger an immediate dream
205
+ * pass scoped to those groups. Used at session boot so a freshly opened
206
+ * agent doesn't have to wait an hour (or for traffic to cross the nudge
207
+ * threshold) before its first memory write.
208
+ *
209
+ * Pure side-effect, fire-and-forget. Failure logs at debug only — never
210
+ * blocks session load.
211
+ *
212
+ * @param {{ yeaftDir: string, memoryIndex: import('../memory/index-db.js').SegmentIndex|null, dreamScheduler: { triggerDreamForScopes: (s:string[]) => Promise<any> }, config?: { debug?: boolean } }} args
213
+ * @returns {Promise<{ triggered: string[] }>}
214
+ */
215
+ export async function bootInitEmptyGroups(args) {
216
+ const out = { triggered: [] };
217
+ if (!args || !args.memoryIndex || !args.dreamScheduler) return out;
218
+ const groupsRoot = join(args.yeaftDir, 'groups');
219
+ let ids;
220
+ try { ids = listGroups(groupsRoot).map(g => g.id); }
221
+ catch { return out; }
222
+ const empty = [];
223
+ for (const gid of ids) {
224
+ let segCount;
225
+ try { segCount = args.memoryIndex.listByScope(`group/${gid}`).length; }
226
+ catch { continue; }
227
+ if (segCount > 0) continue;
228
+ let hasMessages = false;
229
+ try {
230
+ const h = openGroup(groupsRoot, gid);
231
+ // Any message at all is enough — pull the first record off the
232
+ // iterator and stop.
233
+ const first = h.streamMessages().next();
234
+ hasMessages = !first.done;
235
+ } catch { continue; }
236
+ if (!hasMessages) continue;
237
+ empty.push(`group/${gid}`);
238
+ }
239
+ if (empty.length === 0) return out;
240
+ if (args.config?.debug) {
241
+ // eslint-disable-next-line no-console
242
+ console.log(`[dream-v2] boot init: triggering empty-AMS dream for ${empty.length} group(s):`, empty);
243
+ }
244
+ // Fire-and-forget; the scheduler swallows its own failures.
245
+ Promise.resolve(args.dreamScheduler.triggerDreamForScopes(empty)).catch(() => {});
246
+ out.triggered = empty;
247
+ return out;
248
+ }
package/unify/engine.js CHANGED
@@ -574,8 +574,6 @@ export class Engine {
574
574
  try {
575
575
  const result = await runAdjust({
576
576
  trigger: {
577
- newMemoryWritten: false, // dream writes happen async; treat as false here
578
- onDemandSize: ctx.ams.onDemandIds().length,
579
577
  turnTokenUsage: args.turnTokenUsage,
580
578
  totalBudget,
581
579
  adjustRanThisSession,
@@ -12,23 +12,25 @@
12
12
  * manipulates AMS membership.
13
13
  *
14
14
  * Triggered conditionally — typical session shape is "hot turn skips,
15
- * adjust runs every 5–10 turns or on first turn":
15
+ * adjust runs once per session or under budget pressure":
16
16
  *
17
17
  * shouldRunAdjust =
18
- * (newMemoryWritten && onDemand.size >= 5)
19
- * || (turnTokenUsage > totalBudget * 0.9)
20
- * || (!session.adjustRanThisSession) // first-turn guarantee
18
+ * (!session.adjustRanThisSession) // first-turn guarantee
19
+ * || (turnTokenUsage > totalBudget * 0.9) // budget pressure
21
20
  *
22
21
  * The trigger lives at the call site (engine post-turn hook); this
23
22
  * module just exposes the policy + the LLM round-trip.
23
+ *
24
+ * task-710: the legacy `newMemoryWritten + onDemand >= 5` trigger was
25
+ * dropped — dream writes happen async on a background timer, so the
26
+ * caller had no good signal to pass and was hard-coding `false`. Adjust
27
+ * now relies on first-turn-guarantee + budget-pressure only.
24
28
  */
25
29
 
26
30
  import { approxTokens } from './budget.js';
27
31
 
28
32
  /**
29
33
  * @typedef {object} AdjustTriggerInput
30
- * @property {boolean} newMemoryWritten
31
- * @property {number} onDemandSize
32
34
  * @property {number} turnTokenUsage
33
35
  * @property {number} totalBudget
34
36
  * @property {boolean} adjustRanThisSession
@@ -48,9 +50,6 @@ export function shouldRunAdjust(input) {
48
50
  if (input.turnTokenUsage > input.totalBudget * 0.9) {
49
51
  return { run: true, reason: 'budget-pressure' };
50
52
  }
51
- if (input.newMemoryWritten && input.onDemandSize >= 5) {
52
- return { run: true, reason: 'new-memory+onDemand-saturated' };
53
- }
54
53
  return { run: false, reason: 'no-trigger' };
55
54
  }
56
55
 
package/unify/session.js CHANGED
@@ -26,11 +26,12 @@ import { Engine } from './engine.js';
26
26
  // H2.f.5: threads/, pipeline/dispatcher and input-queue retired. The
27
27
  // session now exposes a single Engine.
28
28
  //
29
- // GC.1 (final): when config.memoryV2 is on, the session opens a
30
- // SegmentIndex (SQLite FTS5 over memory.md) and passes it to the
31
- // Engine. Engine.#recallMemory routes pre-turn recall through
32
- // groups/pre-flow.js memory/preflow.js (the previous per-scope
33
- // file reader recall-v2.js has been deleted).
29
+ // GC.1 (final): the session opens a SegmentIndex (SQLite FTS5 over
30
+ // memory.md) and passes it to the Engine. Engine.#recallMemory routes
31
+ // pre-turn recall through groups/pre-flow.js memory/preflow.js (the
32
+ // previous per-scope file reader recall-v2.js has been deleted).
33
+ // The `config.memoryV2` opt-out flag was retired in task-710; wiring is
34
+ // unconditional.
34
35
  //
35
36
  // GC.1 follow-up: when memoryIndex is wired we also open an
36
37
  // AmsRegistry. The registry caches per-group ActiveMemorySet
@@ -42,7 +43,7 @@ import { Engine } from './engine.js';
42
43
  import { ensureDefaultGroupIfEmpty } from './groups/group-crud.js';
43
44
  import { seedDefaultVps } from './vp/seed-defaults.js';
44
45
  import { runSummaryBackfill } from './memory/seed-backfill.js';
45
- import { createV2DreamScheduler } from './dream-v2/session-wiring.js';
46
+ import { createV2DreamScheduler, bootInitEmptyGroups } from './dream-v2/session-wiring.js';
46
47
  import { openSegmentIndex } from './memory/index-db.js';
47
48
  import { syncAll as syncSegmentIndex } from './memory/segment-sync.js';
48
49
  import { openAmsRegistry } from './memory/ams-registry.js';
@@ -169,15 +170,14 @@ export async function loadSession(options = {}) {
169
170
  const conversationStore = new ConversationStore(yeaftDir);
170
171
 
171
172
  // ─── 5-fts. (GC.1) Open SegmentIndex for FTS pre-flow ────
172
- // When config.memoryV2 is on, build a SQLite FTS5 index over
173
- // ~/.yeaft/memory/<scope>/memory.md and pass it to the Engine.
174
- // Engine.#recallMemory uses it via groups/pre-flow.js →
175
- // memory/preflow.js. Disk is the source of truth; on boot we
176
- // reconcile disk index via syncAll. Failure to open the index
177
- // is non-fatal: #recallMemory returns an empty result and the
178
- // turn proceeds without pre-injected memory.
173
+ // Build a SQLite FTS5 index over ~/.yeaft/memory/<scope>/memory.md
174
+ // and pass it to the Engine. Engine.#recallMemory uses it via
175
+ // groups/pre-flow.js → memory/preflow.js. Disk is the source of
176
+ // truth; on boot we reconcile disk index via syncAll. Failure
177
+ // to open the index is non-fatal: #recallMemory returns an empty
178
+ // result and the turn proceeds without pre-injected memory.
179
179
  let memoryIndex = null;
180
- if (config.memoryV2 && !config._readOnly) {
180
+ if (!config._readOnly) {
181
181
  try {
182
182
  const indexPath = join(yeaftDir, 'memory', 'index.db');
183
183
  memoryIndex = openSegmentIndex(indexPath);
@@ -300,8 +300,8 @@ export async function loadSession(options = {}) {
300
300
 
301
301
  // ─── 9a. Create dream scheduler ────────────
302
302
  // The legacy R6 dream-scheduler was retired alongside recall-r6;
303
- // dream-v2 is the only active path. The `memoryV2: false` opt-out
304
- // no longer leaves a usable system, so we always wire v2 here.
303
+ // dream-v2 is the only active path (the `config.memoryV2` opt-out
304
+ // flag was retired in task-710 wiring is unconditional).
305
305
  // partialSession lets the v2 scheduler dereference adapter/config/
306
306
  // engine/trace lazily — safe because callers attach more fields
307
307
  // after this line.
@@ -314,6 +314,20 @@ export async function loadSession(options = {}) {
314
314
  };
315
315
  const dreamScheduler = createV2DreamScheduler(partialSession);
316
316
 
317
+ // task-710: kick a dream pass at boot for any group that has user
318
+ // messages but zero memory segments in the FTS index. Without this a
319
+ // freshly opened agent had to wait an hour (or for the nudge counter
320
+ // to cross 50) before the first segment landed and recall could find
321
+ // anything. Fire-and-forget; failure logs at debug only.
322
+ if (memoryIndex && !config._readOnly) {
323
+ bootInitEmptyGroups({
324
+ yeaftDir,
325
+ memoryIndex,
326
+ dreamScheduler,
327
+ config,
328
+ }).catch(() => { /* best-effort boot init */ });
329
+ }
330
+
317
331
  // H2.f.5: thread engine registry, input queue, and dispatcher retired.
318
332
  // The session exposes a single `engine`; web-bridge calls engine.query()
319
333
  // directly. Memory recall happens via memory/preflow.js (pre-turn) and