@yeaft/webchat-agent 0.1.616 → 0.1.617
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/compact/orchestrator.js +1 -1
- package/unify/{task-message.js → feature-message.js} +38 -38
- package/unify/features/store.js +583 -0
- package/unify/{tasks → features}/summary.js +54 -85
- package/unify/memory/dream-shard.js +15 -15
- package/unify/memory/recall-r6.js +2 -2
- package/unify/memory/schema.js +7 -7
- package/unify/memory/scope-tree.js +6 -6
- package/unify/pipeline/dispatcher.js +7 -7
- package/unify/router/intent-classifier.js +25 -25
- package/unify/session.js +3 -3
- package/unify/threads/store.js +18 -18
- package/unify/tools/{task-tools.js → feature-tools.js} +180 -175
- package/unify/tools/index.js +21 -21
- package/unify/tools/thread-tools.js +29 -108
- package/unify/web-bridge.js +61 -61
- package/unify/memory/migrate-r5-to-r6.js +0 -611
- package/unify/migration/detect.js +0 -176
- package/unify/migration/map-fields.js +0 -293
- package/unify/migration/v0-to-v1.js +0 -588
- package/unify/tasks/store.js +0 -652
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* summary.js — task-334n:
|
|
2
|
+
* summary.js — task-334n: Feature multi-VP collaboration summary protocol.
|
|
3
3
|
*
|
|
4
4
|
* Owns:
|
|
5
|
-
* - postSummary()
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
5
|
+
* - postSummary() — write a `type=summary` message to the group jsonl
|
|
6
|
+
* and run the extractor (B + C)
|
|
7
|
+
* - extractFeatureMemory() — turn a summary body into 2-5 feature-memory entries
|
|
8
|
+
* via 334f feature-memory shard lib (C)
|
|
9
9
|
* - buildSummaryReminder() — compute the §Δ31.4 3-AND soft reminder shape
|
|
10
|
-
* consumed by 334e's `
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
10
|
+
* consumed by 334e's `featureCtx.summaryReminder` (D)
|
|
11
|
+
* - buildFeatureCtxMemories() — assemble feature-memory top-5 (pinned + recent +
|
|
12
|
+
* tag relevance) for feature_ctx (E)
|
|
13
13
|
*
|
|
14
14
|
* Hard boundaries:
|
|
15
15
|
* - does NOT touch 334o jsonl rotation internals (calls group.appendMessage)
|
|
16
16
|
* - does NOT touch 334f shard-store impl (calls openMemoryShardStore API)
|
|
17
17
|
* - does NOT touch 334e prompts main frame (returns plain shapes that feed
|
|
18
|
-
* the existing
|
|
19
|
-
* - does NOT self-loop-write VP-memory (extractor writes
|
|
18
|
+
* the existing renderFeatureCtx contract)
|
|
19
|
+
* - does NOT self-loop-write VP-memory (extractor writes feature-memory only;
|
|
20
20
|
* VP-level synthesis is deferred to 334g dream)
|
|
21
21
|
* - softCap overflow does NOT create new shards (334f already routes into
|
|
22
22
|
* dream queue via projectDeriveHint; we just surface `needsRecompression`)
|
|
@@ -44,7 +44,7 @@ export const PROGRESS_ANCHORS = Object.freeze([
|
|
|
44
44
|
/** Whitelist of R6 kinds emitted by the summary-extractor. */
|
|
45
45
|
const EXTRACT_KINDS = Object.freeze(['progress', 'decision']);
|
|
46
46
|
|
|
47
|
-
/** Shard routing for each extracted kind (§Δ25.2
|
|
47
|
+
/** Shard routing for each extracted kind (§Δ25.2 feature-memory fixed set). */
|
|
48
48
|
const KIND_TO_SHARD = Object.freeze({
|
|
49
49
|
progress: 'progress',
|
|
50
50
|
decision: 'decision',
|
|
@@ -54,26 +54,25 @@ const KIND_TO_SHARD = Object.freeze({
|
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* Write a `type=summary` message to the group log, then auto-run the
|
|
57
|
-
* extractor to derive
|
|
57
|
+
* extractor to derive feature-memory entries.
|
|
58
58
|
*
|
|
59
59
|
* @param {{
|
|
60
60
|
* group: import('../groups/group-store.js').GroupHandle,
|
|
61
|
-
*
|
|
61
|
+
* featureId: string,
|
|
62
62
|
* fromVpId: string,
|
|
63
63
|
* body: string,
|
|
64
|
-
* progress?: number|string,
|
|
65
|
-
* supersedes?: string[],
|
|
66
|
-
* memoryDir: string, // groups/<g>/
|
|
67
|
-
* now?: () => number,
|
|
64
|
+
* progress?: number|string,
|
|
65
|
+
* supersedes?: string[],
|
|
66
|
+
* memoryDir: string, // groups/<g>/features/<f>/memory/
|
|
67
|
+
* now?: () => number,
|
|
68
68
|
* extractor?: (body:string) => Array<{kind:string,body:string,tags?:string[]}>
|
|
69
|
-
* // optional hook; default uses `defaultExtractor` (heuristic, no LLM)
|
|
70
69
|
* }} opts
|
|
71
70
|
* @returns {{ message: any, memoryIds: string[], supersededSummaryIds: string[] }}
|
|
72
71
|
*/
|
|
73
72
|
export function postSummary(opts) {
|
|
74
73
|
const {
|
|
75
74
|
group,
|
|
76
|
-
|
|
75
|
+
featureId,
|
|
77
76
|
fromVpId,
|
|
78
77
|
body,
|
|
79
78
|
progress,
|
|
@@ -86,7 +85,7 @@ export function postSummary(opts) {
|
|
|
86
85
|
if (!group || typeof group.appendMessage !== 'function') {
|
|
87
86
|
throw new Error('postSummary: group handle required');
|
|
88
87
|
}
|
|
89
|
-
if (!
|
|
88
|
+
if (!featureId) throw new Error('postSummary: featureId required');
|
|
90
89
|
if (!fromVpId) throw new Error('postSummary: fromVpId required');
|
|
91
90
|
if (typeof body !== 'string' || !body.trim()) {
|
|
92
91
|
throw new Error('postSummary: body required (non-empty string)');
|
|
@@ -112,7 +111,7 @@ export function postSummary(opts) {
|
|
|
112
111
|
from: fromVpId,
|
|
113
112
|
role: 'assistant',
|
|
114
113
|
text: body,
|
|
115
|
-
|
|
114
|
+
featureId,
|
|
116
115
|
meta: {
|
|
117
116
|
type: 'summary',
|
|
118
117
|
progress: progress == null ? null : (typeof progress === 'string' ? progress : Number(progress)),
|
|
@@ -120,10 +119,10 @@ export function postSummary(opts) {
|
|
|
120
119
|
},
|
|
121
120
|
});
|
|
122
121
|
|
|
123
|
-
// 2) Run the extractor → write
|
|
122
|
+
// 2) Run the extractor → write feature-memory entries (C).
|
|
124
123
|
const memoryIds = [];
|
|
125
124
|
try {
|
|
126
|
-
const store = openMemoryShardStore(memoryDir, '
|
|
125
|
+
const store = openMemoryShardStore(memoryDir, 'feature');
|
|
127
126
|
const raw = extractor(body) || [];
|
|
128
127
|
const bounded = clampExtracted(raw);
|
|
129
128
|
for (const [i, item] of bounded.entries()) {
|
|
@@ -134,11 +133,11 @@ export function postSummary(opts) {
|
|
|
134
133
|
id,
|
|
135
134
|
shard,
|
|
136
135
|
kind,
|
|
137
|
-
|
|
136
|
+
featureId,
|
|
138
137
|
body: typeof item.body === 'string' ? item.body.trim() : '',
|
|
139
138
|
tags: Array.isArray(item.tags) ? item.tags.slice(0, 5) : [],
|
|
140
139
|
authoredBy: AUTHORED_BY.SUMMARY,
|
|
141
|
-
sourceRef: {
|
|
140
|
+
sourceRef: { featureId, msgIds: [stored.id] },
|
|
142
141
|
createdAt: new Date(now()).toISOString(),
|
|
143
142
|
});
|
|
144
143
|
memoryIds.push(id);
|
|
@@ -167,17 +166,6 @@ function clampExtracted(arr) {
|
|
|
167
166
|
|
|
168
167
|
// ─── (C) default extractor ───────────────────────────────────────
|
|
169
168
|
|
|
170
|
-
/**
|
|
171
|
-
* Heuristic extractor — no LLM, deterministic, safe for tests.
|
|
172
|
-
*
|
|
173
|
-
* Strategy:
|
|
174
|
-
* - Split body into non-empty lines (trim bullets).
|
|
175
|
-
* - Lines starting with keywords "decide/decision/chose/chosen" → kind=decision.
|
|
176
|
-
* - Lines starting with "progress/ship/shipped/done/completed/blocker/todo"
|
|
177
|
-
* → kind=progress.
|
|
178
|
-
* - Everything else → kind=progress (default).
|
|
179
|
-
* - Emit up to EXTRACT_MAX_ENTRIES.
|
|
180
|
-
*/
|
|
181
169
|
export function defaultExtractor(body) {
|
|
182
170
|
if (typeof body !== 'string') return [];
|
|
183
171
|
const lines = body
|
|
@@ -194,12 +182,7 @@ export function defaultExtractor(body) {
|
|
|
194
182
|
out.push({ kind, body: line });
|
|
195
183
|
if (out.length >= EXTRACT_MAX_ENTRIES) break;
|
|
196
184
|
}
|
|
197
|
-
// If we ended up with fewer than MIN and there was a body, collapse to
|
|
198
|
-
// one "progress" entry carrying the trimmed full body so we never emit 0
|
|
199
|
-
// when the caller gave us real content and asked for 2-5.
|
|
200
185
|
if (out.length < EXTRACT_MIN_ENTRIES && body.trim()) {
|
|
201
|
-
// Pad up to EXTRACT_MIN_ENTRIES with the full trimmed body when
|
|
202
|
-
// the line-by-line pass yielded fewer entries than the minimum.
|
|
203
186
|
while (out.length < EXTRACT_MIN_ENTRIES) {
|
|
204
187
|
out.push({ kind: 'progress', body: body.trim() });
|
|
205
188
|
}
|
|
@@ -210,39 +193,34 @@ export function defaultExtractor(body) {
|
|
|
210
193
|
// ─── (D) soft reminder builder ───────────────────────────────────
|
|
211
194
|
|
|
212
195
|
/**
|
|
213
|
-
* Build the `
|
|
214
|
-
* Returns null when the 3-AND conditions do not all hold. The prompt layer
|
|
215
|
-
* adds a 4th check (currentVpId === initiatorVpId) so we gate here too so
|
|
216
|
-
* callers can debug-log why it was suppressed.
|
|
196
|
+
* Build the `featureCtx.summaryReminder` shape consumed by 334e's prompt.
|
|
217
197
|
*
|
|
218
198
|
* §Δ31.4 conditions:
|
|
219
|
-
* (1)
|
|
220
|
-
* (2) caller role === 'initiator' (i.e. currentVpId ===
|
|
199
|
+
* (1) feature.members.length > 1
|
|
200
|
+
* (2) caller role === 'initiator' (i.e. currentVpId === feature.initiator)
|
|
221
201
|
* (3) (now - lastSummaryAt) ≥ 20 min OR nonSummaryTurns ≥ 10
|
|
222
202
|
*
|
|
223
203
|
* @param {{
|
|
224
|
-
*
|
|
204
|
+
* feature: { initiator?: string, members?: string[] },
|
|
225
205
|
* currentVpId: string,
|
|
226
|
-
* lastSummaryAt: number,
|
|
206
|
+
* lastSummaryAt: number,
|
|
227
207
|
* nonSummaryTurns: number,
|
|
228
208
|
* now?: number,
|
|
229
209
|
* }} input
|
|
230
|
-
* @returns {{ triggered: boolean, nonSummaryCount: number, lastSummaryAt: number,
|
|
231
|
-
* now: number, reasons: string[] }}
|
|
232
210
|
*/
|
|
233
211
|
export function buildSummaryReminder(input) {
|
|
234
|
-
const {
|
|
212
|
+
const { feature, currentVpId, lastSummaryAt = 0, nonSummaryTurns = 0 } = input || {};
|
|
235
213
|
const now = typeof input?.now === 'number' ? input.now : Date.now();
|
|
236
214
|
const reasons = [];
|
|
237
215
|
|
|
238
|
-
if (!
|
|
239
|
-
return { triggered: false, reasons: ['no-
|
|
216
|
+
if (!feature || typeof feature !== 'object') {
|
|
217
|
+
return { triggered: false, reasons: ['no-feature'], nonSummaryCount: nonSummaryTurns, lastSummaryAt, now };
|
|
240
218
|
}
|
|
241
|
-
const members = Array.isArray(
|
|
242
|
-
const isInitiator = !!currentVpId &&
|
|
219
|
+
const members = Array.isArray(feature.members) ? feature.members : [];
|
|
220
|
+
const isInitiator = !!currentVpId && feature.initiator === currentVpId;
|
|
243
221
|
|
|
244
222
|
if (!isInitiator) reasons.push('not-initiator');
|
|
245
|
-
if (members.length <= SUMMARY_REMINDER_MIN_MEMBERS - 1) reasons.push('solo-
|
|
223
|
+
if (members.length <= SUMMARY_REMINDER_MIN_MEMBERS - 1) reasons.push('solo-feature');
|
|
246
224
|
|
|
247
225
|
const ageMs = lastSummaryAt > 0 ? now - lastSummaryAt : Number.POSITIVE_INFINITY;
|
|
248
226
|
const ageOk = ageMs >= SUMMARY_REMINDER_MIN_AGE_MS;
|
|
@@ -259,29 +237,25 @@ export function buildSummaryReminder(input) {
|
|
|
259
237
|
};
|
|
260
238
|
}
|
|
261
239
|
|
|
262
|
-
// ─── (E)
|
|
240
|
+
// ─── (E) feature_ctx top-5 feature-memory builder ────────────────
|
|
263
241
|
|
|
264
242
|
/**
|
|
265
|
-
* Assemble
|
|
243
|
+
* Assemble feature-memory top-5 for 334e's `featureCtx.memories` field.
|
|
266
244
|
* Ordering (§Δ16.5): pinned first → recent → tag-relevant. Supersedes are
|
|
267
245
|
* hidden (entries with supersededBy != null are filtered out).
|
|
268
246
|
*
|
|
269
|
-
* @param {string} memoryDir groups/<g>/
|
|
247
|
+
* @param {string} memoryDir groups/<g>/features/<f>/memory/
|
|
270
248
|
* @param {{ tags?: string[], top?: number }} [opts]
|
|
271
|
-
* tags : optional tag hints to boost relevance
|
|
272
|
-
* top : default 5
|
|
273
249
|
* @returns {Array<{body:string, shard:string, authoredBy?:string}>}
|
|
274
250
|
*/
|
|
275
|
-
export function
|
|
251
|
+
export function buildFeatureCtxMemories(memoryDir, opts = {}) {
|
|
276
252
|
const top = Number.isFinite(opts.top) ? Number(opts.top) : 5;
|
|
277
253
|
const tagHints = Array.isArray(opts.tags) ? opts.tags : [];
|
|
278
254
|
const nowMs = typeof opts.now === 'number' ? opts.now : Date.now();
|
|
279
255
|
let results = [];
|
|
280
256
|
try {
|
|
281
|
-
const store = openMemoryShardStore(memoryDir, '
|
|
257
|
+
const store = openMemoryShardStore(memoryDir, 'feature');
|
|
282
258
|
const q = store.query({});
|
|
283
|
-
// query() returns thin entries (id/shard/kind/tags/pinned/groupId/taskId/supersededBy);
|
|
284
|
-
// we need the body too.
|
|
285
259
|
const hits = (q.results || [])
|
|
286
260
|
.filter((r) => !r.supersededBy)
|
|
287
261
|
.map((r) => {
|
|
@@ -301,21 +275,17 @@ export function buildTaskCtxMemories(memoryDir, opts = {}) {
|
|
|
301
275
|
const score = (r) => {
|
|
302
276
|
let s = 0;
|
|
303
277
|
if (r.pinned) s += 1000;
|
|
304
|
-
// Recency decay: half-life of 24h. Recent entries score up to +100,
|
|
305
|
-
// decaying toward 0 as they age.
|
|
306
278
|
if (r.createdAt) {
|
|
307
279
|
const ageMs = nowMs - new Date(r.createdAt).getTime();
|
|
308
|
-
const halfLifeMs = 24 * 60 * 60 * 1000;
|
|
280
|
+
const halfLifeMs = 24 * 60 * 60 * 1000;
|
|
309
281
|
s += Math.max(0, 100 * Math.pow(0.5, Math.max(0, ageMs) / halfLifeMs));
|
|
310
282
|
}
|
|
311
|
-
// tag relevance
|
|
312
283
|
for (const t of tagHints) if (r.tags.includes(t)) s += 5;
|
|
313
284
|
return s;
|
|
314
285
|
};
|
|
315
286
|
hits.sort((a, b) => {
|
|
316
287
|
const ds = score(b) - score(a);
|
|
317
288
|
if (ds !== 0) return ds;
|
|
318
|
-
// stable recency tie-break
|
|
319
289
|
return String(b.createdAt || '').localeCompare(String(a.createdAt || ''));
|
|
320
290
|
});
|
|
321
291
|
results = hits.slice(0, top).map((r) => ({
|
|
@@ -329,31 +299,30 @@ export function buildTaskCtxMemories(memoryDir, opts = {}) {
|
|
|
329
299
|
return results;
|
|
330
300
|
}
|
|
331
301
|
|
|
332
|
-
// ─── (F) related-
|
|
302
|
+
// ─── (F) related-feature ACL fail-closed gate ─────────────────────
|
|
333
303
|
|
|
334
304
|
/**
|
|
335
|
-
* Return memory/summary hints for a related
|
|
336
|
-
* Caller passes the
|
|
305
|
+
* Return memory/summary hints for a related feature only when ACL grants.
|
|
306
|
+
* Caller passes the FeatureStore so we can ask `canAccessRelated()`.
|
|
337
307
|
*
|
|
338
308
|
* @param {{
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
309
|
+
* featureStore: import('./store.js').FeatureStore,
|
|
310
|
+
* currentFeatureId: string,
|
|
311
|
+
* otherFeatureId: string,
|
|
342
312
|
* vpId: string,
|
|
343
313
|
* groupsRoot: string,
|
|
344
314
|
* top?: number,
|
|
345
315
|
* }} input
|
|
346
316
|
* @returns {null | { id:string, title:string, members:string[], updatedAt?:number, memories:Array<{body:string,shard:string}> }}
|
|
347
|
-
* null iff ACL denies — NEVER leak taskId in that case.
|
|
348
317
|
*/
|
|
349
|
-
export function
|
|
350
|
-
const {
|
|
351
|
-
if (!
|
|
352
|
-
if (!
|
|
353
|
-
const other =
|
|
318
|
+
export function getRelatedFeatureCtx(input) {
|
|
319
|
+
const { featureStore, currentFeatureId, otherFeatureId, vpId, groupsRoot, top = 2 } = input || {};
|
|
320
|
+
if (!featureStore || !currentFeatureId || !otherFeatureId || !vpId || !groupsRoot) return null;
|
|
321
|
+
if (!featureStore.canAccessRelated(currentFeatureId, otherFeatureId, vpId)) return null;
|
|
322
|
+
const other = featureStore.get(otherFeatureId);
|
|
354
323
|
if (!other || !other.groupId) return null;
|
|
355
|
-
const memoryDir = join(groupsRoot, other.groupId, '
|
|
356
|
-
const mems =
|
|
324
|
+
const memoryDir = join(groupsRoot, other.groupId, 'features', other.id, 'memory');
|
|
325
|
+
const mems = buildFeatureCtxMemories(memoryDir, { top });
|
|
357
326
|
return {
|
|
358
327
|
id: other.id,
|
|
359
328
|
title: other.title || other.id,
|
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
* Replaces the old entries-based dream scanner with shard-aware streaming:
|
|
5
5
|
* 1. Shard scanner: iterate shards → stream entries → build orient/merge/prune inputs
|
|
6
6
|
* 2. Compact job: rewrite shards with utilization < 50% to reclaim tombstones
|
|
7
|
-
* 3.
|
|
7
|
+
* 3. Feature-memory guard: dream NEVER writes to feature-memory shards (avoids double-write)
|
|
8
8
|
*
|
|
9
9
|
* References:
|
|
10
10
|
* - R5 delta §Δ17.5: compact job spec
|
|
11
|
-
* - R5 delta §Δ16.4.3: "auto-dream 不写
|
|
11
|
+
* - R5 delta §Δ16.4.3: "auto-dream 不写 feature-memory"
|
|
12
12
|
* - 334f shard-store API: stageRecompression / commitRecompression / abortRecompression
|
|
13
|
-
* - schema.js:
|
|
13
|
+
* - schema.js: FEATURE_SHARDS, softCapFor
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { FEATURE_SHARDS, softCapFor } from './schema.js';
|
|
17
17
|
import { AUTHORED_BY } from './shard-store.js';
|
|
18
18
|
import { pickEffort } from '../effort.js';
|
|
19
19
|
|
|
@@ -28,30 +28,30 @@ const MAX_COMPACTS_PER_DREAM = 4;
|
|
|
28
28
|
/** Maximum LLM calls for shard-based dream phases. */
|
|
29
29
|
const MAX_SHARD_DREAM_LLM_CALLS = 5;
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
const
|
|
31
|
+
/** Feature-memory shard names — dream must never write to these. */
|
|
32
|
+
const FEATURE_SHARD_SET = new Set(FEATURE_SHARDS);
|
|
33
33
|
|
|
34
|
-
// ───
|
|
34
|
+
// ─── Feature-Memory Guard ─────────────────────────────────────
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Returns true if the shard name belongs to
|
|
37
|
+
* Returns true if the shard name belongs to feature-memory.
|
|
38
38
|
* Dream must NOT write entries to these shards.
|
|
39
39
|
*
|
|
40
40
|
* @param {string} shardName
|
|
41
41
|
* @returns {boolean}
|
|
42
42
|
*/
|
|
43
|
-
export function
|
|
44
|
-
return
|
|
43
|
+
export function isFeatureMemoryShard(shardName) {
|
|
44
|
+
return FEATURE_SHARD_SET.has(shardName);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Filter out
|
|
48
|
+
* Filter out feature-memory shards from a list of shard names.
|
|
49
49
|
*
|
|
50
50
|
* @param {string[]} shardNames
|
|
51
51
|
* @returns {string[]}
|
|
52
52
|
*/
|
|
53
53
|
export function filterDreamableShards(shardNames) {
|
|
54
|
-
return shardNames.filter(s => !
|
|
54
|
+
return shardNames.filter(s => !isFeatureMemoryShard(s));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// ─── Shard Scanner ─────────────────────────────────────────
|
|
@@ -319,7 +319,7 @@ function compactShard(shardStore, shardName) {
|
|
|
319
319
|
* 3. Merge — LLM-driven merge of duplicate/superseded entries
|
|
320
320
|
* 4. Prune — LLM-driven removal of stale entries
|
|
321
321
|
*
|
|
322
|
-
*
|
|
322
|
+
* Feature-memory guard: all phases skip FEATURE_SHARDS entirely.
|
|
323
323
|
*
|
|
324
324
|
* @param {{
|
|
325
325
|
* shardStore: object,
|
|
@@ -509,7 +509,7 @@ async function runPrunePhase({ shardStore, scan, adapter, config }) {
|
|
|
509
509
|
const st = shardStore.stats();
|
|
510
510
|
const overCapShards = [];
|
|
511
511
|
for (const [name, bucket] of Object.entries(st.shards)) {
|
|
512
|
-
if (
|
|
512
|
+
if (isFeatureMemoryShard(name)) continue;
|
|
513
513
|
const cap = softCapFor(name);
|
|
514
514
|
if (bucket.entries > cap.entries || bucket.bytes > cap.bytes) {
|
|
515
515
|
overCapShards.push(name);
|
|
@@ -551,7 +551,7 @@ async function runPrunePhase({ shardStore, scan, adapter, config }) {
|
|
|
551
551
|
if (typeof id !== 'string') continue;
|
|
552
552
|
// Double-check it's not in a task shard
|
|
553
553
|
const entry = scan.entries.find(e => e.id === id);
|
|
554
|
-
if (entry &&
|
|
554
|
+
if (entry && isFeatureMemoryShard(entry.shard)) continue;
|
|
555
555
|
try {
|
|
556
556
|
shardStore.remove(id);
|
|
557
557
|
result.prunedCount++;
|
|
@@ -15,7 +15,7 @@ import { createHash } from 'crypto';
|
|
|
15
15
|
import { pickEffort } from '../effort.js';
|
|
16
16
|
import {
|
|
17
17
|
VP_DEFAULT_SHARDS,
|
|
18
|
-
|
|
18
|
+
FEATURE_SHARDS,
|
|
19
19
|
USER_SHARDS,
|
|
20
20
|
} from './schema.js';
|
|
21
21
|
|
|
@@ -286,6 +286,6 @@ export function clearR6RecallCache() {
|
|
|
286
286
|
|
|
287
287
|
export const R6_DEFAULTS = {
|
|
288
288
|
VP_DEFAULT_SHARDS,
|
|
289
|
-
|
|
289
|
+
FEATURE_SHARDS,
|
|
290
290
|
USER_SHARDS,
|
|
291
291
|
};
|
package/unify/memory/schema.js
CHANGED
|
@@ -19,8 +19,8 @@ export const VP_DEFAULT_SHARDS = Object.freeze([
|
|
|
19
19
|
'preferences',
|
|
20
20
|
]);
|
|
21
21
|
|
|
22
|
-
// ─── §Δ25.2
|
|
23
|
-
export const
|
|
22
|
+
// ─── §Δ25.2 Feature-memory fixed 5 shards ───────────────────────
|
|
23
|
+
export const FEATURE_SHARDS = Object.freeze([
|
|
24
24
|
'decision',
|
|
25
25
|
'progress',
|
|
26
26
|
'context',
|
|
@@ -49,7 +49,7 @@ export const SOFT_CAPS = Object.freeze({
|
|
|
49
49
|
lessons: { entries: 80, bytes: 64 * 1024 },
|
|
50
50
|
preferences: { entries: 80, bytes: 64 * 1024 },
|
|
51
51
|
relations: { entries: 50, bytes: 32 * 1024 },
|
|
52
|
-
|
|
52
|
+
// Feature (Δ26.3 feature-memory row)
|
|
53
53
|
decision: { entries: 40, bytes: 24 * 1024 },
|
|
54
54
|
progress: { entries: 40, bytes: 24 * 1024 },
|
|
55
55
|
context: { entries: 40, bytes: 24 * 1024 },
|
|
@@ -99,16 +99,16 @@ export function softCapFor(shardName) {
|
|
|
99
99
|
/**
|
|
100
100
|
* Build a schema object suitable for `openShardStore(dir, schema)` (334o).
|
|
101
101
|
*
|
|
102
|
-
* @param {'vp'|'
|
|
102
|
+
* @param {'vp'|'feature'|'user'} kind
|
|
103
103
|
* @param {{ extraShards?: string[] }} [opts] e.g. existing project shards
|
|
104
104
|
* @returns {{ shards: string[], softCap: Record<string,{entries:number,bytes:number}>, defaultSoftCap: object }}
|
|
105
105
|
*/
|
|
106
106
|
export function buildShardSchema(kind, opts = {}) {
|
|
107
107
|
let shards;
|
|
108
108
|
switch (kind) {
|
|
109
|
-
case 'vp':
|
|
110
|
-
case '
|
|
111
|
-
case 'user':
|
|
109
|
+
case 'vp': shards = [...VP_DEFAULT_SHARDS]; break;
|
|
110
|
+
case 'feature': shards = [...FEATURE_SHARDS]; break;
|
|
111
|
+
case 'user': shards = [...USER_SHARDS]; break;
|
|
112
112
|
default: throw new Error(`buildShardSchema: unknown kind "${kind}"`);
|
|
113
113
|
}
|
|
114
114
|
if (Array.isArray(opts.extraShards)) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* entries/<yyyy-mm-dd>-<slug>.md
|
|
11
11
|
* groups/<groupId>/ — same shape
|
|
12
12
|
* vp/<vpId>/ — same shape
|
|
13
|
-
*
|
|
13
|
+
* features/<featureId>/ — same shape, plus archive/
|
|
14
14
|
*
|
|
15
15
|
* This module is concerned ONLY with on-disk shape + atomic writes. It does
|
|
16
16
|
* NOT do any LLM work (extraction, summarisation, dream maintenance) — those
|
|
@@ -50,12 +50,12 @@ import { homedir } from 'os';
|
|
|
50
50
|
/** Default memory root. Tests override via `opts.root`. */
|
|
51
51
|
export const DEFAULT_MEMORY_ROOT = join(homedir(), '.yeaft', 'memory');
|
|
52
52
|
|
|
53
|
-
/** @typedef {'user'|'group'|'vp'|'
|
|
53
|
+
/** @typedef {'user'|'group'|'vp'|'feature'} ScopeKind */
|
|
54
54
|
/** @typedef {{kind: ScopeKind, id?: string}} Scope */
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Compute the scope's path segment relative to the memory root.
|
|
58
|
-
* `user/`, `groups/<id>/`, `vp/<id>/`, `
|
|
58
|
+
* `user/`, `groups/<id>/`, `vp/<id>/`, `features/<id>/`.
|
|
59
59
|
*
|
|
60
60
|
* @param {Scope} scope
|
|
61
61
|
* @returns {string}
|
|
@@ -73,9 +73,9 @@ export function scopeDir(scope) {
|
|
|
73
73
|
case 'vp':
|
|
74
74
|
if (!scope.id) throw new Error('scopeDir: vp scope requires id');
|
|
75
75
|
return `vp/${scope.id}`;
|
|
76
|
-
case '
|
|
77
|
-
if (!scope.id) throw new Error('scopeDir:
|
|
78
|
-
return `
|
|
76
|
+
case 'feature':
|
|
77
|
+
if (!scope.id) throw new Error('scopeDir: feature scope requires id');
|
|
78
|
+
return `features/${scope.id}`;
|
|
79
79
|
default:
|
|
80
80
|
throw new Error(`scopeDir: unknown kind ${JSON.stringify(scope.kind)}`);
|
|
81
81
|
}
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
*/
|
|
75
75
|
|
|
76
76
|
import { MAIN_THREAD_ID } from '../threads/store.js';
|
|
77
|
-
import {
|
|
77
|
+
import { getFeatureStore } from '../tools/feature-tools.js';
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* Per-entry transient metadata (messageId, override) lives here — a
|
|
@@ -220,7 +220,7 @@ export class Dispatcher {
|
|
|
220
220
|
const allThreads = threadStore.list().map(t => ({
|
|
221
221
|
id: t.id, name: t.name, goal: t.goal, status: t.status,
|
|
222
222
|
}));
|
|
223
|
-
const
|
|
223
|
+
const pendingFeatures = this.#listPendingFeatures();
|
|
224
224
|
|
|
225
225
|
// ── Step 3: classify (explicit override > classifier) ──
|
|
226
226
|
/** @type {import('../router/intent-classifier.js').RouterDecision} */
|
|
@@ -245,7 +245,7 @@ export class Dispatcher {
|
|
|
245
245
|
userMessage: claimed.text,
|
|
246
246
|
currentThreadId,
|
|
247
247
|
allThreads,
|
|
248
|
-
|
|
248
|
+
pendingFeatures,
|
|
249
249
|
messageId: meta.messageId || undefined,
|
|
250
250
|
});
|
|
251
251
|
} catch (err) {
|
|
@@ -347,12 +347,12 @@ export class Dispatcher {
|
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
-
#
|
|
351
|
-
// Best-effort: the
|
|
350
|
+
#listPendingFeatures() {
|
|
351
|
+
// Best-effort: the FeatureStore is a singleton initialised in loadSession().
|
|
352
352
|
// If the store isn't available (e.g. unit tests without a session) we
|
|
353
|
-
// just return []. Never let a
|
|
353
|
+
// just return []. Never let a FeatureStore exception break routing.
|
|
354
354
|
try {
|
|
355
|
-
const store =
|
|
355
|
+
const store = getFeatureStore();
|
|
356
356
|
if (!store || typeof store.list !== 'function') return [];
|
|
357
357
|
const pending = store.list({ status: 'pending' }) || [];
|
|
358
358
|
return pending.map(t => ({
|