@yeaft/webchat-agent 0.1.656 → 0.1.658
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 +10 -0
- package/unify/engine.js +28 -1
- package/unify/groups/coordinator.js +50 -81
- package/unify/groups/pre-flow.js +308 -0
- package/unify/session.js +46 -2
- package/unify/vp/index.js +0 -7
- package/unify/vp/registry.js +10 -117
- package/unify/web-bridge.js +11 -1
- package/unify/vp/core-memory-recall.js +0 -75
- package/unify/vp/engine-binding.js +0 -96
- package/unify/vp/role-instance.js +0 -216
- package/unify/vp/run-turn.js +0 -251
- package/unify/vp/system-prompt.js +0 -311
package/package.json
CHANGED
package/unify/config.js
CHANGED
|
@@ -219,6 +219,8 @@ function loadLegacyConfig(dir, overrides) {
|
|
|
219
219
|
unify: normaliseUnifySection(null),
|
|
220
220
|
// DESIGN-v2 feature flag. Default true (PR-E flipped). Override wins.
|
|
221
221
|
memoryV2: overrides.memoryV2 !== undefined ? !!overrides.memoryV2 : true,
|
|
222
|
+
// GC.1: FTS pre-flow flag (legacy fallback config — defaults true).
|
|
223
|
+
memoryPreflow: overrides.memoryPreflow !== undefined ? !!overrides.memoryPreflow : true,
|
|
222
224
|
providers: null,
|
|
223
225
|
primaryModel: null,
|
|
224
226
|
fastModel: null,
|
|
@@ -323,6 +325,14 @@ export function loadConfig(overrides = {}) {
|
|
|
323
325
|
memoryV2: overrides.memoryV2 !== undefined ? !!overrides.memoryV2
|
|
324
326
|
: (jsonConfig.memoryV2 !== undefined ? !!jsonConfig.memoryV2 : true),
|
|
325
327
|
|
|
328
|
+
// GC.1 feature flag — route pre-turn memory recall through
|
|
329
|
+
// memory/preflow.js (SQLite FTS5) instead of memory/recall-v2.js
|
|
330
|
+
// (per-scope file reads). When OFF the engine falls back to v2.
|
|
331
|
+
// Default ON. Users can opt out via `"memoryPreflow": false` in
|
|
332
|
+
// ~/.yeaft/config.json. Only applies when memoryV2 is also ON.
|
|
333
|
+
memoryPreflow: overrides.memoryPreflow !== undefined ? !!overrides.memoryPreflow
|
|
334
|
+
: (jsonConfig.memoryPreflow !== undefined ? !!jsonConfig.memoryPreflow : true),
|
|
335
|
+
|
|
326
336
|
// Legacy fields (null when using config.json)
|
|
327
337
|
apiKey: overrides.apiKey || null,
|
|
328
338
|
openaiApiKey: null,
|
package/unify/engine.js
CHANGED
|
@@ -22,6 +22,7 @@ import { buildSystemPrompt, buildWorkerPrompt } from './prompts.js';
|
|
|
22
22
|
import { LLMContextError, LLMAbortError } from './llm/adapter.js';
|
|
23
23
|
import { recallR6, formatForInjection } from './memory/recall-r6.js';
|
|
24
24
|
import { recallV2 } from './memory/recall-v2.js';
|
|
25
|
+
import { runMemoryPreflow } from './groups/pre-flow.js';
|
|
25
26
|
import { shouldConsolidate, consolidate, partitionMessages } from './memory/consolidate.js';
|
|
26
27
|
import { extractMemories } from './memory/extract.js';
|
|
27
28
|
import { runCompact as runCompactOrchestrator } from './compact/orchestrator.js';
|
|
@@ -152,6 +153,9 @@ export class Engine {
|
|
|
152
153
|
/** @type {object|null} — R6 memory shard store (task-334f) */
|
|
153
154
|
#memoryShardStore;
|
|
154
155
|
|
|
156
|
+
/** @type {import('./memory/index-db.js').SegmentIndex|null} — GC.1: SQLite FTS5 segment index */
|
|
157
|
+
#memoryIndex;
|
|
158
|
+
|
|
155
159
|
/** @type {import('./tools/registry.js').ToolRegistry|null} */
|
|
156
160
|
#toolRegistry;
|
|
157
161
|
|
|
@@ -223,13 +227,14 @@ export class Engine {
|
|
|
223
227
|
* config: object,
|
|
224
228
|
* conversationStore?: import('./conversation/persist.js').ConversationStore,
|
|
225
229
|
* memoryStore?: import('./memory/store.js').MemoryStore,
|
|
230
|
+
* memoryIndex?: import('./memory/index-db.js').SegmentIndex,
|
|
226
231
|
* toolRegistry?: import('./tools/registry.js').ToolRegistry,
|
|
227
232
|
* skillManager?: import('./skills.js').SkillManager,
|
|
228
233
|
* mcpManager?: import('./mcp.js').MCPManager,
|
|
229
234
|
* yeaftDir?: string,
|
|
230
235
|
* }} params
|
|
231
236
|
*/
|
|
232
|
-
constructor({ adapter, trace, config, conversationStore, memoryStore, memoryShardStore, toolRegistry, skillManager, mcpManager, yeaftDir }) {
|
|
237
|
+
constructor({ adapter, trace, config, conversationStore, memoryStore, memoryShardStore, memoryIndex, toolRegistry, skillManager, mcpManager, yeaftDir }) {
|
|
233
238
|
this.#adapter = adapter;
|
|
234
239
|
this.#trace = trace;
|
|
235
240
|
this.#config = config;
|
|
@@ -238,6 +243,7 @@ export class Engine {
|
|
|
238
243
|
this.#conversationStore = conversationStore || null;
|
|
239
244
|
this.#memoryStore = memoryStore || null;
|
|
240
245
|
this.#memoryShardStore = memoryShardStore || null;
|
|
246
|
+
this.#memoryIndex = memoryIndex || null;
|
|
241
247
|
this.#toolRegistry = toolRegistry || null;
|
|
242
248
|
this.#skillManager = skillManager || null;
|
|
243
249
|
this.#mcpManager = mcpManager || null;
|
|
@@ -505,6 +511,27 @@ export class Engine {
|
|
|
505
511
|
async #recallMemory(prompt, ctx = {}) {
|
|
506
512
|
const memory = { profile: '', entries: [], formatted: '' };
|
|
507
513
|
|
|
514
|
+
// ─── GC.1: FTS5 pre-flow path ──────────────────────────────
|
|
515
|
+
// When the SegmentIndex is wired and the feature flag is on,
|
|
516
|
+
// route recall through groups/pre-flow.js → memory/preflow.js
|
|
517
|
+
// (SQLite FTS5). On any failure fall through to v2.
|
|
518
|
+
if (this.#memoryIndex && this.#config && this.#config.memoryPreflow) {
|
|
519
|
+
try {
|
|
520
|
+
const result = runMemoryPreflow(this.#memoryIndex, {
|
|
521
|
+
userMsg: prompt,
|
|
522
|
+
groupId: ctx.groupId,
|
|
523
|
+
vpId: ctx.vpId,
|
|
524
|
+
featureId: ctx.featureId,
|
|
525
|
+
});
|
|
526
|
+
memory.profile = result.profile || '';
|
|
527
|
+
memory.entries = result.entries || [];
|
|
528
|
+
memory.formatted = result.formatted || '';
|
|
529
|
+
return memory;
|
|
530
|
+
} catch {
|
|
531
|
+
// Fall through to v2 / R6 paths.
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
508
535
|
// ─── v2 path (DESIGN-v2) ───────────────────────────────────
|
|
509
536
|
if (this.#config && this.#config.memoryV2 && this.#yeaftDir) {
|
|
510
537
|
try {
|
|
@@ -2,55 +2,28 @@
|
|
|
2
2
|
* coordinator.js — Group Coordinator (task-334b).
|
|
3
3
|
*
|
|
4
4
|
* Consumes user/VP messages, persists them to the group's 334o jsonl-log,
|
|
5
|
-
*
|
|
6
|
-
* `inputQueue`.
|
|
5
|
+
* and dispatches user-text turns to target RoleInstances'
|
|
6
|
+
* `inputQueue`.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* report; coordinator does not mutate roster on stranger mentions.
|
|
15
|
-
* - No @-mention on a user message → falls back to `defaultVpId` via
|
|
16
|
-
* resolveFallbackVp (architecture G2).
|
|
17
|
-
* - taskId: if the inbound message carries `taskId`, dispatch is scoped to
|
|
18
|
-
* task.members — passed in via `options.taskMembers` (task storage lives
|
|
19
|
-
* in 334n; coordinator only enforces the filter when the caller provides
|
|
20
|
-
* the member list).
|
|
8
|
+
* As of GC.1 Commit B, VP-selection (parseMentions + dispatch matrix:
|
|
9
|
+
* mention / @all / fallback / vp-author no-op) lives in
|
|
10
|
+
* `groups/pre-flow.js` so the same logic can be invoked directly by
|
|
11
|
+
* the parallel fan-out path in web-bridge.js. Coordinator's job is now
|
|
12
|
+
* narrower: persist the message and translate the selection result
|
|
13
|
+
* into deliver() calls.
|
|
21
14
|
*
|
|
22
15
|
* This module DOES NOT run the engine. It only:
|
|
23
16
|
* 1. Persists the message (via GroupHandle.appendMessage)
|
|
24
|
-
* 2.
|
|
17
|
+
* 2. Asks pre-flow for the list of target vpIds
|
|
25
18
|
* 3. Calls a user-supplied deliver(vpId, envelope) per target
|
|
26
|
-
*
|
|
27
|
-
* That lets 334c (RoleInstance/Engine) own how a target is actually woken up
|
|
28
|
-
* (inputQueue push, status transition, etc) without coordinator owning it.
|
|
29
19
|
*/
|
|
30
20
|
|
|
31
|
-
import {
|
|
21
|
+
import { parseMentions, selectRespondingVps } from './pre-flow.js';
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* Extract an ordered, unique list of @-mentions from a text string.
|
|
38
|
-
* Recognises the literal token `@all` as broadcast.
|
|
39
|
-
*/
|
|
40
|
-
export function parseMentions(text) {
|
|
41
|
-
if (!text || typeof text !== 'string') return [];
|
|
42
|
-
const out = [];
|
|
43
|
-
const seen = new Set();
|
|
44
|
-
MENTION_RE.lastIndex = 0;
|
|
45
|
-
let m;
|
|
46
|
-
while ((m = MENTION_RE.exec(text))) {
|
|
47
|
-
const id = m[2];
|
|
48
|
-
if (seen.has(id)) continue;
|
|
49
|
-
seen.add(id);
|
|
50
|
-
out.push(id);
|
|
51
|
-
}
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
23
|
+
// Re-export so existing importers (`createCoordinator(...).parseMentions`,
|
|
24
|
+
// or modules importing `parseMentions` from coordinator) keep working
|
|
25
|
+
// without churn. New code should import from `./pre-flow.js` directly.
|
|
26
|
+
export { parseMentions };
|
|
54
27
|
|
|
55
28
|
/**
|
|
56
29
|
* Build a Group Coordinator bound to a single GroupHandle.
|
|
@@ -101,8 +74,18 @@ export function createCoordinator(group, options = {}) {
|
|
|
101
74
|
role: input.role || (fromUser ? 'user' : 'assistant'),
|
|
102
75
|
});
|
|
103
76
|
|
|
104
|
-
//
|
|
105
|
-
|
|
77
|
+
// Ask pre-flow which VPs (if any) should respond.
|
|
78
|
+
const selection = selectRespondingVps({
|
|
79
|
+
meta,
|
|
80
|
+
fromUser,
|
|
81
|
+
mentions,
|
|
82
|
+
sender: input.from,
|
|
83
|
+
fanOutCap,
|
|
84
|
+
taskMembers: opts.taskMembers,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// VP-authored: persist but no dispatch.
|
|
88
|
+
if (selection.reason === 'vp-author-no-text-routing') {
|
|
106
89
|
return {
|
|
107
90
|
message: stored,
|
|
108
91
|
dispatched: [],
|
|
@@ -112,63 +95,48 @@ export function createCoordinator(group, options = {}) {
|
|
|
112
95
|
};
|
|
113
96
|
}
|
|
114
97
|
|
|
115
|
-
|
|
116
|
-
if (mentions.includes('all')) {
|
|
117
|
-
const roster = meta.roster.filter((v) => v !== input.from).slice(0, fanOutCap);
|
|
118
|
-
const scoped = opts.taskMembers
|
|
119
|
-
? roster.filter((v) => opts.taskMembers.includes(v))
|
|
120
|
-
: roster;
|
|
98
|
+
if (selection.reason === 'broadcast') {
|
|
121
99
|
const envelope = makeEnvelope(stored, meta, 'broadcast');
|
|
122
|
-
for (const vpId of
|
|
100
|
+
for (const vpId of selection.dispatched) deliver(vpId, envelope);
|
|
123
101
|
return {
|
|
124
102
|
message: stored,
|
|
125
|
-
dispatched:
|
|
103
|
+
dispatched: selection.dispatched,
|
|
126
104
|
fallback: null,
|
|
127
|
-
errors:
|
|
105
|
+
errors: selection.errors,
|
|
128
106
|
broadcast: true,
|
|
129
|
-
truncatedAtFanOutCap:
|
|
107
|
+
truncatedAtFanOutCap: !!selection.truncatedAtFanOutCap,
|
|
130
108
|
};
|
|
131
109
|
}
|
|
132
110
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const dispatched = [];
|
|
136
|
-
const errors = [];
|
|
137
|
-
for (const vpId of mentions) {
|
|
138
|
-
if (!isMember(meta, vpId)) {
|
|
139
|
-
errors.push({ vpId, error: 'not_in_roster' });
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (opts.taskMembers && !opts.taskMembers.includes(vpId)) {
|
|
143
|
-
errors.push({ vpId, error: 'not_in_task_members' });
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
dispatched.push(vpId);
|
|
111
|
+
if (selection.reason === 'mention') {
|
|
112
|
+
for (const vpId of selection.dispatched) {
|
|
147
113
|
deliver(vpId, makeEnvelope(stored, meta, 'mention'));
|
|
148
114
|
}
|
|
149
|
-
return { message: stored, dispatched, fallback: null, errors };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// No @-mention → fallback
|
|
153
|
-
const fallback = resolveFallbackVp(meta);
|
|
154
|
-
if (!fallback) {
|
|
155
115
|
return {
|
|
156
116
|
message: stored,
|
|
157
|
-
dispatched:
|
|
117
|
+
dispatched: selection.dispatched,
|
|
158
118
|
fallback: null,
|
|
159
|
-
errors:
|
|
119
|
+
errors: selection.errors,
|
|
160
120
|
};
|
|
161
121
|
}
|
|
162
|
-
|
|
122
|
+
|
|
123
|
+
if (selection.reason === 'fallback' && selection.fallback) {
|
|
124
|
+
deliver(selection.fallback, makeEnvelope(stored, meta, 'fallback'));
|
|
163
125
|
return {
|
|
164
126
|
message: stored,
|
|
165
|
-
dispatched:
|
|
166
|
-
fallback:
|
|
167
|
-
errors:
|
|
127
|
+
dispatched: selection.dispatched,
|
|
128
|
+
fallback: selection.fallback,
|
|
129
|
+
errors: selection.errors,
|
|
168
130
|
};
|
|
169
131
|
}
|
|
170
|
-
|
|
171
|
-
|
|
132
|
+
|
|
133
|
+
// no-default / nothing to dispatch
|
|
134
|
+
return {
|
|
135
|
+
message: stored,
|
|
136
|
+
dispatched: [],
|
|
137
|
+
fallback: null,
|
|
138
|
+
errors: selection.errors,
|
|
139
|
+
};
|
|
172
140
|
}
|
|
173
141
|
|
|
174
142
|
return {
|
|
@@ -204,3 +172,4 @@ function makeEnvelope(msg, meta, trigger) {
|
|
|
204
172
|
* @property {boolean=} truncatedAtFanOutCap
|
|
205
173
|
* @property {string=} skipped
|
|
206
174
|
*/
|
|
175
|
+
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* groups/pre-flow.js — explicit pre-flow stage for Unify.
|
|
3
|
+
*
|
|
4
|
+
* Pre-flow is the "before any VP runs" stage. It owns:
|
|
5
|
+
*
|
|
6
|
+
* (1) VP selection — which VP(s) respond to this user turn?
|
|
7
|
+
* Pure function `selectRespondingVps({meta, fromUser, mentions,
|
|
8
|
+
* sender, taskMembers, fanOutCap})` that mirrors the legacy
|
|
9
|
+
* coordinator dispatch matrix: mention → broadcast → fallback to
|
|
10
|
+
* defaultVpId, with VP-authored messages routed via the explicit
|
|
11
|
+
* route_forward tool instead of free-text @-mentions.
|
|
12
|
+
*
|
|
13
|
+
* (2) Memory recall — what memory gets pre-injected into each
|
|
14
|
+
* responding VP's prompt? Thin wrapper around
|
|
15
|
+
* memory/preflow.js's FTS5 recall.
|
|
16
|
+
*
|
|
17
|
+
* Commit C will flip the caller (web-bridge.js) to fan out responding
|
|
18
|
+
* VPs in parallel via Promise.all.
|
|
19
|
+
*
|
|
20
|
+
* Why one module: a single import surface for the full pre-flow stage,
|
|
21
|
+
* a stable seam for the engine, and a place to format FTS hits into the
|
|
22
|
+
* {profile, entries, formatted} shape the engine already consumes.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { runPreflow as runFtsPreflow } from '../memory/preflow.js';
|
|
26
|
+
import { isMember, resolveFallbackVp } from './roster.js';
|
|
27
|
+
|
|
28
|
+
/** Matches `@vp-id` where id is [A-Za-z0-9_-]+. Captures the id. */
|
|
29
|
+
const MENTION_RE = /(^|\s)@([A-Za-z0-9_][A-Za-z0-9_-]*)/g;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract an ordered, unique list of @-mentions from a text string.
|
|
33
|
+
* Recognises the literal token `@all` as broadcast.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} text
|
|
36
|
+
* @returns {string[]}
|
|
37
|
+
*/
|
|
38
|
+
export function parseMentions(text) {
|
|
39
|
+
if (!text || typeof text !== 'string') return [];
|
|
40
|
+
const out = [];
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
MENTION_RE.lastIndex = 0;
|
|
43
|
+
let m;
|
|
44
|
+
while ((m = MENTION_RE.exec(text))) {
|
|
45
|
+
const id = m[2];
|
|
46
|
+
if (seen.has(id)) continue;
|
|
47
|
+
seen.add(id);
|
|
48
|
+
out.push(id);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {object} SelectionInput
|
|
55
|
+
* @property {object} meta GroupHandle meta (roster + defaultVpId)
|
|
56
|
+
* @property {boolean} fromUser true = user-authored; false = VP-authored
|
|
57
|
+
* @property {string[]} mentions Already-parsed @-mentions
|
|
58
|
+
* @property {string=} sender VP id when fromUser=false
|
|
59
|
+
* @property {number} [fanOutCap=16]
|
|
60
|
+
* @property {string[]=} taskMembers When set, restricts dispatch to this list
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {object} SelectionResult
|
|
65
|
+
* @property {string[]} dispatched VP ids that should respond
|
|
66
|
+
* @property {string|null} fallback The fallback vp, if any
|
|
67
|
+
* @property {Array<{vpId?:string,error:string}>} errors
|
|
68
|
+
* @property {'mention'|'broadcast'|'fallback'|'vp-author-no-text-routing'|'no-default'} reason
|
|
69
|
+
* @property {boolean=} truncatedAtFanOutCap
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Pure VP-selection step of pre-flow. Returns ids only — caller owns
|
|
74
|
+
* persistence + envelope construction + deliver().
|
|
75
|
+
*
|
|
76
|
+
* @param {SelectionInput} input
|
|
77
|
+
* @returns {SelectionResult}
|
|
78
|
+
*/
|
|
79
|
+
export function selectRespondingVps(input) {
|
|
80
|
+
const meta = input.meta;
|
|
81
|
+
if (!meta) {
|
|
82
|
+
return { dispatched: [], fallback: null, errors: [{ error: 'no_group_meta' }], reason: 'no-default' };
|
|
83
|
+
}
|
|
84
|
+
const fanOutCap = Number.isFinite(input.fanOutCap) ? input.fanOutCap : 16;
|
|
85
|
+
const taskMembers = Array.isArray(input.taskMembers) ? input.taskMembers : null;
|
|
86
|
+
const mentions = Array.isArray(input.mentions) ? input.mentions : [];
|
|
87
|
+
|
|
88
|
+
// VP-authored messages: never auto-route through @-mentions; VPs hand
|
|
89
|
+
// off through the explicit route_forward tool instead.
|
|
90
|
+
if (!input.fromUser) {
|
|
91
|
+
return {
|
|
92
|
+
dispatched: [],
|
|
93
|
+
fallback: null,
|
|
94
|
+
errors: [],
|
|
95
|
+
reason: 'vp-author-no-text-routing',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// @all broadcast — fan out to every roster member except the sender,
|
|
100
|
+
// honouring fanOutCap and taskMembers.
|
|
101
|
+
if (mentions.includes('all')) {
|
|
102
|
+
const roster = meta.roster.filter((v) => v !== input.sender).slice(0, fanOutCap);
|
|
103
|
+
const scoped = taskMembers ? roster.filter((v) => taskMembers.includes(v)) : roster;
|
|
104
|
+
return {
|
|
105
|
+
dispatched: scoped,
|
|
106
|
+
fallback: null,
|
|
107
|
+
errors: [],
|
|
108
|
+
reason: 'broadcast',
|
|
109
|
+
truncatedAtFanOutCap: meta.roster.length - 1 > fanOutCap,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Explicit @-mentions
|
|
114
|
+
if (mentions.length > 0) {
|
|
115
|
+
const dispatched = [];
|
|
116
|
+
const errors = [];
|
|
117
|
+
for (const vpId of mentions) {
|
|
118
|
+
if (!isMember(meta, vpId)) {
|
|
119
|
+
errors.push({ vpId, error: 'not_in_roster' });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (taskMembers && !taskMembers.includes(vpId)) {
|
|
123
|
+
errors.push({ vpId, error: 'not_in_task_members' });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
dispatched.push(vpId);
|
|
127
|
+
}
|
|
128
|
+
return { dispatched, fallback: null, errors, reason: 'mention' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// No @-mention → fallback to defaultVpId (architecture G2)
|
|
132
|
+
const fallback = resolveFallbackVp(meta);
|
|
133
|
+
if (!fallback) {
|
|
134
|
+
return {
|
|
135
|
+
dispatched: [],
|
|
136
|
+
fallback: null,
|
|
137
|
+
errors: [{ error: 'no_default_vp' }],
|
|
138
|
+
reason: 'no-default',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (taskMembers && !taskMembers.includes(fallback)) {
|
|
142
|
+
return {
|
|
143
|
+
dispatched: [],
|
|
144
|
+
fallback: null,
|
|
145
|
+
errors: [{ vpId: fallback, error: 'not_in_task_members' }],
|
|
146
|
+
reason: 'no-default',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
dispatched: [fallback],
|
|
151
|
+
fallback,
|
|
152
|
+
errors: [],
|
|
153
|
+
reason: 'fallback',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build the heading for a single scope's formatted memory block.
|
|
160
|
+
*
|
|
161
|
+
* Mirrors recall-v2's formatRecallV2 heading style so the system
|
|
162
|
+
* prompt looks the same to the LLM whether recall came from FTS
|
|
163
|
+
* (here) or from per-scope file reads (recall-v2).
|
|
164
|
+
*
|
|
165
|
+
* @param {string} scope
|
|
166
|
+
* @returns {string}
|
|
167
|
+
*/
|
|
168
|
+
function scopeHeading(scope) {
|
|
169
|
+
if (scope === 'user') return '## Memory: User';
|
|
170
|
+
if (scope.startsWith('group/')) return `## Memory: Group ${scope.slice(6)}`;
|
|
171
|
+
if (scope.startsWith('vp/')) return `## Memory: VP ${scope.slice(3)}`;
|
|
172
|
+
if (scope.startsWith('feature/')) return `## Memory: Feature ${scope.slice(8)}`;
|
|
173
|
+
if (scope.startsWith('topic/')) return `## Memory: Topic ${scope.slice(6)}`;
|
|
174
|
+
return `## Memory: ${scope}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Format FTS picked segments into the prompt-ready string.
|
|
179
|
+
*
|
|
180
|
+
* Picked segments are grouped by scope (preserving the FTS rerank
|
|
181
|
+
* order within each scope group), then rendered as markdown blocks
|
|
182
|
+
* with one heading per scope.
|
|
183
|
+
*
|
|
184
|
+
* @param {Array<{scope: string, body: string, tags?: string[], kind?: string}>} picked
|
|
185
|
+
* @returns {string}
|
|
186
|
+
*/
|
|
187
|
+
export function formatPickedForInjection(picked) {
|
|
188
|
+
if (!picked || picked.length === 0) return '';
|
|
189
|
+
const byScope = new Map();
|
|
190
|
+
// Preserve insertion order (which is rerank order within each scope).
|
|
191
|
+
for (const seg of picked) {
|
|
192
|
+
const scope = seg.scope || 'unknown';
|
|
193
|
+
if (!byScope.has(scope)) byScope.set(scope, []);
|
|
194
|
+
byScope.get(scope).push(seg);
|
|
195
|
+
}
|
|
196
|
+
const parts = [];
|
|
197
|
+
for (const [scope, segs] of byScope.entries()) {
|
|
198
|
+
parts.push(scopeHeading(scope));
|
|
199
|
+
for (const s of segs) {
|
|
200
|
+
const body = (s.body || '').trim();
|
|
201
|
+
if (body) parts.push(body);
|
|
202
|
+
}
|
|
203
|
+
parts.push(''); // blank line between scopes
|
|
204
|
+
}
|
|
205
|
+
return parts.join('\n').trim();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @typedef {object} MemoryPreflowOptions
|
|
210
|
+
* @property {string} userMsg The user's message
|
|
211
|
+
* @property {string} [groupId] Active group, if any
|
|
212
|
+
* @property {string} [vpId] Responding VP id, if any
|
|
213
|
+
* @property {string} [featureId] Active feature, if any
|
|
214
|
+
* @property {string[]} [extraScopes] Additional scopes to include
|
|
215
|
+
* @property {string[]} [currentTags] Contextual tags for rerank
|
|
216
|
+
* @property {number} [topK] Max FTS rows fetched (default 50)
|
|
217
|
+
* @property {number} [budgetTokens] Token budget for picked segments
|
|
218
|
+
*/
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @typedef {object} MemoryPreflowResult
|
|
222
|
+
* @property {string} profile User-scope summary (best-effort)
|
|
223
|
+
* @property {object[]} entries Picked segments (raw)
|
|
224
|
+
* @property {string} formatted Prompt-ready string
|
|
225
|
+
* @property {object} meta Raw FTS preflow metadata
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build the canonical scope list for a given (groupId, vpId, featureId).
|
|
230
|
+
* Always includes 'user'. The order is significant — preflow.js's scope
|
|
231
|
+
* filter accepts/rejects by membership, and the formatter renders in
|
|
232
|
+
* order.
|
|
233
|
+
*
|
|
234
|
+
* @param {{groupId?: string, vpId?: string, featureId?: string, extra?: string[]}} ctx
|
|
235
|
+
* @returns {string[]}
|
|
236
|
+
*/
|
|
237
|
+
export function buildRelevantScopes({ groupId, vpId, featureId, extra } = {}) {
|
|
238
|
+
const scopes = ['user'];
|
|
239
|
+
if (groupId) scopes.push(`group/${groupId}`);
|
|
240
|
+
if (vpId) scopes.push(`vp/${vpId}`);
|
|
241
|
+
if (featureId) scopes.push(`feature/${featureId}`);
|
|
242
|
+
if (Array.isArray(extra)) {
|
|
243
|
+
for (const s of extra) {
|
|
244
|
+
if (s && !scopes.includes(s)) scopes.push(s);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return scopes;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Run memory pre-flow for one VP turn. Thin wrapper around
|
|
252
|
+
* `memory/preflow.js::runPreflow` that:
|
|
253
|
+
*
|
|
254
|
+
* - resolves canonical scope list from {groupId, vpId, featureId},
|
|
255
|
+
* - invokes FTS5 recall,
|
|
256
|
+
* - formats picked segments for prompt injection.
|
|
257
|
+
*
|
|
258
|
+
* Returns the engine-consumable {profile, entries, formatted, meta}
|
|
259
|
+
* shape so the existing recall pipeline can swap in without changes.
|
|
260
|
+
*
|
|
261
|
+
* @param {import('../memory/index-db.js').SegmentIndex} index
|
|
262
|
+
* @param {MemoryPreflowOptions} opts
|
|
263
|
+
* @returns {MemoryPreflowResult}
|
|
264
|
+
*/
|
|
265
|
+
export function runMemoryPreflow(index, opts) {
|
|
266
|
+
if (!index) {
|
|
267
|
+
return { profile: '', entries: [], formatted: '', meta: { skipped: 'no-index' } };
|
|
268
|
+
}
|
|
269
|
+
const userMsg = (opts?.userMsg || '').trim();
|
|
270
|
+
if (!userMsg) {
|
|
271
|
+
return { profile: '', entries: [], formatted: '', meta: { skipped: 'no-user-msg' } };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const relevantScopes = buildRelevantScopes({
|
|
275
|
+
groupId: opts.groupId,
|
|
276
|
+
vpId: opts.vpId,
|
|
277
|
+
featureId: opts.featureId,
|
|
278
|
+
extra: opts.extraScopes,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const result = runFtsPreflow(index, {
|
|
282
|
+
userMsg,
|
|
283
|
+
relevantScopes,
|
|
284
|
+
ownVpId: opts.vpId || null,
|
|
285
|
+
currentTags: opts.currentTags || [],
|
|
286
|
+
topK: opts.topK,
|
|
287
|
+
budgetTokens: opts.budgetTokens,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Best-effort profile: pick any user-scope segment body.
|
|
291
|
+
const userSeg = (result.picked || []).find(p => p.scope === 'user');
|
|
292
|
+
const profile = userSeg ? (userSeg.body || '').trim() : '';
|
|
293
|
+
|
|
294
|
+
const formatted = formatPickedForInjection(result.picked || []);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
profile,
|
|
298
|
+
entries: result.picked || [],
|
|
299
|
+
formatted,
|
|
300
|
+
meta: {
|
|
301
|
+
keywords: result.keywords,
|
|
302
|
+
ftsQuery: result.ftsQuery,
|
|
303
|
+
pickedTokens: result.pickedTokens,
|
|
304
|
+
droppedCount: result.droppedCount,
|
|
305
|
+
hitCount: (result.hits || []).length,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
package/unify/session.js
CHANGED
|
@@ -26,13 +26,22 @@ import { createFullRegistry } from './tools/index.js';
|
|
|
26
26
|
import { initFeatureStore } from './tools/feature-tools.js';
|
|
27
27
|
import { Engine } from './engine.js';
|
|
28
28
|
// H2.f.5: threads/, pipeline/dispatcher and input-queue retired. The
|
|
29
|
-
// session now exposes a single Engine.
|
|
30
|
-
//
|
|
29
|
+
// session now exposes a single Engine.
|
|
30
|
+
//
|
|
31
|
+
// GC.1 Commit A: when config.memoryV2 && config.memoryPreflow, the
|
|
32
|
+
// session opens a SegmentIndex (SQLite FTS5 over memory.md) and
|
|
33
|
+
// passes it to the Engine. The Engine's #recallMemory then routes
|
|
34
|
+
// pre-turn recall through groups/pre-flow.js → memory/preflow.js
|
|
35
|
+
// instead of the per-scope file reader (memory/recall-v2.js).
|
|
36
|
+
// Post-turn adjustMemory (memory/adjust.js) wiring lands in a later
|
|
37
|
+
// commit.
|
|
31
38
|
import { ensureDefaultGroupIfEmpty } from './groups/group-crud.js';
|
|
32
39
|
import { seedDefaultVps } from './vp/seed-defaults.js';
|
|
33
40
|
import { createDreamScheduler } from './memory/dream-scheduler.js';
|
|
34
41
|
import { createV2DreamScheduler } from './dream-v2/session-wiring.js';
|
|
35
42
|
import { getUserMemoryStore } from './memory/user-memory-store.js';
|
|
43
|
+
import { openSegmentIndex } from './memory/index-db.js';
|
|
44
|
+
import { syncAll as syncSegmentIndex } from './memory/segment-sync.js';
|
|
36
45
|
import { migrateR6toV2 } from './memory/migrate-r6-to-v2.js';
|
|
37
46
|
import { join } from 'path';
|
|
38
47
|
import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe, writeFileSync as writeFileSyncSafe } from 'fs';
|
|
@@ -213,6 +222,35 @@ export async function loadSession(options = {}) {
|
|
|
213
222
|
console.warn(`[Yeaft] Failed to open R6 memory shard store: ${err?.message || err}`);
|
|
214
223
|
}
|
|
215
224
|
|
|
225
|
+
// ─── 5-fts. (GC.1) Open SegmentIndex for FTS pre-flow ────
|
|
226
|
+
// When config.memoryV2 && config.memoryPreflow, build a SQLite
|
|
227
|
+
// FTS5 index over ~/.yeaft/memory/<scope>/memory.md and pass it
|
|
228
|
+
// to the Engine. Engine.#recallMemory uses it via
|
|
229
|
+
// groups/pre-flow.js → memory/preflow.js. Disk is the source of
|
|
230
|
+
// truth; on boot we reconcile disk → index via syncAll.
|
|
231
|
+
// Failure to open the index is non-fatal: the Engine falls back
|
|
232
|
+
// to recall-v2 transparently.
|
|
233
|
+
let memoryIndex = null;
|
|
234
|
+
if (config.memoryV2 && config.memoryPreflow && !config._readOnly) {
|
|
235
|
+
try {
|
|
236
|
+
const indexPath = join(yeaftDir, 'memory', 'index.db');
|
|
237
|
+
memoryIndex = openSegmentIndex(indexPath);
|
|
238
|
+
const memoryRoot = join(yeaftDir, 'memory');
|
|
239
|
+
try {
|
|
240
|
+
syncSegmentIndex(memoryRoot, memoryIndex);
|
|
241
|
+
} catch (syncErr) {
|
|
242
|
+
// Sync is best-effort; an empty / partial index just produces
|
|
243
|
+
// empty recall results, never an error.
|
|
244
|
+
if (config.debug) {
|
|
245
|
+
console.warn(`[Yeaft] FTS index sync warning: ${syncErr?.message || syncErr}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.warn(`[Yeaft] Failed to open FTS segment index (preflow disabled): ${err?.message || err}`);
|
|
250
|
+
memoryIndex = null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
216
254
|
// ─── 5a. Initialize feature store ──────────────────────
|
|
217
255
|
initFeatureStore(yeaftDir, { readOnly: config._readOnly || false });
|
|
218
256
|
|
|
@@ -277,6 +315,7 @@ export async function loadSession(options = {}) {
|
|
|
277
315
|
conversationStore,
|
|
278
316
|
memoryStore,
|
|
279
317
|
memoryShardStore,
|
|
318
|
+
memoryIndex,
|
|
280
319
|
toolRegistry,
|
|
281
320
|
skillManager,
|
|
282
321
|
mcpManager,
|
|
@@ -355,6 +394,11 @@ export async function loadSession(options = {}) {
|
|
|
355
394
|
} catch {
|
|
356
395
|
// Trace might not have close() (NullTrace)
|
|
357
396
|
}
|
|
397
|
+
try {
|
|
398
|
+
if (memoryIndex) memoryIndex.close();
|
|
399
|
+
} catch {
|
|
400
|
+
// Best-effort cleanup
|
|
401
|
+
}
|
|
358
402
|
}
|
|
359
403
|
|
|
360
404
|
return {
|