@yeaft/webchat-agent 1.0.456 → 1.0.458
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/connection/message-router.js +21 -2
- package/local-runtime/server/handlers/agent-sync.js +19 -0
- package/local-runtime/server/handlers/client-misc.js +8 -0
- package/local-runtime/server/ws-utils.js +1 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +33 -32
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/config.js +2 -0
- package/yeaft/dream/segment-extract.js +4 -4
- package/yeaft/dream/session-wiring.js +21 -4
- package/yeaft/dream/triage.js +83 -27
- package/yeaft/session.js +2 -0
|
Binary file
|
package/package.json
CHANGED
package/yeaft/config.js
CHANGED
|
@@ -377,6 +377,7 @@ function loadLegacyConfig(dir, overrides) {
|
|
|
377
377
|
// task-318: legacy path never had the `yeaft` section — defaults.
|
|
378
378
|
yeaft: normaliseYeaftSection(null),
|
|
379
379
|
telemetry: normaliseTelemetrySection(null),
|
|
380
|
+
dream: { enabled: true },
|
|
380
381
|
browserRuntime: normaliseBrowserRuntimeSection(null),
|
|
381
382
|
plugins: {},
|
|
382
383
|
providers: null,
|
|
@@ -534,6 +535,7 @@ export function loadConfig(overrides = {}) {
|
|
|
534
535
|
// don't pollute the flat config namespace used by chat code.
|
|
535
536
|
yeaft: normaliseYeaftSection(jsonConfig.yeaft),
|
|
536
537
|
telemetry: normaliseTelemetrySection(jsonConfig.telemetry),
|
|
538
|
+
dream: { enabled: jsonConfig.dream?.enabled !== false },
|
|
537
539
|
browserRuntime: normaliseBrowserRuntimeSection(jsonConfig.browserRuntime),
|
|
538
540
|
|
|
539
541
|
// Agent-level tools / skills / MCP server allowlists. Missing fields mean
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dream/segment-extract.js.
|
|
3
3
|
*
|
|
4
|
-
* Extract atomic evidence segments for scopes touched by a
|
|
5
|
-
* persist them through memory/segment-store. Dream Apply
|
|
6
|
-
* canonical content.md and the short catalog summary.md.
|
|
4
|
+
* Extract atomic evidence segments only for scopes successfully touched by a
|
|
5
|
+
* Dream pass and persist them through memory/segment-store. Dream Apply
|
|
6
|
+
* separately maintains canonical content.md and the short catalog summary.md.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { readScope, writeScope } from '../memory/segment-store.js';
|
|
@@ -144,7 +144,7 @@ function templateVarsForScope(scope, sessionId) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
function normalizeTargetScopes(sessionId, targets) {
|
|
147
|
-
const out = new Set(
|
|
147
|
+
const out = new Set();
|
|
148
148
|
for (const target of targets) {
|
|
149
149
|
if (typeof target !== 'string') continue;
|
|
150
150
|
const clean = target.trim();
|
|
@@ -668,8 +668,8 @@ export function createV2DreamScheduler(session) {
|
|
|
668
668
|
keepAlive: !!session?.config?.serverMode,
|
|
669
669
|
logger: session.config?.debug ? console : undefined,
|
|
670
670
|
});
|
|
671
|
-
|
|
672
|
-
v2.start();
|
|
671
|
+
let enabled = session?.config?.dream?.enabled !== false;
|
|
672
|
+
if (enabled) v2.start();
|
|
673
673
|
|
|
674
674
|
// task-710: wire `noteUserMessage` to a real per-session message
|
|
675
675
|
// counter. When the count crosses DREAM_NUDGE_AFTER_MESSAGES (default
|
|
@@ -685,6 +685,7 @@ export function createV2DreamScheduler(session) {
|
|
|
685
685
|
// immediately, defeating the 50-message guarantee.
|
|
686
686
|
let messagesSinceLastNudgeFire = 0;
|
|
687
687
|
function nudgeOnUserMessage() {
|
|
688
|
+
if (!enabled) return;
|
|
688
689
|
messagesSinceLastNudgeFire += 1;
|
|
689
690
|
if (messagesSinceLastNudgeFire < DREAM_NUDGE_AFTER_MESSAGES) return;
|
|
690
691
|
if (v2.isRunning()) {
|
|
@@ -709,8 +710,20 @@ export function createV2DreamScheduler(session) {
|
|
|
709
710
|
* below threshold are skipped — same shape as the interval timer
|
|
710
711
|
* tick. Used by `bootCatchUpStaleDream`.
|
|
711
712
|
*/
|
|
712
|
-
catchUpNudge() {
|
|
713
|
+
catchUpNudge() {
|
|
714
|
+
return enabled ? v2.nudge() : Promise.resolve({ skipped: true, skippedReason: 'disabled', trigger: 'auto' });
|
|
715
|
+
},
|
|
716
|
+
setEnabled(nextEnabled) {
|
|
717
|
+
enabled = nextEnabled !== false;
|
|
718
|
+
if (enabled) v2.start();
|
|
719
|
+
else {
|
|
720
|
+
messagesSinceLastNudgeFire = 0;
|
|
721
|
+
v2.stop();
|
|
722
|
+
}
|
|
723
|
+
return enabled;
|
|
724
|
+
},
|
|
713
725
|
shutdown() { v2.stop(); },
|
|
726
|
+
get enabled() { return enabled; },
|
|
714
727
|
get isRunning() { return v2.isRunning(); },
|
|
715
728
|
// Preserve direct access for tests.
|
|
716
729
|
_v2: v2,
|
|
@@ -727,12 +740,16 @@ export function createV2DreamScheduler(session) {
|
|
|
727
740
|
* Pure side-effect, fire-and-forget. Failure logs at debug only — never
|
|
728
741
|
* blocks session load.
|
|
729
742
|
*
|
|
730
|
-
* @param {{ yeaftDir: string, memoryIndex: import('../memory/index-db.js').SegmentIndex|null, dreamScheduler: { triggerDreamForScopes: (s:string[]) => Promise<any> }, config?: { debug?: boolean } }} args
|
|
743
|
+
* @param {{ yeaftDir: string, memoryIndex: import('../memory/index-db.js').SegmentIndex|null, dreamScheduler: { triggerDreamForScopes: (s:string[]) => Promise<any> }, config?: { debug?: boolean, dream?: { enabled?: boolean } } }} args
|
|
731
744
|
* @returns {Promise<{ triggered: string[] }>}
|
|
732
745
|
*/
|
|
733
746
|
export async function bootInitEmptyGroups(args) {
|
|
734
747
|
const out = { triggered: [] };
|
|
735
748
|
if (!args || !args.memoryIndex || !args.dreamScheduler) return out;
|
|
749
|
+
// This is automatic boot catch-up, not an explicit user trigger. The
|
|
750
|
+
// scheduler's scoped trigger intentionally remains manual-capable while
|
|
751
|
+
// disabled, so enforce the Agent toggle at this automatic entry point.
|
|
752
|
+
if (args.config?.dream?.enabled === false) return out;
|
|
736
753
|
const sessionsRoot = join(args.yeaftDir, 'sessions');
|
|
737
754
|
let ids;
|
|
738
755
|
try { ids = listSessionMetas(sessionsRoot).map(g => g.id); }
|
package/yeaft/dream/triage.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { inspect } from 'util';
|
|
2
|
+
|
|
1
3
|
import { isValidTopic } from '../memory/store.js';
|
|
2
4
|
/**
|
|
3
5
|
* dream/triage.js.
|
|
@@ -6,9 +8,10 @@ import { isValidTopic } from '../memory/store.js';
|
|
|
6
8
|
* The decision is two-staged on purpose:
|
|
7
9
|
*
|
|
8
10
|
* 1. **Hard rules** (this module, no LLM): everything we can determine
|
|
9
|
-
* from message metadata. Always include the active session
|
|
10
|
-
* that spoke as an assistant in the diff
|
|
11
|
-
*
|
|
11
|
+
* from message metadata. Always include the active session and every VP
|
|
12
|
+
* that spoke as an assistant in the diff. User-profile scopes are not a
|
|
13
|
+
* structural property, so they are added only when soft classification
|
|
14
|
+
* confirms a durable profile signal. (Feature scope was dropped
|
|
12
15
|
* 2026-05-13 along with the rest of the Feature system.)
|
|
13
16
|
*
|
|
14
17
|
* 2. **Soft classification** (LLM, two passes):
|
|
@@ -18,14 +21,15 @@ import { isValidTopic } from '../memory/store.js';
|
|
|
18
21
|
* it to an exact existing path or propose
|
|
19
22
|
* a new ≤2-level path.
|
|
20
23
|
*
|
|
21
|
-
* VP /
|
|
24
|
+
* VP / Session are deliberately NOT asked of the LLM — Hard Rules
|
|
22
25
|
* already cover them, and giving the LLM a chance to drop a
|
|
23
26
|
* structurally-required scope would weaken the contract.
|
|
24
27
|
*
|
|
25
|
-
* `user_profile_signals === true` does not need Pass-2
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
28
|
+
* `user_profile_signals === true` does not need Pass-2: it directly
|
|
29
|
+
* adds the global `user` scope and, for a real collaborative Session,
|
|
30
|
+
* the Session-local `sessions/<id>/user` scope. Keeping those scopes
|
|
31
|
+
* out when the classifier says false avoids two Apply rewrites and two
|
|
32
|
+
* extraction requests for ordinary task-focused conversation.
|
|
29
33
|
*
|
|
30
34
|
* The LLM is injected as a callable: `llm({ pass, prompt, system })` →
|
|
31
35
|
* Promise<string>. Tests pass a stub; runner injects the real adapter.
|
|
@@ -63,8 +67,8 @@ export function applyHardRules({ sessionId, chatId, messages }) {
|
|
|
63
67
|
const out = new Map();
|
|
64
68
|
const add = (scope) => { if (!out.has(scope)) out.set(scope, { kind: 'update', scope }); };
|
|
65
69
|
|
|
66
|
-
//
|
|
67
|
-
|
|
70
|
+
// User-profile scopes are content-dependent and are added by soft
|
|
71
|
+
// classification only when the diff contains a durable profile signal.
|
|
68
72
|
|
|
69
73
|
// chat path takes precedence: chat sessions have no collaborative session context.
|
|
70
74
|
if (chatId) {
|
|
@@ -84,7 +88,6 @@ export function applyHardRules({ sessionId, chatId, messages }) {
|
|
|
84
88
|
// active collaborative session, except the virtual _no-session bucket.
|
|
85
89
|
if (sessionId && sessionId !== '_no-session') {
|
|
86
90
|
add(`sessions/${sessionId}`);
|
|
87
|
-
add(`sessions/${sessionId}/user`);
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
for (const m of (messages || [])) {
|
|
@@ -159,18 +162,21 @@ export async function classifySoft({ root, sessionId, messages, topicSummaries,
|
|
|
159
162
|
const pass1Prompt = buildPass1Prompt({ sessionId, messages, topicSummaries, language, maxPromptChars });
|
|
160
163
|
const pass1Raw = await llm({ pass: 'triage-pass1', prompt: pass1Prompt, system: triageSystem(language) });
|
|
161
164
|
const pass1 = parseJsonSafe(pass1Raw);
|
|
165
|
+
if (!isValidPass1(pass1)) {
|
|
166
|
+
throw triageContractError('Pass-1', pass1Raw);
|
|
167
|
+
}
|
|
162
168
|
const out = [];
|
|
163
169
|
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
if (pass1 && pass1.user_profile_signals === true) {
|
|
170
|
+
// User-profile scopes are expensive shared-memory rewrites, not structural
|
|
171
|
+
// Session scopes. Route to them only when Pass-1 finds a durable signal.
|
|
172
|
+
if (pass1.user_profile_signals) {
|
|
168
173
|
out.push({ kind: 'update', scope: 'user' });
|
|
174
|
+
if (sessionId && sessionId !== '_no-session') {
|
|
175
|
+
out.push({ kind: 'update', scope: `sessions/${sessionId}/user` });
|
|
176
|
+
}
|
|
169
177
|
}
|
|
170
178
|
|
|
171
|
-
const
|
|
172
|
-
for (const description of topicDescriptions) {
|
|
173
|
-
if (typeof description !== 'string' || !description.trim()) continue;
|
|
179
|
+
for (const description of pass1.topics) {
|
|
174
180
|
const pass2Prompt = buildPass2Prompt({
|
|
175
181
|
description: description.trim(),
|
|
176
182
|
existingTopics: topicSummaries || [],
|
|
@@ -179,20 +185,18 @@ export async function classifySoft({ root, sessionId, messages, topicSummaries,
|
|
|
179
185
|
});
|
|
180
186
|
const pass2Raw = await llm({ pass: 'triage-pass2', prompt: pass2Prompt, system: triageSystem(language) });
|
|
181
187
|
const pass2 = parseJsonSafe(pass2Raw);
|
|
182
|
-
if (!pass2
|
|
188
|
+
if (!isValidPass2(pass2, { sessionId, topicSummaries })) {
|
|
189
|
+
throw triageContractError('Pass-2', pass2Raw);
|
|
190
|
+
}
|
|
183
191
|
if (pass2.decision === 'none') continue;
|
|
184
|
-
const path =
|
|
185
|
-
if (!path) continue;
|
|
186
|
-
const segs = path.split('/').filter(Boolean);
|
|
187
|
-
if (!sessionId || sessionId === '_no-session') continue;
|
|
188
|
-
if (!isValidTopic({ kind: 'session-topic', sessionId, path: segs })) continue;
|
|
192
|
+
const path = pass2.path;
|
|
189
193
|
const redirected = root
|
|
190
|
-
? resolveTopicRedirect(root, sessionId,
|
|
191
|
-
:
|
|
194
|
+
? resolveTopicRedirect(root, sessionId, path)
|
|
195
|
+
: path;
|
|
192
196
|
const scope = `sessions/${sessionId}/topic/${redirected}`;
|
|
193
197
|
if (pass2.decision === 'match') {
|
|
194
198
|
out.push({ kind: 'update', scope });
|
|
195
|
-
} else
|
|
199
|
+
} else {
|
|
196
200
|
out.push({ kind: 'create', scope });
|
|
197
201
|
}
|
|
198
202
|
}
|
|
@@ -268,6 +272,58 @@ function oneLine(s) {
|
|
|
268
272
|
return String(s || '').replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
269
273
|
}
|
|
270
274
|
|
|
275
|
+
function isPlainObject(value) {
|
|
276
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function hasExactKeys(value, expected) {
|
|
280
|
+
const keys = Object.keys(value).sort();
|
|
281
|
+
return keys.length === expected.length && keys.every((key, index) => key === expected[index]);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isValidPass1(value) {
|
|
285
|
+
if (!isPlainObject(value)
|
|
286
|
+
|| !hasExactKeys(value, ['topics', 'trivial_only', 'user_profile_signals'])
|
|
287
|
+
|| typeof value.user_profile_signals !== 'boolean'
|
|
288
|
+
|| typeof value.trivial_only !== 'boolean'
|
|
289
|
+
|| !Array.isArray(value.topics)
|
|
290
|
+
|| !value.topics.every(topic => typeof topic === 'string' && topic.trim().length > 0)) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return !value.trivial_only || (!value.user_profile_signals && value.topics.length === 0);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function isValidPass2(value, { sessionId, topicSummaries }) {
|
|
297
|
+
if (!isPlainObject(value) || !['match', 'new', 'none'].includes(value.decision)) return false;
|
|
298
|
+
if (value.decision === 'none') {
|
|
299
|
+
return hasExactKeys(value, ['decision']);
|
|
300
|
+
}
|
|
301
|
+
if (!hasExactKeys(value, ['decision', 'path'])
|
|
302
|
+
|| !sessionId
|
|
303
|
+
|| sessionId === '_no-session'
|
|
304
|
+
|| typeof value.path !== 'string'
|
|
305
|
+
|| value.path !== value.path.trim()) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
const path = value.path;
|
|
309
|
+
const segments = path.split('/');
|
|
310
|
+
if (!isValidTopic({ kind: 'session-topic', sessionId, path: segments })) return false;
|
|
311
|
+
const pathExists = (topicSummaries || []).some(topic => topic?.path === path);
|
|
312
|
+
return value.decision === 'match' ? pathExists : !pathExists;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function triageContractError(pass, raw) {
|
|
316
|
+
const err = new Error(`triage: ${pass} returned malformed JSON`);
|
|
317
|
+
err.rawSnippet = rawResponseSnippet(raw);
|
|
318
|
+
return err;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function rawResponseSnippet(raw) {
|
|
322
|
+
if (typeof raw === 'string') return raw.slice(0, 1000);
|
|
323
|
+
if (raw == null) return String(raw);
|
|
324
|
+
return inspect(raw, { depth: 2, maxArrayLength: 10, breakLength: 120 }).slice(0, 1000);
|
|
325
|
+
}
|
|
326
|
+
|
|
271
327
|
/** Lenient JSON parse: tolerate fenced ```json blocks. Returns null on failure. */
|
|
272
328
|
export function parseJsonSafe(raw) {
|
|
273
329
|
if (typeof raw !== 'string') return null;
|
package/yeaft/session.js
CHANGED
|
@@ -136,6 +136,7 @@ export async function loadSession(options = {}) {
|
|
|
136
136
|
extraTools = [],
|
|
137
137
|
configOverrides = {},
|
|
138
138
|
serverMode = false,
|
|
139
|
+
dreamEnabled,
|
|
139
140
|
managedCliReady = null,
|
|
140
141
|
} = options;
|
|
141
142
|
|
|
@@ -172,6 +173,7 @@ export async function loadSession(options = {}) {
|
|
|
172
173
|
// can decide whether to keep its interval timer alive (server) or
|
|
173
174
|
// unref it (CLI / tests). Non-persisted — set per-session by caller.
|
|
174
175
|
if (serverMode) config.serverMode = true;
|
|
176
|
+
if (typeof dreamEnabled === 'boolean') config.dream.enabled = dreamEnabled;
|
|
175
177
|
|
|
176
178
|
// Propagate the (clamped) cold-start replay window to the conversation
|
|
177
179
|
// store. The default is 20 turns; a user wanting more recall after a
|