metame-cli 1.6.2 → 1.6.3
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/index.js +86 -6
- package/package.json +1 -1
- package/scripts/agent-intent-shared.js +11 -2
- package/scripts/core/session-source-db.js +125 -0
- package/scripts/daemon-agent-intent.js +51 -15
- package/scripts/daemon-agent-tools.js +52 -3
- package/scripts/daemon-agent-workflow.js +98 -0
- package/scripts/daemon-bridges.js +9 -2
- package/scripts/daemon-command-router.js +1 -1
- package/scripts/daemon-engine-runtime.js +16 -6
- package/scripts/daemon-user-acl.js +19 -1
- package/scripts/daemon-weixin-bridge.js +6 -2
- package/scripts/daemon.js +46 -3
- package/scripts/docs/hermes-memory-upgrade-converged.md +461 -0
- package/scripts/docs/hermes-memory-upgrade-plan.md +506 -0
- package/scripts/feishu-adapter.js +78 -2
- package/scripts/memory-extract.js +72 -4
- package/scripts/memory-wiki-schema.js +31 -0
- package/scripts/memory.js +8 -2
- package/skills/send-to-user/SKILL.md +76 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# MetaMe Hermes-Style Memory Upgrade Plan
|
|
2
|
+
|
|
3
|
+
Status: draft for expert review
|
|
4
|
+
Scope: MetaMe session memory, wiki recall, agent memory injection
|
|
5
|
+
Non-goal: replacing the daemon runtime or introducing a new agent server
|
|
6
|
+
|
|
7
|
+
## 1. Executive Summary
|
|
8
|
+
|
|
9
|
+
MetaMe already has most primitives required for a Hermes-style long-term memory system:
|
|
10
|
+
|
|
11
|
+
- `scripts/memory.js` owns `~/.metame/memory.db`, `memory_items`, FTS5, state, project/scope fields, and compatibility APIs.
|
|
12
|
+
- `scripts/memory-wiki-schema.js` owns `wiki_pages`, `wiki_topics`, `content_chunks`, and `embedding_queue`.
|
|
13
|
+
- `scripts/session-analytics.js` parses Claude Code JSONL transcripts into local skeleton/evidence.
|
|
14
|
+
- `scripts/memory-extract.js` extracts atomic facts from unanalyzed sessions.
|
|
15
|
+
- `scripts/wiki-reflect.js` builds topic wiki pages from accumulated facts.
|
|
16
|
+
- `scripts/agent-layer.js` injects per-agent `memory-snapshot.md` into engine prompts.
|
|
17
|
+
- `scripts/memory-search.js` already exposes cross-session hybrid recall.
|
|
18
|
+
|
|
19
|
+
The gap is not storage. The gap is an explicit lifecycle:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
raw session episode -> episode index -> session note/wiki page -> structured memory item
|
|
23
|
+
-> hybrid recall -> bounded prompt injection -> audit/supersession
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Hermes Agent validates the same architecture pattern: small curated memory is always injected; large session history is stored losslessly and searched only when needed. MetaMe should converge on that pattern without replacing its daemon, routing, or existing memory DB.
|
|
27
|
+
|
|
28
|
+
## 2. Current Architecture Findings
|
|
29
|
+
|
|
30
|
+
### 2.1 Existing Strengths
|
|
31
|
+
|
|
32
|
+
1. Unified memory DB exists.
|
|
33
|
+
`memory.js` initializes `~/.metame/memory.db`, enables WAL and foreign keys, and creates `memory_items` plus `memory_items_fts`.
|
|
34
|
+
|
|
35
|
+
2. Memory kinds already map well to Hermes layers.
|
|
36
|
+
Existing `memory_items.kind` covers `profile`, `convention`, `episode`, and `insight`.
|
|
37
|
+
|
|
38
|
+
3. Wiki schema already supports topic pages and hybrid retrieval.
|
|
39
|
+
`memory-wiki-schema.js` creates `wiki_pages`, `wiki_topics`, `wiki_pages_fts`, `content_chunks`, and `embedding_queue`.
|
|
40
|
+
|
|
41
|
+
4. Hybrid wiki search already exists.
|
|
42
|
+
`core/hybrid-search.js` combines FTS5 and vector chunk search with RRF fusion, falling back to FTS-only.
|
|
43
|
+
|
|
44
|
+
5. Session extraction is already isolated.
|
|
45
|
+
`session-analytics.js` performs local skeleton extraction, while `memory-extract.js` calls the model only for high-value atomic facts.
|
|
46
|
+
|
|
47
|
+
6. Agent snapshot injection already exists.
|
|
48
|
+
`agent-layer.js` reads per-agent `memory-snapshot.md` and injects it as `[Agent memory snapshot: ...]`.
|
|
49
|
+
|
|
50
|
+
### 2.2 Current Gaps
|
|
51
|
+
|
|
52
|
+
1. No first-class raw episode table.
|
|
53
|
+
Raw session text exists in external JSONL files and a daily markdown diary, but `memory.db` does not have a durable episode registry with transcript path, hash, engine, chat id, project key, status, and lineage.
|
|
54
|
+
|
|
55
|
+
2. `episode` memory items are summaries, not provenance.
|
|
56
|
+
`saveSession()` stores an `episode` as one `memory_items` row, but it does not preserve the raw transcript pointer, turn counts, tool counts, or source hash needed for rebuild/audit.
|
|
57
|
+
|
|
58
|
+
3. Session wiki pages are exported, not modeled as first-class episode artifacts.
|
|
59
|
+
`wiki-reflect.js` exports recent session summaries, but there is no explicit `session_notes` / `episode_notes` table connecting raw transcript -> generated session note -> extracted facts -> wiki pages.
|
|
60
|
+
|
|
61
|
+
4. Prompt injection is mostly static.
|
|
62
|
+
`agent-layer.js` refreshes `memory-snapshot.md` from recent sessions/facts, but there is no query-conditioned recall block for ambiguous/new tasks.
|
|
63
|
+
|
|
64
|
+
5. Candidate memory lifecycle is incomplete.
|
|
65
|
+
`memory-extract.js` saves facts as `candidate`; `searchFacts()` only searches active items. Promotion, deprecation, supersession, and source-level audit need to be made explicit.
|
|
66
|
+
|
|
67
|
+
6. Subagent memory write boundaries are not codified in the memory layer.
|
|
68
|
+
Dispatch has safety guards, but memory writes should also carry `writer_type`, `writer_agent`, and a default quarantine rule for delegated work.
|
|
69
|
+
|
|
70
|
+
7. Scope model is under-specified.
|
|
71
|
+
`project`, `scope`, `task_key`, `session_id`, and `agent_key` exist, but there is no single resolver that maps chat/thread/project/agent into a memory scope for both read and write.
|
|
72
|
+
|
|
73
|
+
## 3. Target Architecture
|
|
74
|
+
|
|
75
|
+
### 3.1 Memory Layers
|
|
76
|
+
|
|
77
|
+
MetaMe should use four explicit layers:
|
|
78
|
+
|
|
79
|
+
1. L0 Raw Episode
|
|
80
|
+
Lossless source of truth. Immutable transcript metadata plus pointer/hash to raw JSONL or Codex rollout DB. Never summarized in place.
|
|
81
|
+
|
|
82
|
+
2. L1 Session Note
|
|
83
|
+
Derived markdown/wiki note for each significant session. Contains task, decisions, changed files, validation, risks, next steps, and evidence links.
|
|
84
|
+
|
|
85
|
+
3. L2 Structured Memory
|
|
86
|
+
Atomic facts in `memory_items`: conventions, decisions, bug lessons, config facts, workflow rules, milestones, profile facts. Each item must reference L0/L1 provenance.
|
|
87
|
+
|
|
88
|
+
4. L3 Curated Injection
|
|
89
|
+
Small per-agent `memory-snapshot.md` plus query-conditioned recall block. This is the only layer routinely injected into prompts.
|
|
90
|
+
|
|
91
|
+
### 3.2 Data Flow
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Claude/Codex transcript
|
|
95
|
+
-> session episode indexer
|
|
96
|
+
-> episode row + checksum + metadata
|
|
97
|
+
-> session note builder
|
|
98
|
+
-> memory fact extractor
|
|
99
|
+
-> topic/wiki staleness update
|
|
100
|
+
-> wiki rebuild/export
|
|
101
|
+
-> recall router
|
|
102
|
+
-> prompt snapshot / query recall block
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 3.3 Retrieval Policy
|
|
106
|
+
|
|
107
|
+
MetaMe should follow Hermes' split:
|
|
108
|
+
|
|
109
|
+
- Always inject: small curated memory snapshot for the active agent/project.
|
|
110
|
+
- On demand: run recall when user asks "之前/记得/上次/不清楚/查一下历史/为什么这么定" or when intent router detects low-confidence context.
|
|
111
|
+
- Never inject blindly: raw transcript, large wiki pages, or all recent sessions.
|
|
112
|
+
|
|
113
|
+
## 4. Proposed Schema Extensions
|
|
114
|
+
|
|
115
|
+
All changes belong in `scripts/memory-wiki-schema.js` or a new `scripts/core/memory-schema.js` if we split schema ownership later.
|
|
116
|
+
|
|
117
|
+
### 4.1 `session_episodes`
|
|
118
|
+
|
|
119
|
+
Purpose: first-class registry for L0 raw episodes.
|
|
120
|
+
|
|
121
|
+
Columns:
|
|
122
|
+
|
|
123
|
+
- `id TEXT PRIMARY KEY`
|
|
124
|
+
- `engine TEXT NOT NULL CHECK (engine IN ('claude','codex','unknown'))`
|
|
125
|
+
- `session_id TEXT NOT NULL`
|
|
126
|
+
- `chat_id TEXT`
|
|
127
|
+
- `project TEXT DEFAULT '*'`
|
|
128
|
+
- `scope TEXT`
|
|
129
|
+
- `agent_key TEXT`
|
|
130
|
+
- `cwd TEXT`
|
|
131
|
+
- `transcript_path TEXT`
|
|
132
|
+
- `transcript_hash TEXT`
|
|
133
|
+
- `parent_episode_id TEXT`
|
|
134
|
+
- `status TEXT DEFAULT 'indexed' CHECK (status IN ('indexed','summarized','extracted','archived','error'))`
|
|
135
|
+
- `message_count INTEGER DEFAULT 0`
|
|
136
|
+
- `tool_call_count INTEGER DEFAULT 0`
|
|
137
|
+
- `tool_error_count INTEGER DEFAULT 0`
|
|
138
|
+
- `first_ts TEXT`
|
|
139
|
+
- `last_ts TEXT`
|
|
140
|
+
- `created_at TEXT DEFAULT (datetime('now'))`
|
|
141
|
+
- `updated_at TEXT DEFAULT (datetime('now'))`
|
|
142
|
+
|
|
143
|
+
Indexes:
|
|
144
|
+
|
|
145
|
+
- `(session_id)`
|
|
146
|
+
- `(project, scope, last_ts)`
|
|
147
|
+
- `(agent_key, last_ts)`
|
|
148
|
+
- `(transcript_hash)`
|
|
149
|
+
|
|
150
|
+
### 4.2 `session_notes`
|
|
151
|
+
|
|
152
|
+
Purpose: first-class L1 derived notes.
|
|
153
|
+
|
|
154
|
+
Columns:
|
|
155
|
+
|
|
156
|
+
- `id TEXT PRIMARY KEY`
|
|
157
|
+
- `episode_id TEXT NOT NULL`
|
|
158
|
+
- `slug TEXT UNIQUE NOT NULL`
|
|
159
|
+
- `title TEXT NOT NULL`
|
|
160
|
+
- `content TEXT NOT NULL`
|
|
161
|
+
- `summary TEXT`
|
|
162
|
+
- `tags TEXT DEFAULT '[]'`
|
|
163
|
+
- `evidence_refs TEXT DEFAULT '[]'`
|
|
164
|
+
- `note_hash TEXT`
|
|
165
|
+
- `status TEXT DEFAULT 'active' CHECK (status IN ('active','stale','archived','error'))`
|
|
166
|
+
- `created_at TEXT DEFAULT (datetime('now'))`
|
|
167
|
+
- `updated_at TEXT DEFAULT (datetime('now'))`
|
|
168
|
+
|
|
169
|
+
FTS:
|
|
170
|
+
|
|
171
|
+
- `session_notes_fts(title, content, tags)`
|
|
172
|
+
|
|
173
|
+
### 4.3 `memory_items` Additions
|
|
174
|
+
|
|
175
|
+
Add idempotent `ALTER TABLE` migrations:
|
|
176
|
+
|
|
177
|
+
- `valid_from TEXT`
|
|
178
|
+
- `valid_to TEXT`
|
|
179
|
+
- `source_episode_id TEXT`
|
|
180
|
+
- `source_note_id TEXT`
|
|
181
|
+
- `writer_type TEXT DEFAULT 'system'`
|
|
182
|
+
- `writer_agent TEXT`
|
|
183
|
+
- `review_state TEXT DEFAULT 'unreviewed' CHECK (review_state IN ('unreviewed','verified','rejected'))`
|
|
184
|
+
|
|
185
|
+
Rationale:
|
|
186
|
+
|
|
187
|
+
- `source_episode_id` and `source_note_id` make every memory traceable.
|
|
188
|
+
- `valid_from` / `valid_to` make temporal override explicit.
|
|
189
|
+
- `review_state` separates "active for recall" from "human-verified".
|
|
190
|
+
|
|
191
|
+
## 5. Component Plan
|
|
192
|
+
|
|
193
|
+
### Phase 0: Stabilize Existing Memory Contract
|
|
194
|
+
|
|
195
|
+
No behavior change. Add tests around current invariants before refactor.
|
|
196
|
+
|
|
197
|
+
Files:
|
|
198
|
+
|
|
199
|
+
- `scripts/memory.js`
|
|
200
|
+
- `scripts/memory-wiki-schema.js`
|
|
201
|
+
- `scripts/core/memory-model.js`
|
|
202
|
+
- `scripts/memory-wiki-integration.test.js`
|
|
203
|
+
|
|
204
|
+
Acceptance:
|
|
205
|
+
|
|
206
|
+
- Existing `memory_items` schema remains backward compatible.
|
|
207
|
+
- `saveFacts()`, `searchFacts()`, `searchSessions()`, `hybridSearchWiki()` signatures remain stable.
|
|
208
|
+
- No config or runtime copy under `~/.metame/` is edited directly.
|
|
209
|
+
|
|
210
|
+
### Phase 1: Episode Registry
|
|
211
|
+
|
|
212
|
+
Add `session_episodes` and an indexer module.
|
|
213
|
+
|
|
214
|
+
New module:
|
|
215
|
+
|
|
216
|
+
- `scripts/core/session-episode-db.js`
|
|
217
|
+
|
|
218
|
+
Exports:
|
|
219
|
+
|
|
220
|
+
- `upsertSessionEpisode(db, episode)`
|
|
221
|
+
- `getSessionEpisode(db, episodeId)`
|
|
222
|
+
- `findEpisodeBySessionId(db, sessionId)`
|
|
223
|
+
- `listRecentEpisodes(db, opts)`
|
|
224
|
+
- `markEpisodeStatus(db, episodeId, status, errorMessage?)`
|
|
225
|
+
|
|
226
|
+
Integration points:
|
|
227
|
+
|
|
228
|
+
- `memory.js` should apply the schema at DB init.
|
|
229
|
+
- `memory-extract.js` should call the new episode DB before fact extraction.
|
|
230
|
+
- `session-analytics.js` remains pure extraction and should not open memory DB directly.
|
|
231
|
+
|
|
232
|
+
Acceptance:
|
|
233
|
+
|
|
234
|
+
- Running extraction on a known transcript creates one `session_episodes` row.
|
|
235
|
+
- Re-running extraction is idempotent by `session_id + transcript_hash`.
|
|
236
|
+
- Raw transcript content is not copied into DB; only path/hash/metadata are stored.
|
|
237
|
+
|
|
238
|
+
### Phase 2: Session Note Builder
|
|
239
|
+
|
|
240
|
+
Add L1 session note generation separate from fact extraction.
|
|
241
|
+
|
|
242
|
+
New module:
|
|
243
|
+
|
|
244
|
+
- `scripts/session-note-build.js`
|
|
245
|
+
|
|
246
|
+
Responsibilities:
|
|
247
|
+
|
|
248
|
+
- Build a concise session note from skeleton + evidence.
|
|
249
|
+
- Store note in `session_notes`.
|
|
250
|
+
- Export note to `~/.metame/wiki/sessions/` through the existing wiki export path.
|
|
251
|
+
- Link note to episode via `episode_id`.
|
|
252
|
+
|
|
253
|
+
Suggested note structure:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
# <title>
|
|
257
|
+
|
|
258
|
+
## Task
|
|
259
|
+
## Decisions
|
|
260
|
+
## Changes
|
|
261
|
+
## Validation
|
|
262
|
+
## Risks
|
|
263
|
+
## Next Steps
|
|
264
|
+
## Evidence
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Acceptance:
|
|
268
|
+
|
|
269
|
+
- Every non-trivial episode can have exactly one active session note.
|
|
270
|
+
- Session note generation failure does not block fact extraction.
|
|
271
|
+
- Notes are rebuildable from L0 transcript.
|
|
272
|
+
|
|
273
|
+
### Phase 3: Provenance-Aware Fact Extraction
|
|
274
|
+
|
|
275
|
+
Update `memory-extract.js` and `memory.saveFacts()`.
|
|
276
|
+
|
|
277
|
+
Changes:
|
|
278
|
+
|
|
279
|
+
- Pass `source_episode_id` and `source_note_id` into `saveFacts()`.
|
|
280
|
+
- Store extracted facts as `candidate` and `review_state='unreviewed'`.
|
|
281
|
+
- Auto-promote only high-confidence low-risk facts after validation checks.
|
|
282
|
+
- Keep manual `memory-write.js` writes as `source_type='manual'`, `review_state='verified'`.
|
|
283
|
+
|
|
284
|
+
Acceptance:
|
|
285
|
+
|
|
286
|
+
- Every extracted fact has either `source_episode_id` or a clear legacy fallback.
|
|
287
|
+
- Manual facts are protected and searchable immediately.
|
|
288
|
+
- Delegated/subagent facts default to unreviewed unless promoted by parent/main agent.
|
|
289
|
+
|
|
290
|
+
### Phase 4: Recall Router
|
|
291
|
+
|
|
292
|
+
Add a pure router that decides when and what to recall.
|
|
293
|
+
|
|
294
|
+
New module:
|
|
295
|
+
|
|
296
|
+
- `scripts/core/recall-router.js`
|
|
297
|
+
|
|
298
|
+
Inputs:
|
|
299
|
+
|
|
300
|
+
- current user text
|
|
301
|
+
- current project/scope/agent
|
|
302
|
+
- active session metadata
|
|
303
|
+
- optional intent classification
|
|
304
|
+
|
|
305
|
+
Outputs:
|
|
306
|
+
|
|
307
|
+
- `shouldRecall`
|
|
308
|
+
- `queries[]`
|
|
309
|
+
- `modes[]`: `facts`, `episodes`, `wiki`, `session_notes`
|
|
310
|
+
- `budget`
|
|
311
|
+
- `reason`
|
|
312
|
+
|
|
313
|
+
Rules:
|
|
314
|
+
|
|
315
|
+
- Recall if the user asks about history: "之前", "上次", "记得", "为什么这么定", "有没有踩过坑".
|
|
316
|
+
- Recall if command/router confidence is low and project is known.
|
|
317
|
+
- Recall exact identifiers via FTS first: paths, function names, error codes, config keys.
|
|
318
|
+
- Recall semantic questions via wiki/session notes first.
|
|
319
|
+
- Do not recall on simple one-shot commands where history is unlikely to help.
|
|
320
|
+
|
|
321
|
+
Acceptance:
|
|
322
|
+
|
|
323
|
+
- Router is pure and unit-tested with Chinese and English triggers.
|
|
324
|
+
- Router never reads files or DB directly.
|
|
325
|
+
- Router returns no recall for obviously self-contained tasks.
|
|
326
|
+
|
|
327
|
+
### Phase 5: Prompt Injection Upgrade
|
|
328
|
+
|
|
329
|
+
Keep current per-agent snapshot, add query-conditioned recall block.
|
|
330
|
+
|
|
331
|
+
Files:
|
|
332
|
+
|
|
333
|
+
- `scripts/agent-layer.js`
|
|
334
|
+
- `scripts/daemon-claude-engine.js`
|
|
335
|
+
- `scripts/daemon-prompt-context.js`
|
|
336
|
+
|
|
337
|
+
Design:
|
|
338
|
+
|
|
339
|
+
- `memory-snapshot.md` stays small and stable.
|
|
340
|
+
- A new `[Relevant memory recall: ...]` block is added only when `recall-router` says recall is needed.
|
|
341
|
+
- Recall block must include source labels: fact id, session note slug, episode id, or wiki slug.
|
|
342
|
+
- Recall block should be capped by character budget and grouped by type.
|
|
343
|
+
|
|
344
|
+
Proposed API:
|
|
345
|
+
|
|
346
|
+
```js
|
|
347
|
+
memory.assembleRecallContext({
|
|
348
|
+
query,
|
|
349
|
+
scope: { project, scope, agent, session },
|
|
350
|
+
modes: ['facts', 'session_notes', 'wiki', 'episodes'],
|
|
351
|
+
budget: { totalChars: 4000 }
|
|
352
|
+
})
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Acceptance:
|
|
356
|
+
|
|
357
|
+
- Prompt cache remains stable for normal messages without recall.
|
|
358
|
+
- Recall block appears only when triggered.
|
|
359
|
+
- The block contains provenance and does not include raw transcript dumps.
|
|
360
|
+
|
|
361
|
+
### Phase 6: Wiki Integration
|
|
362
|
+
|
|
363
|
+
Extend wiki pipeline so session notes are a first-class source, not just exported afterthought.
|
|
364
|
+
|
|
365
|
+
Files:
|
|
366
|
+
|
|
367
|
+
- `scripts/wiki-reflect.js`
|
|
368
|
+
- `scripts/wiki-reflect-export.js`
|
|
369
|
+
- `scripts/core/wiki-db.js`
|
|
370
|
+
|
|
371
|
+
Changes:
|
|
372
|
+
|
|
373
|
+
- Add `listRecentSessionNotes()` alongside `listRecentSessionSummaries()`.
|
|
374
|
+
- Export `session_notes` with stable slugs.
|
|
375
|
+
- Allow topic wiki pages to reference session notes as `raw_source_ids`.
|
|
376
|
+
- Add backlinks from session note -> related wiki topics.
|
|
377
|
+
|
|
378
|
+
Acceptance:
|
|
379
|
+
|
|
380
|
+
- `~/.metame/wiki/sessions/_index.md` is generated from `session_notes`, not only `memory_items.kind='episode'`.
|
|
381
|
+
- Each wiki page can trace claims back to facts and session notes.
|
|
382
|
+
- Existing wiki export behavior stays backward compatible.
|
|
383
|
+
|
|
384
|
+
### Phase 7: Review, Promotion, and GC
|
|
385
|
+
|
|
386
|
+
Make memory lifecycle auditable.
|
|
387
|
+
|
|
388
|
+
Files:
|
|
389
|
+
|
|
390
|
+
- `scripts/memory-gc.js`
|
|
391
|
+
- `scripts/memory-nightly-reflect.js`
|
|
392
|
+
- `scripts/memory-search.js`
|
|
393
|
+
- optional new `scripts/memory-review.js`
|
|
394
|
+
|
|
395
|
+
Rules:
|
|
396
|
+
|
|
397
|
+
- Candidate facts can become active by manual approval, repeated successful recall, or nightly reflection.
|
|
398
|
+
- Facts with conflicts should set `valid_to` and `supersedes_id`, not be overwritten.
|
|
399
|
+
- Low-confidence unused candidate facts expire.
|
|
400
|
+
- Manual/protected facts are never archived automatically.
|
|
401
|
+
|
|
402
|
+
Acceptance:
|
|
403
|
+
|
|
404
|
+
- A rejected/candidate fact does not appear in normal prompt injection.
|
|
405
|
+
- Search CLI can optionally include `--candidates` for review.
|
|
406
|
+
- GC never deletes raw episode rows or session notes; it only archives derived memory.
|
|
407
|
+
|
|
408
|
+
## 6. Safety and Privacy Rules
|
|
409
|
+
|
|
410
|
+
1. Raw transcripts are sensitive.
|
|
411
|
+
Store paths and hashes in DB. Do not copy full transcript into `memory.db` unless explicitly needed later.
|
|
412
|
+
|
|
413
|
+
2. Credentials are never extracted.
|
|
414
|
+
Extend extraction prompts and filters to reject tokens, secrets, bot tokens, app secrets, chat ids unless they are placeholder values.
|
|
415
|
+
|
|
416
|
+
3. Agent scope is mandatory.
|
|
417
|
+
Reads and writes must include project/scope/agent when known. Global memory should be rare and explicit.
|
|
418
|
+
|
|
419
|
+
4. Personal assistant boundary remains hard.
|
|
420
|
+
No automatic dispatch or memory sharing into `personal`. If future memory sharing exists, personal scope must be opt-in.
|
|
421
|
+
|
|
422
|
+
5. Review state matters.
|
|
423
|
+
Automatically extracted memory is not equivalent to verified memory.
|
|
424
|
+
|
|
425
|
+
## 7. Test Matrix
|
|
426
|
+
|
|
427
|
+
Required unit tests:
|
|
428
|
+
|
|
429
|
+
- `scripts/core/session-episode-db.test.js`
|
|
430
|
+
- `scripts/session-note-build.test.js`
|
|
431
|
+
- `scripts/core/recall-router.test.js`
|
|
432
|
+
- Extend `scripts/memory-wiki-schema.test.js`
|
|
433
|
+
- Extend `scripts/memory-wiki-integration.test.js`
|
|
434
|
+
- Extend `scripts/memory-search.js` coverage if currently absent
|
|
435
|
+
|
|
436
|
+
Required integration tests:
|
|
437
|
+
|
|
438
|
+
- `node --test scripts/memory-wiki-integration.test.js`
|
|
439
|
+
- `node --test scripts/memory-extract-step4.test.js`
|
|
440
|
+
- `node --test scripts/daemon-session-store.test.js`
|
|
441
|
+
- `node --test scripts/daemon-prompt-context.test.js`
|
|
442
|
+
- `node --test scripts/daemon-claude-engine.test.js`
|
|
443
|
+
|
|
444
|
+
Daemon edit rule:
|
|
445
|
+
|
|
446
|
+
- If any `scripts/daemon*.js` file changes, run:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
npx eslint scripts/daemon*.js
|
|
450
|
+
node --test scripts/daemon-*.test.js
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## 8. Rollout Plan
|
|
454
|
+
|
|
455
|
+
1. Ship schema and episode registry first.
|
|
456
|
+
This is low-risk and backward compatible.
|
|
457
|
+
|
|
458
|
+
2. Backfill historical sessions.
|
|
459
|
+
Add a dry-run mode before writing rows. Use small batch size and lock files.
|
|
460
|
+
|
|
461
|
+
3. Enable session note generation for new sessions only.
|
|
462
|
+
Do not backfill all history with LLM immediately.
|
|
463
|
+
|
|
464
|
+
4. Add recall router in observe-only mode.
|
|
465
|
+
Log `shouldRecall`, queries, and candidate hits without injecting into prompts.
|
|
466
|
+
|
|
467
|
+
5. Enable recall injection behind config.
|
|
468
|
+
Suggested config key under `daemon`:
|
|
469
|
+
|
|
470
|
+
```yaml
|
|
471
|
+
daemon:
|
|
472
|
+
memory_recall:
|
|
473
|
+
enabled: false
|
|
474
|
+
max_chars: 4000
|
|
475
|
+
modes: [facts, session_notes, wiki]
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Do not edit `daemon-default.yaml` for real user config. Runtime config remains `~/.metame/daemon.yaml`; template changes are only placeholders.
|
|
479
|
+
|
|
480
|
+
6. Turn on by project/agent.
|
|
481
|
+
Global enablement should wait until recall precision is measured.
|
|
482
|
+
|
|
483
|
+
## 9. Expert Review Checklist
|
|
484
|
+
|
|
485
|
+
Ask reviewers to challenge these points first:
|
|
486
|
+
|
|
487
|
+
1. Is `session_episodes` enough provenance, or must raw transcript chunks be indexed directly?
|
|
488
|
+
2. Should `session_notes` live in `memory.db`, filesystem markdown, or both?
|
|
489
|
+
3. What is the exact promotion policy from `candidate` to `active`?
|
|
490
|
+
4. Should recall be router-triggered only, or also model-tool-triggered?
|
|
491
|
+
5. Should `memory-snapshot.md` remain a file, or become generated at session start?
|
|
492
|
+
6. How strict should project/scope isolation be for global facts?
|
|
493
|
+
7. Is `valid_from` / `valid_to` sufficient for temporal facts, or do we need a graph later?
|
|
494
|
+
8. What metrics define recall quality: hit rate, false-positive rate, saved tokens, user correction rate?
|
|
495
|
+
|
|
496
|
+
## 10. Recommended First PR
|
|
497
|
+
|
|
498
|
+
Keep the first PR deliberately boring:
|
|
499
|
+
|
|
500
|
+
1. Add schema for `session_episodes` and `session_notes`.
|
|
501
|
+
2. Add `core/session-episode-db.js`.
|
|
502
|
+
3. Add unit tests for idempotent upsert, transcript hash dedup, and recent listing.
|
|
503
|
+
4. Add no prompt injection and no LLM calls.
|
|
504
|
+
|
|
505
|
+
This creates the foundation experts can inspect without debating model behavior.
|
|
506
|
+
|
|
@@ -61,10 +61,10 @@ function withTimeout(promise, ms = 10000) {
|
|
|
61
61
|
|
|
62
62
|
// Wait for DNS to resolve a target host with exponential backoff.
|
|
63
63
|
// Used after system wake / before reconnect: the OS may report clock/events
|
|
64
|
-
// restored before WiFi+DNS are actually usable. Retries 1/2/4/8s, total cap
|
|
64
|
+
// restored before WiFi+DNS are actually usable. Retries 1/2/4/8s, total cap 60s.
|
|
65
65
|
async function waitForNetworkReady(hostname, opts = {}) {
|
|
66
66
|
const log = opts.log || (() => {});
|
|
67
|
-
const totalBudget = Number.isFinite(opts.totalBudgetMs) ? opts.totalBudgetMs :
|
|
67
|
+
const totalBudget = Number.isFinite(opts.totalBudgetMs) ? opts.totalBudgetMs : 60000;
|
|
68
68
|
const lookup = opts.lookup || dns.promises.lookup;
|
|
69
69
|
const sleep = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
70
70
|
const startedAt = Date.now();
|
|
@@ -429,6 +429,82 @@ function createBot(config) {
|
|
|
429
429
|
}
|
|
430
430
|
},
|
|
431
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Create a new Feishu group chat. The bot is automatically a member of
|
|
434
|
+
* any chat it creates; pass `inviteOpenIds` to add humans at creation time.
|
|
435
|
+
* Requires the app to have `im:chat` (and `im:chat.member` for invitees)
|
|
436
|
+
* permission. Returns { ok, chatId, error }; never throws — callers can
|
|
437
|
+
* fall back to the manual /activate flow on failure.
|
|
438
|
+
*
|
|
439
|
+
* @param {object} opts
|
|
440
|
+
* @param {string} opts.name Chat name shown in user's chat list.
|
|
441
|
+
* @param {string} [opts.description] Optional description.
|
|
442
|
+
* @param {string[]} [opts.inviteOpenIds] open_ids of humans to add now.
|
|
443
|
+
* @param {string} [opts.ownerOpenId] open_id to mark as chat owner.
|
|
444
|
+
*/
|
|
445
|
+
async createChat({ name, description = '', inviteOpenIds = [], ownerOpenId = null }) {
|
|
446
|
+
if (!name) return { ok: false, error: 'name is required' };
|
|
447
|
+
try {
|
|
448
|
+
const data = {
|
|
449
|
+
name: String(name).slice(0, 60),
|
|
450
|
+
description: String(description).slice(0, 256),
|
|
451
|
+
chat_mode: 'group',
|
|
452
|
+
chat_type: 'private',
|
|
453
|
+
// Owner is required by the API; default to the inviter if not given.
|
|
454
|
+
...(ownerOpenId ? { owner_id: ownerOpenId } : {}),
|
|
455
|
+
...(inviteOpenIds.length > 0 ? { user_id_list: inviteOpenIds.slice(0, 50) } : {}),
|
|
456
|
+
};
|
|
457
|
+
const res = await withTimeout(
|
|
458
|
+
client.im.chat.create({ params: { user_id_type: 'open_id' }, data }),
|
|
459
|
+
15000
|
|
460
|
+
);
|
|
461
|
+
const chatId = res?.data?.chat_id || null;
|
|
462
|
+
if (!chatId) {
|
|
463
|
+
return { ok: false, error: `chat.create returned no chat_id: ${JSON.stringify(res?.data || res)}` };
|
|
464
|
+
}
|
|
465
|
+
return { ok: true, chatId };
|
|
466
|
+
} catch (err) {
|
|
467
|
+
const errDetail = err?.response?.data || err;
|
|
468
|
+
const code = errDetail?.code;
|
|
469
|
+
const msg = errDetail?.msg || errDetail?.message || String(err);
|
|
470
|
+
// Permission denied is the common first-time failure — surface a hint.
|
|
471
|
+
if (code === 99991663 || /permission|forbidden|scope/i.test(msg)) {
|
|
472
|
+
return { ok: false, error: `飞书应用缺少 im:chat 权限(${msg})`, code };
|
|
473
|
+
}
|
|
474
|
+
return { ok: false, error: msg, code };
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Invite humans to an existing chat by open_id. Returns invalid_id_list
|
|
480
|
+
* so the caller can decide whether to surface the partial-failure case.
|
|
481
|
+
*/
|
|
482
|
+
async inviteToChat(chatId, openIds = []) {
|
|
483
|
+
if (!chatId) return { ok: false, error: 'chatId is required' };
|
|
484
|
+
const list = (Array.isArray(openIds) ? openIds : [openIds]).filter(Boolean).slice(0, 50);
|
|
485
|
+
if (list.length === 0) return { ok: true, invalid: [] };
|
|
486
|
+
try {
|
|
487
|
+
const res = await withTimeout(
|
|
488
|
+
client.im.chat.members.create({
|
|
489
|
+
path: { chat_id: chatId },
|
|
490
|
+
params: { member_id_type: 'open_id' },
|
|
491
|
+
data: { id_list: list },
|
|
492
|
+
}),
|
|
493
|
+
15000
|
|
494
|
+
);
|
|
495
|
+
const invalid = res?.data?.invalid_id_list || [];
|
|
496
|
+
return { ok: true, invalid };
|
|
497
|
+
} catch (err) {
|
|
498
|
+
const errDetail = err?.response?.data || err;
|
|
499
|
+
const code = errDetail?.code;
|
|
500
|
+
const msg = errDetail?.msg || errDetail?.message || String(err);
|
|
501
|
+
if (code === 99991663 || /permission|forbidden|scope/i.test(msg)) {
|
|
502
|
+
return { ok: false, error: `飞书应用缺少 im:chat.member 权限(${msg})`, code };
|
|
503
|
+
}
|
|
504
|
+
return { ok: false, error: msg, code };
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
|
|
432
508
|
/**
|
|
433
509
|
* Start WebSocket long connection to receive messages (with auto-reconnect)
|
|
434
510
|
* @param {function} onMessage - callback(chatId, text, event)
|