@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 +3 -2
- package/unify/dream-v2/prompts/create.md +14 -0
- package/unify/dream-v2/prompts/extract-group.md +57 -0
- package/unify/dream-v2/prompts/extract-topic.md +59 -0
- package/unify/dream-v2/prompts/extract-user.md +56 -0
- package/unify/dream-v2/prompts/extract-vp.md +55 -0
- package/unify/dream-v2/prompts/summarize-scope.md +40 -0
- package/unify/dream-v2/prompts/triage-pass1.md +20 -0
- package/unify/dream-v2/prompts/triage-pass2.md +15 -0
- package/unify/dream-v2/prompts/update.md +33 -0
- package/unify/vp/vp-bridge.js +27 -12
- package/unify/web-bridge.js +32 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yeaft/webchat-agent",
|
|
3
|
-
"version": "0.1.
|
|
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": "..." }
|
package/unify/vp/vp-bridge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
153
|
-
// every on-disk VP into the test registry, overwriting
|
|
154
|
-
// fixture. Tests don't need hot-reload anyway;
|
|
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
|
|
249
|
-
// registry.removeVp() for seeded ids that don't exist on disk,
|
|
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
|
}
|
package/unify/web-bridge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1211
|
-
const
|
|
1212
|
-
|
|
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 {
|
|
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
|
|
1253
|
-
const
|
|
1254
|
-
|
|
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
|
|
1307
|
+
const { libDir } = configuredVpPaths();
|
|
1308
|
+
const vp = readVp(vpId, libDir ? { libDir } : {});
|
|
1286
1309
|
if (!vp) {
|
|
1287
1310
|
sendVpCrudResult({
|
|
1288
1311
|
op: 'read',
|