@yeaft/webchat-agent 0.1.605 → 0.1.607

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.605",
3
+ "version": "0.1.607",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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: async (_scope) => {
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,
package/unify/prompts.js CHANGED
@@ -207,7 +207,6 @@ const PROMPTS = {
207
207
  userProfileHeader: '## user_profile',
208
208
  coreMemoryHeader: '## core_memory',
209
209
  coreMemoryMeta: 'To open the original message behind any entry above, call `memory_trace`.',
210
- vpPersonaHeader: '## active_persona',
211
210
  vpPersonaIntro: (name, role) =>
212
211
  `You ARE **${name}**${role ? ` (${role})` : ''}. Speak in the first person as ${name}; do not refer to yourself as "Yeaft" or as a generic AI assistant. The text below is your identity, expertise, and decision style.`,
213
212
  },
@@ -228,7 +227,6 @@ const PROMPTS = {
228
227
  userProfileHeader: '## user_profile',
229
228
  coreMemoryHeader: '## core_memory',
230
229
  coreMemoryMeta: '如需原始 message,调 `memory_trace`。',
231
- vpPersonaHeader: '## active_persona',
232
230
  vpPersonaIntro: (name, role) =>
233
231
  `你就是 **${name}**${role ? `(${role})` : ''}。请以 ${name} 的第一人称发言;不要自称 "Yeaft" 或泛指的 AI 助手。下面的文字是你的身份、专业方向与判断风格。`,
234
232
  },