@yeaft/webchat-agent 0.1.605 → 0.1.606
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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dream-v2/refresh.js — DESIGN.md §9.14 refresh hook (Phase 8 PR-J).
|
|
3
|
+
*
|
|
4
|
+
* The PR-F wire-up plumbed `runDreamTick` into the scheduler with a v1
|
|
5
|
+
* no-op refresh placeholder. PR-J makes the refresh real but stays
|
|
6
|
+
* within the §8 v1 charter — "Thin: skip pruning/demotion; refresh-only
|
|
7
|
+
* in v1, no LLM".
|
|
8
|
+
*
|
|
9
|
+
* What "refresh" does today:
|
|
10
|
+
* 1. Read the scope's `index.md` rows (the entry catalog).
|
|
11
|
+
* 2. Select the top-N most recent rows by `updated` timestamp
|
|
12
|
+
* (default 12 — enough to surface the active context without
|
|
13
|
+
* exploding Layer A token cost).
|
|
14
|
+
* 3. Render a deterministic markdown synopsis (one bullet per row:
|
|
15
|
+
* `- <title> [<kind>; tags] (<updated>)`).
|
|
16
|
+
* 4. Write to the scope's `summary.md` via `writeSummary` (atomic
|
|
17
|
+
* rename — readers never see a partial write).
|
|
18
|
+
*
|
|
19
|
+
* If the index is empty (cold-start scope), we leave `summary.md`
|
|
20
|
+
* alone. Overwriting with an empty file would erase any human-written
|
|
21
|
+
* synopsis a future tool may have placed there.
|
|
22
|
+
*
|
|
23
|
+
* No LLM call. No pruning. No tombstones. The cursor is advanced by
|
|
24
|
+
* `runDreamTick` after this hook resolves, so a clean run of refresh
|
|
25
|
+
* counts as "scope handled" and we won't re-run until the diff-gate
|
|
26
|
+
* sees new content.
|
|
27
|
+
*
|
|
28
|
+
* Errors are propagated — `runDreamTick` already catches per-scope
|
|
29
|
+
* failures and records them under `errors[]` without aborting siblings.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { readIndex, writeSummary } from '../memory/scope-tree.js';
|
|
33
|
+
|
|
34
|
+
/** Default number of recent entries surfaced on the synopsis. */
|
|
35
|
+
export const DEFAULT_TOP_N = 12;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render a deterministic markdown synopsis for a scope from its index rows.
|
|
39
|
+
* Pure function — exported for unit tests.
|
|
40
|
+
*
|
|
41
|
+
* @param {{ kind: string, id?: string, scopeDir: string }} scope
|
|
42
|
+
* @param {Array<{ path: string, title?: string, tags?: string[]|string, kind?: string, updated?: string }>} rows
|
|
43
|
+
* @param {number} [topN]
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
export function buildScopeSynopsis(scope, rows, topN = DEFAULT_TOP_N) {
|
|
47
|
+
if (!Array.isArray(rows) || rows.length === 0) return '';
|
|
48
|
+
|
|
49
|
+
// Sort by `updated` desc, then by path asc as a stable tiebreaker.
|
|
50
|
+
const sorted = [...rows].sort((a, b) => {
|
|
51
|
+
const u = (b.updated || '').localeCompare(a.updated || '');
|
|
52
|
+
return u !== 0 ? u : (a.path || '').localeCompare(b.path || '');
|
|
53
|
+
});
|
|
54
|
+
const top = sorted.slice(0, Math.max(1, topN | 0));
|
|
55
|
+
|
|
56
|
+
const header = `# ${scope.scopeDir} — recent context`;
|
|
57
|
+
const lines = top.map((r) => {
|
|
58
|
+
const title = (r.title || r.path || 'entry').trim();
|
|
59
|
+
const kind = r.kind ? `${r.kind}` : '';
|
|
60
|
+
const tagsRaw = Array.isArray(r.tags) ? r.tags : (typeof r.tags === 'string' ? r.tags.split(',') : []);
|
|
61
|
+
const tags = tagsRaw.map((t) => String(t).trim()).filter(Boolean);
|
|
62
|
+
const meta = [kind, ...tags].filter(Boolean).join('; ');
|
|
63
|
+
const metaPart = meta ? ` [${meta}]` : '';
|
|
64
|
+
const updated = r.updated ? ` (${r.updated})` : '';
|
|
65
|
+
return `- ${title}${metaPart}${updated}`;
|
|
66
|
+
});
|
|
67
|
+
return [header, '', ...lines, ''].join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Translate a `runDreamTick` ScopeRef ({ kind, id?, scopeDir }) into a
|
|
72
|
+
* `scope-tree.js` Scope ({ kind, id }). The two surfaces share the
|
|
73
|
+
* `kind` enum but `runDreamTick` carries `scopeDir` as the canonical
|
|
74
|
+
* identifier; scope-tree derives it from `kind`+`id`.
|
|
75
|
+
*
|
|
76
|
+
* @param {{ kind: string, id?: string, scopeDir: string }} ref
|
|
77
|
+
* @returns {{ kind: string, id?: string }}
|
|
78
|
+
*/
|
|
79
|
+
export function refToScope(ref) {
|
|
80
|
+
if (!ref || typeof ref !== 'object') {
|
|
81
|
+
throw new Error('refToScope: ref required');
|
|
82
|
+
}
|
|
83
|
+
if (ref.kind === 'user') return { kind: 'user' };
|
|
84
|
+
if (!ref.id) throw new Error(`refToScope: ${ref.kind} scope requires id`);
|
|
85
|
+
return { kind: ref.kind, id: ref.id };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build a refresh hook bound to a specific memory root. The returned
|
|
90
|
+
* function is the `refresh` arg `runDreamTick` expects.
|
|
91
|
+
*
|
|
92
|
+
* @param {{ root: string, topN?: number }} args
|
|
93
|
+
* @returns {(scope: { kind: string, id?: string, scopeDir: string }) => Promise<void>}
|
|
94
|
+
*/
|
|
95
|
+
export function createScopeRefreshHook({ root, topN = DEFAULT_TOP_N } = {}) {
|
|
96
|
+
if (!root || typeof root !== 'string') {
|
|
97
|
+
throw new Error('createScopeRefreshHook: root required');
|
|
98
|
+
}
|
|
99
|
+
return async function refreshScopeSummary(scope) {
|
|
100
|
+
const target = refToScope(scope);
|
|
101
|
+
const rows = await readIndex(target, { root });
|
|
102
|
+
const body = buildScopeSynopsis(scope, rows, topN);
|
|
103
|
+
if (!body) {
|
|
104
|
+
// Cold-start scope: leave summary.md alone (see header docs).
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await writeSummary(target, body, { root });
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -25,6 +25,7 @@ import { dreamShard } from './dream-shard.js';
|
|
|
25
25
|
import { checkRecompression } from './recompression.js';
|
|
26
26
|
import { runUserDreamJob } from './user-memory-store.js';
|
|
27
27
|
import { runDreamTick } from '../dream-v2/tick.js';
|
|
28
|
+
import { createScopeRefreshHook } from '../dream-v2/refresh.js';
|
|
28
29
|
|
|
29
30
|
/** Default idle timeout before dream triggers (ms). */
|
|
30
31
|
export const DREAM_IDLE_MS = 30 * 60 * 1000; // 30 min
|
|
@@ -169,16 +170,14 @@ export function createDreamScheduler(opts = {}) {
|
|
|
169
170
|
try {
|
|
170
171
|
const scopes = [{ kind: 'user', scopeDir: 'user' }];
|
|
171
172
|
if (group?.id) scopes.push({ kind: 'group', id: group.id, scopeDir: `groups/${group.id}` });
|
|
173
|
+
// PR-J: real refresh hook — read each scope's `index.md`,
|
|
174
|
+
// render a deterministic top-N synopsis, atomically write
|
|
175
|
+
// `summary.md`. No LLM, refresh-only (DESIGN.md §8 line 395).
|
|
176
|
+
const refresh = createScopeRefreshHook({ root: memoryDir });
|
|
172
177
|
const tickResult = await runDreamTick({
|
|
173
178
|
root: memoryDir,
|
|
174
179
|
scopes,
|
|
175
|
-
refresh
|
|
176
|
-
// v1 refresh hook: no-op placeholder. The actual scope
|
|
177
|
-
// summary refresh continues to flow through the legacy
|
|
178
|
-
// shard / user-dream paths above; this tick only
|
|
179
|
-
// exercises the diff-gate + cursor write so future hook
|
|
180
|
-
// implementations can plug in without re-wiring.
|
|
181
|
-
},
|
|
180
|
+
refresh,
|
|
182
181
|
});
|
|
183
182
|
result.dreamV2Tick = {
|
|
184
183
|
ran: tickResult.ran.length,
|