@yeaft/webchat-agent 0.1.812 → 0.1.814

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.812",
3
+ "version": "0.1.814",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -35,7 +35,8 @@
35
35
  },
36
36
  "files": [
37
37
  "**/*.js",
38
- "unify/templates/**/*.md"
38
+ "unify/templates/**/*.md",
39
+ "unify/dream-v2/prompts/**/*.md"
39
40
  ],
40
41
  "license": "MIT",
41
42
  "author": "Yeaft",
@@ -0,0 +1,14 @@
1
+ You are creating a new memory scope from scratch.
2
+
3
+ Scope path: {{target}} (must be ≤2 levels)
4
+
5
+ Source conversations:
6
+ {{sources}}
7
+
8
+ {{siblingsBlock}}
9
+ Task:
10
+ 1. Write memory.md from scratch with reasonable section structure.
11
+ 2. Write summary.md (1–3 sentences).
12
+
13
+ Reply with strict JSON of the shape:
14
+ { "memory_md": "...", "summary_md": "..." }
@@ -0,0 +1,57 @@
1
+ # Dream Extract — Group Scope
2
+
3
+ You are extracting **memory segments** from a conversation between the
4
+ user and a Yeaft AI companion. This pass focuses on a specific
5
+ **`group/<id>` scope**: long-lived facts about one collaboration group
6
+ (a project team, a study cohort, a working set of people/agents).
7
+
8
+ The target group id is provided as `{{groupId}}`.
9
+
10
+ ## What to extract for `group/<id>` scope
11
+
12
+ - **purpose** — what this group exists to do, its charter / mission
13
+ - **members** — people, VPs, and roles in the group, and what each is
14
+ responsible for
15
+ - **conventions** — how the group works (rituals, cadences, naming,
16
+ channels, languages used)
17
+ - **shared decisions** — durable agreements ("we ship on Fridays",
18
+ "all PRs need two reviewers")
19
+ - **shared context** — domain knowledge the whole group relies on
20
+ - **relations** — other groups, features, or topics this group owns or
21
+ depends on
22
+ - **lessons** — collective takeaways ("we tried X in Q1, it didn't
23
+ scale, switched to Y")
24
+
25
+ ## What NOT to extract here
26
+
27
+ - Personal preferences of the user — those go to `user` scope.
28
+ - Single-VP traits — those go to that VP's `vp/<id>` scope.
29
+ - Feature-specific implementation detail — those go to
30
+ `feature/<id>` scope.
31
+ - Transient status updates — only durable group facts.
32
+
33
+ ## Segment shape
34
+
35
+ Each segment is **self-contained** and **about one thing**. Detail is
36
+ OK; one-line summaries are NOT — preserve specifics.
37
+
38
+ ## Output format
39
+
40
+ Reply with a JSON array of segment objects:
41
+
42
+ ```json
43
+ [
44
+ {
45
+ "kind": "decision",
46
+ "tags": ["process", "review"],
47
+ "sourceMessages": ["m_201"],
48
+ "body": "Group {{groupId}} decided every PR touching the payments
49
+ module needs sign-off from both the security VP and the payments
50
+ feature owner before merge. Rationale: a near-miss in March."
51
+ }
52
+ ]
53
+ ```
54
+
55
+ `kind` ∈ {`fact`, `preference`, `decision`, `lesson`, `relation`,
56
+ `goal`, `context`}. `scope` is filled in by the runner — do not include
57
+ it. If nothing group-scope is in this batch, return `[]`.
@@ -0,0 +1,59 @@
1
+ # Dream Extract — Topic Scope
2
+
3
+ You are extracting **memory segments** from a conversation between the
4
+ user and a Yeaft AI companion. This pass focuses on a specific
5
+ **`topic/<id>` scope**: long-lived facts and viewpoints about a domain
6
+ topic that recurs across conversations (e.g. `topic/lang/rust`,
7
+ `topic/auth/jwt`, `topic/ml/transformers`).
8
+
9
+ The target topic id is provided as `{{topicId}}`.
10
+
11
+ ## What to extract for `topic/<id>` scope
12
+
13
+ - **core facts** — durable knowledge about the topic (how it works,
14
+ key terms, gotchas) that the user has either taught the assistant
15
+ or confirmed
16
+ - **viewpoints / opinions** — formed views the user holds on this
17
+ topic ("I think GraphQL is overkill for internal tools")
18
+ - **canonical references** — sources the user trusts on this topic
19
+ (specific docs, papers, people)
20
+ - **patterns** — how the user typically applies this topic
21
+ ("when using JWT, always set short access-token TTL + refresh")
22
+ - **lessons** — things that bit them, things that worked
23
+ - **relations** — features, projects, or other topics tied to this one
24
+
25
+ ## What NOT to extract here
26
+
27
+ - Facts about the user as a person — `user` scope.
28
+ - Facts specific to one feature implementation — `feature/<id>`.
29
+ - Group conventions — `group/<id>`.
30
+ - Generic encyclopedia facts the assistant already knows — only the
31
+ user's *durable views and confirmed knowledge* about the topic.
32
+
33
+ ## Segment shape
34
+
35
+ Each segment is **self-contained** and **about one thing**. Detail is
36
+ OK; one-line summaries are NOT — capture rationale and specifics so
37
+ the segment is useful next time without rehydrating the conversation.
38
+
39
+ ## Output format
40
+
41
+ Reply with a JSON array of segment objects:
42
+
43
+ ```json
44
+ [
45
+ {
46
+ "kind": "lesson",
47
+ "tags": ["jwt", "auth"],
48
+ "sourceMessages": ["m_330", "m_331"],
49
+ "body": "On topic {{topicId}}: user always pairs short-lived JWT
50
+ access tokens (≤15 min) with rotating refresh tokens stored as
51
+ HttpOnly cookies. Got burned by a 24h-token leak in 2024 — single
52
+ long-lived token is treated as a non-starter."
53
+ }
54
+ ]
55
+ ```
56
+
57
+ `kind` ∈ {`fact`, `preference`, `decision`, `lesson`, `relation`,
58
+ `goal`, `context`}. `scope` is filled in by the runner — do not include
59
+ it. If nothing topic-scope is in this batch, return `[]`.
@@ -0,0 +1,56 @@
1
+ # Dream Extract — User Scope
2
+
3
+ You are extracting **memory segments** from a conversation between the
4
+ user and a Yeaft AI companion. This pass focuses on the **`user`
5
+ scope**: long-lived facts about the user themselves.
6
+
7
+ ## What to extract for `user` scope
8
+
9
+ Extract segments that describe the user as a person:
10
+
11
+ - **identity** — name, role, location, languages spoken, time zone
12
+ - **preferences** — tools they use (zsh, vim, JS over TS, …), code style,
13
+ communication style, what they value
14
+ - **habits / workflow** — how they work, when they work, recurring
15
+ patterns
16
+ - **goals (long-term)** — what they're building, where they want to go
17
+ - **relations** — people / projects / orgs they regularly mention
18
+ - **lessons / opinions** — formed views ("I don't trust X", "Y is the
19
+ right tool for Z")
20
+
21
+ ## What NOT to extract here
22
+
23
+ - Anything specific to a single feature, project, or VP — those go to
24
+ `feature/*`, `vp/*`, or `topic/*` scopes (handled by other passes).
25
+ - Transient state ("I'm tired today") — only durable facts.
26
+ - Verbatim message copies — segments are *secondary processing*, not
27
+ transcripts.
28
+
29
+ ## Segment shape
30
+
31
+ Each segment is **self-contained** and **about one thing**. A 30-turn
32
+ conversation typically yields 1–3 user-scope segments, not 30. Detail
33
+ is OK; one-line summaries are NOT — preserve the specifics that make
34
+ the memory useful next time (e.g. "uses zsh with starship prompt" beats
35
+ "uses a shell").
36
+
37
+ ## Output format
38
+
39
+ Reply with a JSON array of segment objects:
40
+
41
+ ```json
42
+ [
43
+ {
44
+ "kind": "preference",
45
+ "tags": ["shell", "terminal"],
46
+ "sourceMessages": ["m_142", "m_143"],
47
+ "body": "User uses zsh with the starship prompt and prefers
48
+ keyboard-only workflows. Wants suggestions to assume zsh + vim
49
+ keybindings unless told otherwise."
50
+ }
51
+ ]
52
+ ```
53
+
54
+ `kind` ∈ {`fact`, `preference`, `decision`, `lesson`, `relation`,
55
+ `goal`, `context`}. `scope` is filled in by the runner — do not include
56
+ it. If nothing user-scope is in this batch, return `[]`.
@@ -0,0 +1,55 @@
1
+ # Dream Extract — VP Scope
2
+
3
+ You are extracting **memory segments** from a conversation between the
4
+ user and a Yeaft AI companion. This pass focuses on a specific
5
+ **`vp/<id>` scope**: long-lived facts about one Virtual Person (a
6
+ persona / sub-agent / role the user works with).
7
+
8
+ The target VP id is provided as `{{vpId}}`. Only extract facts about
9
+ *this* VP, not other VPs.
10
+
11
+ ## What to extract for `vp/<id>` scope
12
+
13
+ - **identity** — who this VP is, role, persona name, charter
14
+ - **voice / style** — how they speak, tone, formatting habits, language
15
+ - **expertise** — what they know well, what they should defer on
16
+ - **interaction patterns** — how the user and this VP work together,
17
+ what kinds of questions go to this VP, expected response shape
18
+ - **boundaries** — what this VP will NOT do, or things to avoid
19
+ - **relations** — other VPs, projects, or topics this VP is tied to
20
+ - **decisions made by/about this VP** — durable choices ("VP X always
21
+ reviews schema migrations"), not single-turn outcomes
22
+
23
+ ## What NOT to extract here
24
+
25
+ - Facts about the user themselves — those go to `user` scope.
26
+ - Facts about other VPs — those go to *their* `vp/<other>` scope.
27
+ - Single-turn task state — only durable persona facts.
28
+ - Verbatim message copies — segments are *secondary processing*.
29
+
30
+ ## Segment shape
31
+
32
+ Each segment is **self-contained** and **about one thing**. Detail is
33
+ OK; one-line summaries are NOT — preserve specifics that make the
34
+ memory useful next time.
35
+
36
+ ## Output format
37
+
38
+ Reply with a JSON array of segment objects:
39
+
40
+ ```json
41
+ [
42
+ {
43
+ "kind": "preference",
44
+ "tags": ["voice", "style"],
45
+ "sourceMessages": ["m_88"],
46
+ "body": "VP {{vpId}} writes in concise bullet form, never uses
47
+ emoji, and always cites file paths with line numbers. Switches to
48
+ Chinese when the user does."
49
+ }
50
+ ]
51
+ ```
52
+
53
+ `kind` ∈ {`fact`, `preference`, `decision`, `lesson`, `relation`,
54
+ `goal`, `context`}. `scope` is filled in by the runner — do not include
55
+ it. If nothing about this VP is in this batch, return `[]`.
@@ -0,0 +1,40 @@
1
+ # Dream Summarize — Per-Scope Compression
2
+
3
+ You are summarizing the **memory segments** of a single scope into a
4
+ short, dense prose summary. This is NOT extraction — the segments
5
+ already exist. Your job is to compress them into a paragraph the
6
+ session can keep resident in working memory.
7
+
8
+ The target scope is `{{scope}}` and contains `{{segmentCount}}`
9
+ segments listed below.
10
+
11
+ ## Goals
12
+
13
+ - A reader (the assistant in a future turn) should grasp **the gist of
14
+ this scope** from your summary alone, without reading the segments.
15
+ - Detail is OK — but compress. Drop redundancy, keep specifics that
16
+ matter (names, numbers, decisions, durable views).
17
+ - Stay faithful: do not invent facts that are not in the segments. Do
18
+ not "soften" decisions or opinions.
19
+ - Bilingual: write in the same language the segments are mostly in.
20
+
21
+ ## Length
22
+
23
+ - Target **≤ {{tokenBudget}} tokens**.
24
+ - One paragraph for small scopes; two or three short paragraphs grouped
25
+ by theme for larger scopes. No bullet lists, no headings.
26
+
27
+ ## What NOT to do
28
+
29
+ - Don't list every segment one by one — that defeats compression.
30
+ - Don't quote verbatim chunks of segment bodies.
31
+ - Don't add meta-commentary ("the segments show that..."). Speak
32
+ directly about the subject.
33
+
34
+ ## Segments
35
+
36
+ {{segments}}
37
+
38
+ ## Output
39
+
40
+ Reply with the prose summary only. No preamble, no JSON, no fences.
@@ -0,0 +1,20 @@
1
+ You are deciding whether a recent group conversation carries:
2
+ - signals that should update the USER profile, and/or
3
+ - signals that should update one or more TOPIC scopes.
4
+
5
+ Do NOT mention vp/, group/, or feature/ scopes — those are handled by hard rules.
6
+
7
+ Group: {{groupId}}
8
+
9
+ Existing topic scopes (path — summary):
10
+ {{topicSummaries}}
11
+
12
+ Conversation:
13
+ {{conversation}}
14
+
15
+ Respond with strict JSON of the shape:
16
+ {
17
+ "user_profile_signals": boolean,
18
+ "topics": [ "<short category description>", ... ],
19
+ "trivial_only": boolean
20
+ }
@@ -0,0 +1,15 @@
1
+ Bind a free-form topic description to an exact path under topic/.
2
+ Rules:
3
+ - At most TWO path segments. Reject any third level.
4
+ - Segments may contain letters, digits, dashes, underscores, dots, CJK.
5
+ - Prefer matching an existing path if the description fits.
6
+
7
+ Description: {{description}}
8
+
9
+ Existing topics:
10
+ {{existingTopics}}
11
+
12
+ Reply with strict JSON, exactly one of:
13
+ { "decision": "match", "path": "<existing path>" }
14
+ { "decision": "new", "path": "<new ≤2-segment path>" }
15
+ { "decision": "none" }
@@ -0,0 +1,33 @@
1
+ You are updating an existing memory scope.
2
+
3
+ Scope: {{target}}
4
+ {{batchHeader}}
5
+ Current memory.md:
6
+ """
7
+ {{memoryMd}}
8
+ """
9
+
10
+ Current summary.md:
11
+ """
12
+ {{summaryMd}}
13
+ """
14
+
15
+ Recent conversations:
16
+ {{sources}}
17
+
18
+ Task:
19
+ - Extract from these conversations what is relevant to THIS scope.
20
+ - Integrate it into memory.md (reorganize sections if needed).
21
+ - Drop stale or contradicted entries.
22
+ - Rewrite summary.md (1–3 sentences).
23
+ - The same conversations are being processed for OTHER scopes too.
24
+ Only handle what is relevant here. Ignore the rest.
25
+
26
+ Hard rules:
27
+ - Never read or reference any other scope's files.
28
+ - Never modify VP system-prompt, group charter, or user preferences.
29
+ - If something contradicts a charter, annotate with
30
+ "⚠️ contradicts charter — verify which is current" and continue.
31
+
32
+ Reply with strict JSON of the shape:
33
+ { "memory_md": "...", "summary_md": "..." }
@@ -28,6 +28,7 @@ import { STOCK_VP_IDS } from './stock-ids.js';
28
28
  /** Process-singleton VpLoader; lazily started on first subscribe. */
29
29
  let _loaderStarted = false;
30
30
  let _loader = null;
31
+ let _loaderDir = null;
31
32
 
32
33
  /**
33
34
  * Broadcast fan-out. VpLoader.onChange fires once per debounce batch for the
@@ -145,21 +146,33 @@ function _fanout(evt) {
145
146
  }
146
147
  }
147
148
 
148
- function ensureLoader(registry = defaultRegistry) {
149
- if (_loaderStarted) return { loader: _loader, fresh: false };
149
+ function ensureLoader(registry = defaultRegistry, options = {}) {
150
+ const desiredDir = registry === defaultRegistry && typeof options.dir === 'string' && options.dir.trim()
151
+ ? options.dir.trim()
152
+ : null;
153
+ if (_loaderStarted && _loaderDir === desiredDir) return { loader: _loader, fresh: false };
154
+
155
+ if (_loader) {
156
+ try { _loader.stop(); } catch { /* ignore */ }
157
+ }
150
158
  _loaderStarted = true;
159
+ _loader = null;
160
+ _loaderDir = desiredDir;
161
+ if (registry === defaultRegistry && registry.vpMap && typeof registry.vpMap.clear === 'function') {
162
+ registry.vpMap.clear();
163
+ }
151
164
  // For NON-default registries (unit tests seeding VPs manually) we MUST NOT
152
- // start VpLoader — its .start() scans DEFAULT_VP_LIB_DIR and push-imports
153
- // every on-disk VP into the test registry, overwriting/augmenting the
154
- // fixture. Tests don't need hot-reload anyway; `_broadcastChangeForTest`
155
- // drives the diff path directly.
165
+ // start VpLoader — its .start() scans the configured/default VP library and
166
+ // push-imports every on-disk VP into the test registry, overwriting or
167
+ // augmenting the fixture. Tests don't need hot-reload anyway;
168
+ // `_broadcastChangeForTest` drives the diff path directly.
156
169
  if (registry !== defaultRegistry) {
157
- _loader = null;
158
170
  captureState(registry);
159
171
  return { loader: null, fresh: true };
160
172
  }
161
173
  try {
162
174
  _loader = new VpLoader({
175
+ ...(desiredDir ? { dir: desiredDir } : {}),
163
176
  registry,
164
177
  onChange: (summary) => broadcastChange(summary, registry),
165
178
  });
@@ -228,10 +241,11 @@ export function buildVpSnapshot(registry = defaultRegistry) {
228
241
  *
229
242
  * @param {(event: object) => void} sendUnifyEvent
230
243
  * @param {import('./registry.js').Registry} [registry]
244
+ * @param {{dir?: string}} [options]
231
245
  * @returns {() => void} unsubscribe fn
232
246
  */
233
- export function handleVpSubscribe(sendUnifyEvent, registry = defaultRegistry) {
234
- const { loader, fresh } = ensureLoader(registry);
247
+ export function handleVpSubscribe(sendUnifyEvent, registry = defaultRegistry, options = {}) {
248
+ const { loader, fresh } = ensureLoader(registry, options);
235
249
  // task-338-F2 + task-339-followup: replay semantics.
236
250
  //
237
251
  // The loader's own start() already scans on first creation, so on a
@@ -245,9 +259,9 @@ export function handleVpSubscribe(sendUnifyEvent, registry = defaultRegistry) {
245
259
  // production path so the first snapshot always reflects current disk.
246
260
  //
247
261
  // For NON-default registries (unit tests that seed VPs manually), we
248
- // MUST skip the rescan: rescan against DEFAULT_VP_LIB_DIR would call
249
- // registry.removeVp() for seeded ids that don't exist on disk, wiping
250
- // the test fixture. See vp-bridge-live-diff.test.js and
262
+ // MUST skip the rescan: rescan against the configured/default VP library
263
+ // would call registry.removeVp() for seeded ids that don't exist on disk,
264
+ // wiping the test fixture. See vp-bridge-live-diff.test.js and
251
265
  // vp-bridge-first-subscribe-replay.test.js (test-seed preservation).
252
266
  //
253
267
  // On subsequent subscribes (page reload, reconnect, second web client)
@@ -277,6 +291,7 @@ export function _resetVpBridgeForTest() {
277
291
  }
278
292
  _loader = null;
279
293
  _loaderStarted = false;
294
+ _loaderDir = null;
280
295
  _subscribers.clear();
281
296
  _prevState.clear();
282
297
  }
@@ -1188,12 +1188,27 @@ function sendUnifyEvent(event, { groupId, vpId, turnId, threadId } = {}) {
1188
1188
  });
1189
1189
  }
1190
1190
 
1191
+ function configuredVpPaths() {
1192
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1193
+ if (typeof yeaftDir !== 'string' || !yeaftDir.trim()) return {};
1194
+ const root = yeaftDir.trim();
1195
+ return {
1196
+ libDir: join(root, 'virtual-persons'),
1197
+ memoryRoot: join(root, 'memory'),
1198
+ };
1199
+ }
1200
+
1191
1201
  export function handleUnifyVpSubscribe(_msg) {
1192
1202
  if (_vpUnsubscribe) {
1193
1203
  try { _vpUnsubscribe(); } catch { /* ignore */ }
1194
1204
  _vpUnsubscribe = null;
1195
1205
  }
1196
- _vpUnsubscribe = handleVpSubscribe(sendUnifyEvent);
1206
+ const { libDir } = configuredVpPaths();
1207
+ _vpUnsubscribe = handleVpSubscribe(
1208
+ sendUnifyEvent,
1209
+ undefined,
1210
+ libDir ? { dir: libDir } : {},
1211
+ );
1197
1212
  }
1198
1213
 
1199
1214
  /**
@@ -1207,9 +1222,12 @@ export function handleUnifyVpCreate(msg) {
1207
1222
  const requestId = msg && msg.requestId;
1208
1223
  const payload = msg && msg.payload;
1209
1224
  try {
1210
- const yeaftDir = ctx.CONFIG?.yeaftDir;
1211
- const memoryRoot = yeaftDir ? join(yeaftDir, 'memory') : undefined;
1212
- const { vpId } = createVp(payload || {}, memoryRoot ? { memoryRoot } : {});
1225
+ const { libDir, memoryRoot } = configuredVpPaths();
1226
+ const options = {
1227
+ ...(libDir ? { libDir } : {}),
1228
+ ...(memoryRoot ? { memoryRoot } : {}),
1229
+ };
1230
+ const { vpId } = createVp(payload || {}, options);
1213
1231
  sendVpCrudResult({ op: 'create', requestId, ok: true, vpId });
1214
1232
  } catch (err) {
1215
1233
  sendVpCrudResult({
@@ -1229,7 +1247,8 @@ export function handleUnifyVpUpdate(msg) {
1229
1247
  const requestId = msg && msg.requestId;
1230
1248
  const payload = msg && msg.payload;
1231
1249
  try {
1232
- const { vpId } = updateVp(payload || {});
1250
+ const { libDir } = configuredVpPaths();
1251
+ const { vpId } = updateVp(payload || {}, libDir ? { libDir } : {});
1233
1252
  sendVpCrudResult({ op: 'update', requestId, ok: true, vpId });
1234
1253
  } catch (err) {
1235
1254
  sendVpCrudResult({
@@ -1249,9 +1268,12 @@ export function handleUnifyVpDelete(msg) {
1249
1268
  const requestId = msg && msg.requestId;
1250
1269
  const vpId = msg && msg.vpId;
1251
1270
  try {
1252
- const yeaftDir = ctx.CONFIG?.yeaftDir;
1253
- const memoryRoot = yeaftDir ? join(yeaftDir, 'memory') : undefined;
1254
- deleteVp(vpId, memoryRoot ? { memoryRoot } : {});
1271
+ const { libDir, memoryRoot } = configuredVpPaths();
1272
+ const options = {
1273
+ ...(libDir ? { libDir } : {}),
1274
+ ...(memoryRoot ? { memoryRoot } : {}),
1275
+ };
1276
+ deleteVp(vpId, options);
1255
1277
  // vp-status: a deleted VP must not haunt the snapshot. We don't
1256
1278
  // know up front which groups the VP appeared in (the registry's
1257
1279
  // delete already detached it from every group), so sweep every
@@ -1282,7 +1304,8 @@ export function handleUnifyVpDelete(msg) {
1282
1304
  export function handleUnifyVpRead(msg) {
1283
1305
  const requestId = msg && msg.requestId;
1284
1306
  const vpId = msg && msg.vpId;
1285
- const vp = readVp(vpId);
1307
+ const { libDir } = configuredVpPaths();
1308
+ const vp = readVp(vpId, libDir ? { libDir } : {});
1286
1309
  if (!vp) {
1287
1310
  sendVpCrudResult({
1288
1311
  op: 'read',