@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 +1 -1
- package/unify/config.js +0 -12
- package/unify/dream-v2/limits.js +7 -1
- package/unify/dream-v2/schedule.js +13 -2
- package/unify/dream-v2/session-wiring.js +81 -3
- package/unify/engine.js +0 -2
- package/unify/memory/adjust.js +8 -9
- package/unify/session.js +30 -16
package/package.json
CHANGED
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,
|
package/unify/dream-v2/limits.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* malformed values fall back to default.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
export const DREAM_INTERVAL_HOURS =
|
|
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
|
-
*
|
|
4
|
+
* Three trigger paths:
|
|
5
5
|
*
|
|
6
|
-
* 1.
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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,
|
package/unify/memory/adjust.js
CHANGED
|
@@ -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
|
|
15
|
+
* adjust runs once per session or under budget pressure":
|
|
16
16
|
*
|
|
17
17
|
* shouldRunAdjust =
|
|
18
|
-
* (
|
|
19
|
-
* || (turnTokenUsage
|
|
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):
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
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
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
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 (
|
|
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
|
|
304
|
-
//
|
|
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
|