dsh-engram 0.2.0 → 0.3.2
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/README.md +115 -5
- package/README.zh.md +60 -4
- package/lib/api.js +91 -10
- package/lib/capture.js +11 -7
- package/lib/client.js +4590 -1057
- package/lib/index-block.js +163 -27
- package/lib/index.js +43 -1
- package/lib/mental.js +170 -0
- package/lib/obs.js +71 -0
- package/lib/rerank.js +50 -0
- package/lib/settings.js +3 -0
- package/lib/store.js +330 -32
- package/lib/tools.js +349 -62
- package/lib/trigger.js +379 -0
- package/lib/usage.js +224 -60
- package/lib/util.js +107 -0
- package/package.json +3 -7
package/README.md
CHANGED
|
@@ -15,11 +15,48 @@ with one goal: **save tokens**.
|
|
|
15
15
|
budget 700 chars ≈ 175 tokens; one line per memory) is injected at prompt
|
|
16
16
|
assembly and **frozen per session**, keeping the request prefix byte-stable for
|
|
17
17
|
KV-cache reuse. The agent drills down with `engram_recall` / `engram_detail` instead
|
|
18
|
-
of dumping raw hits into context
|
|
18
|
+
of dumping raw hits into context; recall ranks the in-domain pool with an
|
|
19
|
+
in-process BM25 pass (TF·IDF with label/phrase boosts plus a gentle time-decay
|
|
20
|
+
factor — deterministic, zero deps), appends a compact entity-neighborhood
|
|
21
|
+
table (reusing esr_link: `node --rel--> node · conf%`) for entity-anchored
|
|
22
|
+
hits, re-warms the old `error` entry when a new failure is near-identical
|
|
23
|
+
(repeat-failure revival: recency + hit climb toward promoteHits so the
|
|
24
|
+
failure resurfaces instead of piling up), and on zero local hits falls back
|
|
25
|
+
to DSH's own cross-session full-text index (`ctx.sessionQuery`, filtered by
|
|
26
|
+
cwd) instead of building a parallel SQLite index.
|
|
19
27
|
- **ESR-lite closure protocol** — `esr_task` / `esr_close` / `esr_link` give tasks
|
|
20
28
|
a `draft → active → stable` lifecycle where `stable` requires real evidence
|
|
21
29
|
(`artifact` / `evaluation` / `memory_ref`), surfacing closure gaps instead of
|
|
22
|
-
letting the agent declare victory without proof.
|
|
30
|
+
letting the agent declare victory without proof. With `verifyArtifact` on
|
|
31
|
+
(default), a non-URL artifact is resolved against the workspace (= the session
|
|
32
|
+
cwd) and must exist on disk — otherwise the task stays ACTIVE with the reason;
|
|
33
|
+
`force:true` (or disabling the toggle) skips the disk check, the three gates
|
|
34
|
+
are still mandatory. The tool and the web forms share one gate
|
|
35
|
+
(`store.evidenceGate`), so the two surfaces can never drift.
|
|
36
|
+
Tasks are never isolated graph nodes: `esr_task(entity=…)` one-call wiring
|
|
37
|
+
auto-creates the `ent_<slug>` node and hangs the task on it
|
|
38
|
+
(`task --relates_to--> entity`, idempotent), so a task created with a domain
|
|
39
|
+
anchor immediately shows a relation; and `esr_dep` writes the dependency edge
|
|
40
|
+
into both the task's `deps` (blocker logic) and the shared links table, making
|
|
41
|
+
every task dependency a first-class visible edge in the graph.
|
|
42
|
+
- **Decision-point triggers (P0-A/B + P2 + P3)** — firing where the todo→esr
|
|
43
|
+
routing decision actually happens and where tasks leak: a `tools/result`
|
|
44
|
+
watcher snapshots the native `todo_write` plan and, when the session has ≥ 2
|
|
45
|
+
pending todos that outnumber its active ESR tasks, the [ESR] block appends a
|
|
46
|
+
concrete `promote:` hint (`esr_task(name="…")`, once per session); recurring
|
|
47
|
+
failures (error memories with hits ≥ 2) earn a `root-cause:` task candidate;
|
|
48
|
+
a completed plan (pending → 0) earns a `close:` nudge for evidence-complete
|
|
49
|
+
tasks; long-unupdated active tasks get a `stale:` nudge; and the task list is
|
|
50
|
+
ordered READY-first with a `next: esr_close <id>` line, so the model can act
|
|
51
|
+
top-down. The same subscription feeds an in-memory mem-vs-esr counter that
|
|
52
|
+
restores `escalationHint` (the per-day `usage` table stopped being written
|
|
53
|
+
after assessment P3, silently orphaning the old nudge). Pure rules, zero
|
|
54
|
+
LLM, zero per-render I/O. Every hint line carries a stable `#suggest-*`
|
|
55
|
+
source tag (traceable in logs) and the same recorder measures hint → esr_*
|
|
56
|
+
conversion (shown once per session per hint, attributed when an esr tool
|
|
57
|
+
runs within a 10-minute window) — `GET /api/dsh-engram/triggerstats`
|
|
58
|
+
reports per-kind shown/converted/rate so you can see which nudge actually
|
|
59
|
+
moves behaviour.
|
|
23
60
|
- **Memory GC (pi-esr constraints)** — a scheduled, mechanical, archive-only
|
|
24
61
|
sweep: TTL-expired memories are archived, over-cap workspaces evict the
|
|
25
62
|
lowest-value entries, stable tasks past their retention window leave the
|
|
@@ -98,6 +135,41 @@ scripts/setup-links.mjs --check` prints the state without writing anything.
|
|
|
98
135
|
|
|
99
136
|
After restart, inside the **native** DSH settings surface:
|
|
100
137
|
|
|
138
|
+
- **Sidebar "ESR 看板" entry + full-screen kanban** — one more row under
|
|
139
|
+
New Session with a **live active-task badge** (polled from /overview every
|
|
140
|
+
30s across all workspaces). Clicking it opens a full-screen board in the
|
|
141
|
+
center column: **草稿 / 进行中(gaps) / 就绪(evidence ready) / 已闭环** columns,
|
|
142
|
+
workspace filter + search, an inline create form, and per-card
|
|
143
|
+
"补齐证据 → 关闭" closure forms sharing the esr_close gates (artifact +
|
|
144
|
+
evaluation + memory_refs). A **看板 / 图谱 (board/graph)** toggle in the
|
|
145
|
+
header reuses the full relation graph (esr_node/esr_link force-directed SVG:
|
|
146
|
+
entity circles + task check badges, drag/zoom/select for relation details),
|
|
147
|
+
following the workspace filter and refreshed by the same 20s polling.
|
|
148
|
+
Following the task-board precedent, the entry and
|
|
149
|
+
the board are DOM-mounted and self-heal (MutationObserver re-inserts on shell
|
|
150
|
+
re-renders), with cross-panel exclusivity against task-board/ssh (opening one
|
|
151
|
+
evicts the others; clicking a sidebar session/workspace row hands the center
|
|
152
|
+
column back to the conversation). The conversation subtree stays mounted
|
|
153
|
+
underneath and is hidden by `html[data-dsh-engram-board-active]`, so toggling loses
|
|
154
|
+
no state.
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
- **Unified task strip above the composer** — the conversation dock that DSH
|
|
158
|
+
ships for its built-in todo tool is taken over (same `conversation.input.dock`
|
|
159
|
+
cell / `id: todo` at a lower priority) and **merged** into one modern control:
|
|
160
|
+
the session's current plan (`todo_write`'s `todos` projection) plus the
|
|
161
|
+
workspace's persistent **ESR tasks** (with evidence-gap badges and an inline
|
|
162
|
+
"补齐证据 → close" form) plus the **relation graph** rendered as
|
|
163
|
+
node → relation → node chips with entity/task names resolved. It only shows
|
|
164
|
+
while there is something to show, stays live with 15s polling, and the built-in
|
|
165
|
+
plan still renders (without the ESR parts) if the loopback-fenced API is
|
|
166
|
+
unreachable. A **workspace-switcher chip** leads the strip: it defaults to
|
|
167
|
+
following the current session (the tooltip says so), and its dropdown pins the
|
|
168
|
+
ESR task/relation source to any workspace (✓ marks the active pin; the × or
|
|
169
|
+
"follow session" entry reverts). Switching refetches immediately and is a pure
|
|
170
|
+
UI focus change — the model's session context and the per-session frozen
|
|
171
|
+
injection blocks are untouched.
|
|
172
|
+
|
|
101
173
|
- **Settings → Engram Memory** — a standalone first-class settings section
|
|
102
174
|
(right after the Plugins section, not a child tab of it). Default
|
|
103
175
|
"All workspaces" view shows every workspace's memories/tasks/links
|
|
@@ -111,7 +183,41 @@ After restart, inside the **native** DSH settings surface:
|
|
|
111
183
|
with an inline "new task" form and a per-task "fill evidence to close"
|
|
112
184
|
(artifact / evaluation / memory_ref → STABLE, same gates as esr_close),
|
|
113
185
|
a node + relation list (nodes are domain objects the model registers via
|
|
114
|
-
esr_node — package/service/repo/concept; relations via esr_link),
|
|
186
|
+
esr_node — package/service/repo/concept; relations via esr_link), a separate
|
|
187
|
+
**relation-graph** tab (hand-rolled force-directed SVG, no chart library so
|
|
188
|
+
the bundle stays pure: entities as circles, tasks as check badges, relations
|
|
189
|
+
colored per type with direction arrows; drag nodes, pan, wheel-zoom, re-layout,
|
|
190
|
+
hover highlights the neighborhood, clicking a node pops a floating panel with
|
|
191
|
+
its incident relations and linked objects; dangling links are counted and
|
|
192
|
+
warned about), an **injection-preview** tab that renders the exact
|
|
193
|
+
`[ENGRAM]` index block (order 40) and `[ESR]` task/closure block (order 41)
|
|
194
|
+
the model sees each session — same pure functions as the system-prompt
|
|
195
|
+
sections — as two terminal-style panes with per-line coloring (block
|
|
196
|
+
headers, task lines, drill hint, and the data-driven `escalate:` reminder
|
|
197
|
+
highlighted), line/char/~token cost chips plus memory/task/link/node count
|
|
198
|
+
chips, 20s auto-refresh and one-click copy of the raw block text (backed by
|
|
199
|
+
the new `GET /api/dsh-engram/preview?workspace=…` route). Every ESR task
|
|
200
|
+
card (board and ESR tab) carries an **evidence-progress ring** — a small
|
|
201
|
+
three-arc SVG donut mapping artifact · evaluation · memory_ref, all green
|
|
202
|
+
when closure-ready, amber while gapped, gray with no evidence yet; the
|
|
203
|
+
board header adds an **aggregate ring** showing overall evidence
|
|
204
|
+
completeness (%) plus how many in-progress tasks are closure-ready. Pure
|
|
205
|
+
SVG, no chart library, bundle stays clean. A **telemetry dashboard** tab
|
|
206
|
+
turns the /stats usage rollup (workspace × day) into a pure-SVG dashboard:
|
|
207
|
+
three gauges for **ESR proactivity** (benchmarked against the 0.34 escalate
|
|
208
|
+
threshold, amber + hint when low), **recall hit rate** and **detail
|
|
209
|
+
follow-through**, five stat cards (total / esr / memory calls, avg hits per
|
|
210
|
+
query, failures), a 14-day mem-vs-esr stacked bar chart and a Top-8 tool
|
|
211
|
+
breakdown (mem blue / esr purple), 20s auto-refresh with an automatic
|
|
212
|
+
small-sample (<10 calls) warning. A **details sidebar** (master–detail)
|
|
213
|
+
opens on the right when you click any task / memory / node / relation
|
|
214
|
+
row: task cards show state badge, evidence ring, full id + timeline,
|
|
215
|
+
gap list, clickable memory references and an inline "fill evidence to
|
|
216
|
+
close" form that refreshes the lists on success; memory cards show full
|
|
217
|
+
text, tags, signal/hits/TTL and provenance metadata; node cards list all
|
|
218
|
+
incident relations (typed, colored, with direction + confidence); clicking
|
|
219
|
+
a task's memory reference jumps straight to that memory, with a hint when
|
|
220
|
+
it is not in the loaded set. And a
|
|
115
221
|
memory-GC panel (dry-run toggle + run button + pointer report). The GUI
|
|
116
222
|
create/close use the host's new `POST /api/dsh-engram/tasks` and
|
|
117
223
|
`POST /api/dsh-engram/tasks/close` routes. Model-side proactivity is driven
|
|
@@ -228,8 +334,12 @@ drill: engram_store (user asks to remember) | engram_recall <query> | engram_det
|
|
|
228
334
|
```
|
|
229
335
|
|
|
230
336
|
Prefixes: `[D]` decision · `[E]` error · `[P]` procedure · `[F]` fact ·
|
|
231
|
-
`[I]` insight · `[H]` handoff · `[T]` task.
|
|
232
|
-
|
|
337
|
+
`[I]` insight · `[H]` handoff · `[T]` task. A procedure re-used enough times
|
|
338
|
+
(`hits >= promoteHits`) becomes a "proven experience" — its prefix upgrades to
|
|
339
|
+
`[P✓]` and it is stably ranked first in the block (newest-first within each
|
|
340
|
+
group, keeping the block deterministic): our zero-LLM counterpart to TencentDB
|
|
341
|
+
Agent Memory's "Skill = validated, executable experience". Membership follows
|
|
342
|
+
the *Auto-capture policy* (signal threshold / recall promotion / git-echo guard),
|
|
233
343
|
bounded by the configured line and character budgets. `#` ids address the full
|
|
234
344
|
records via `engram_detail`. When a workspace has no tasks, `[ESR]` still renders
|
|
235
345
|
one line naming `esr_task`/`esr_close` so the mechanism stays visible to the
|
package/README.zh.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-engram
|
|
2
2
|
|
|
3
|
-
> **[中文](README.zh.md) · [English](README.md)**
|
|
3
|
+
> **[中文](README.zh.md) · [English](README.md) · [已交付 UI 控件清单](FEATURES.zh.md)**
|
|
4
4
|
|
|
5
5
|
为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 设计的极简长期记忆插件,融合了
|
|
6
6
|
[symbolic-index](https://github.com/skepsun/symbolic-index) 与 [pi-esr](https://github.com/skepsun/pi-esr) 的思想——
|
|
@@ -11,10 +11,32 @@
|
|
|
11
11
|
——`git push` / `git stash` / 无提交信息的 commit——刻意**从不记录**(见下方「自动捕获策略」)。
|
|
12
12
|
- **符号索引 + 渐进披露** — 一个紧凑的 `[ENGRAM]` 块(默认预算 700 字符 ≈ 175 token;每条记忆一行)在
|
|
13
13
|
组装提示词时注入,并**按会话冻结**,让请求前缀字节稳定以复用 KV 缓存。模型需要细节时用
|
|
14
|
-
`engram_recall` / `engram_detail`
|
|
14
|
+
`engram_recall` / `engram_detail` 下钻,而不是把命中的原文灌进上下文。召回对内存池做
|
|
15
|
+
**进程内 BM25 排序**(TF·IDF + 标签/短语加权 + **时间衰减因子**,确定性、零依赖);
|
|
16
|
+
命中实体锚定的记忆时附带**实体邻域关系简表**(复用 esr_link:`node --rel--> node · conf%`);
|
|
17
|
+
与历史失败**高度同源**的新错误会**唤醒旧 error 记忆**(刷新 recency + 命中,向 promoteHits
|
|
18
|
+
爬升直至重回索引,失败不重复堆积);本地零命中时自动兜底到 **DSH 自带的跨会话全文索引**
|
|
19
|
+
(`ctx.sessionQuery`,按 cwd 过滤)——不另建 SQLite,完全复用宿主。
|
|
15
20
|
- **ESR-lite 证据闭环** — `esr_task` / `esr_close` / `esr_link` 给任务一个 `draft → active → stable`
|
|
16
21
|
生命周期,其中 `stable` 必须要有真实证据(`artifact` / `evaluation` / `memory_ref`),把"缺什么"
|
|
17
|
-
摊在明面上,而不是让 agent
|
|
22
|
+
摊在明面上,而不是让 agent 没有证据就宣布完成。可开 `verifyArtifact`(默认开):非 URL 的
|
|
23
|
+
artifact 按工作区(= 会话 cwd)解析并在磁盘上实存校验,路径不存在则任务保持 ACTIVE、给出原因;
|
|
24
|
+
`force:true`(或关掉该开关)可跳过磁盘校验——三种证据门照样必填。工具与网页表单共享同一个
|
|
25
|
+
证据门(`store.evidenceGate`),口径绝不会漂移。
|
|
26
|
+
任务不再是孤立节点:`esr_task(entity=…)` 一键挂点——自动建 `ent_<slug>` 节点并挂
|
|
27
|
+
`task --relates_to--> entity` 边(幂等),挂着领域锚点的任务立刻带关系;
|
|
28
|
+
`esr_dep` 双写——依赖边同时进 `tasks[].deps`(blocker 逻辑)与 links 表,图谱里每条
|
|
29
|
+
任务依赖都是一等可见的边。
|
|
30
|
+
- **决策点触发(P0-A/B + P2 + P3)** — 在漏斗最窄处(todo 多、esr 少)与任务泄漏处(建了不关)
|
|
31
|
+
给模型侧触发:监听 `tools/result` 里的原生 `todo_write`,当会话 pending todo ≥ 2 且多于 active ESR
|
|
32
|
+
任务时,`[ESR]` 块追加带具体候选的 `promote:` 提示(`esr_task(name="…")`,每会话仅一次);
|
|
33
|
+
复发失败(error hits≥2)给 `root-cause:` 任务候选;收工(todo 全部完成)给 `close:` 提示关证据齐的
|
|
34
|
+
任务;长期无更新的 active 任务给 `stale:` 提示;活动任务按 READY 优先排序并给 `next: esr_close`。
|
|
35
|
+
并用同一订阅维护进程内 mem-vs-esr 实时计数修复 `escalationHint` 的死数据源(评估 P3 移除了
|
|
36
|
+
per-call usage 写路径后原 nudge 已静默失效)。纯规则、零 LLM、零每轮 I/O;每条 nudge 带稳定 `#suggest-*` 来源标记,
|
|
37
|
+
且 recorder 计量「提示曝光 → esr_* 调用」转化(每会话每 kind 记一次曝光,
|
|
38
|
+
esr 工具 10 分钟窗口内调用即归因),`GET /api/dsh-engram/triggerstats`
|
|
39
|
+
返回各 kind 的 shown/converted/rate。
|
|
18
40
|
- **记忆 GC(pi-esr 约束)** — 定时、机械、只归档的回收:TTL 过期记忆归档、超容量工作区淘汰低价值条目、
|
|
19
41
|
stable 任务超保留窗离开 `[ESR]` 表面、悬空链接边清理。工作集(active 任务引用 / 任务记忆 / 已入索引命中)
|
|
20
42
|
永不触碰;**不硬删任何东西**——归档条目保留 id、始终可重取。
|
|
@@ -80,6 +102,18 @@ node scripts/setup-links.mjs # 把 @deepseek-ai 工作区包软链进 node_m
|
|
|
80
102
|
|
|
81
103
|
重启后,全部落在 **DSH 原生**设置界面里:
|
|
82
104
|
|
|
105
|
+
- **侧边栏「ESR 看板」入口 + 全屏看板** — 侧栏(New Session 下方)新增一行 `ESR 看板` 入口,右侧带**实时活动任务数徽标**(30s 轮询 /overview 汇总各工作区 active 任务)。点击在中间列打开全屏看板:**草稿 / 进行中(证据缺口) / 就绪(证据齐) / 已闭环** 四列 + 工作区筛选 + 搜索 + 内联新建表单 + 每张卡片的「补齐证据 → 关闭」表单(与 esr_close 同一证据门:artifact + evaluation + memory_refs)。头部带「**看板 / 图谱**」切换:图谱视图复用完整的关系图谱(esr_node/esr_link 力导向图,实体圆节点 + 任务勾选徽标,支持拖拽/缩放/点选查看关系明细),跟随工作区筛选,20s 轮询保持实时。入口与看板按 task-board 的 DOM 级挂载惯例自愈(MutationObserver 重插/重挂),并与 task-board / ssh 面板做跨面板互斥(打开本面板会关掉对方,点侧栏会话/项目行自动回到对话)。对话子树始终挂载在下方、由 `html[data-dsh-engram-board-active]` 控制显隐,切换零状态丢失。
|
|
106
|
+
|
|
107
|
+
- **输入框上方的「任务」统一条** — 接管 DSH 内建 todo 工具的自己同款 dock 槽位
|
|
108
|
+
(同一个 `conversation.input.dock` 单元格 / `id: todo`、更低 `priority`,从而遮蔽内建
|
|
109
|
+
TodoPanel),把两套任务平面**合并成一个**现代化控件:会话当前计划(`todo_write` 的
|
|
110
|
+
`todos` 投影)+ 工作区持久 **ESR 任务**(证据缺口徽标 + 内联「补齐证据 → 关闭」表单)
|
|
111
|
+
+ **关系图**(以 节点 → 关系 → 节点 芯片呈现,实体/任务名自动解析)。有内容才显示,
|
|
112
|
+
15s 轮询保持实时;若 loopback 围栏的 API 不可达,内建计划仍照常渲染(只是不显示 ESR 部分)。
|
|
113
|
+
条首有**工作区切换 chip**:默认跟随当前会话(标题注明),下拉可把 ESR 任务/关系的来源**固定到任意
|
|
114
|
+
工作区**(打 ✓ 标记),再点 × 或「跟随会话」即恢复;切换即时重取,内置 todo 仍属本会话——纯 UI 焦点
|
|
115
|
+
切换,不动模型会话上下文(注入块按会话冻结,前缀稳定)。
|
|
116
|
+
|
|
83
117
|
- **设置 → Engram 记忆** — 独立的一级设置页签(位于「插件」之后),不再是「插件」页里的子 tab;默认
|
|
84
118
|
「全部工作区」视图完整展示所有工作区的记忆/任务/关系(按工作区分组,工作区下拉 + 上一/下一工作区
|
|
85
119
|
翻页;记忆表格另行 10 条/页分页 + 跳页下拉,仅「类型 / 内容 / 操作」三列——正文列占满,时间、
|
|
@@ -88,7 +122,26 @@ node scripts/setup-links.mjs # 把 @deepseek-ai 工作区包软链进 node_m
|
|
|
88
122
|
token 估算、GC 累计统计)、可搜索/可过滤的记忆表格(含归档与删除操作)、ESR 任务看板(「新建任务」
|
|
89
123
|
表单 + 点击「填写证据关闭…」补 artifact/evaluation/memory_ref 转 STABLE)、节点与关系清单
|
|
90
124
|
(节点 = 模型用 esr_node 登记的领域对象,如包/服务/仓库/概念;关系 = esr_link),
|
|
91
|
-
|
|
125
|
+
以及一个独立的 **关系图谱** 页签:手写 SVG 力导向图(无第三方图库,保持 bundle 纯净),
|
|
126
|
+
实体为圆形节点、任务为勾选徽标、关系按类型着色带方向箭头;支持拖拽节点/平移/滚轮缩放/重组,
|
|
127
|
+
悬停高亮邻域、点选节点在悬浮面板查看其全部关系与关联对象,悬空链接(端点缺失)单独计数提示。
|
|
128
|
+
以及一个 **注入预览** 页签:用与 systemPrompt 完全相同的纯函数实时渲染模型每个会话看到的
|
|
129
|
+
`[ENGRAM]` 索引块(order 40)与 `[ESR]` 任务/闭环块(order 41),终端风双栏展示(行级着色:
|
|
130
|
+
块头/任务行/drill 行/escalate 提醒高亮),附行数·字符·~tokens 成本与记忆/任务/关系/节点计数芯片,
|
|
131
|
+
每 20s 自动刷新、可一键复制注入块原文(新增 `GET /api/dsh-engram/preview?workspace=…`)。
|
|
132
|
+
以及记忆 GC 面板(dry-run 开关 + 运行按钮 + 指针报告)。
|
|
133
|
+
任务卡片(看板与 ESR 页)都带 **证据进度环**:一个三弧 SVG 圆环对应
|
|
134
|
+
artifact · evaluation · memory_ref 三道闭环门——全绿=证据齐可闭环、琥珀=有缺口、灰=尚无证据;
|
|
135
|
+
看板头部还有一个**聚合环**,显示全部进行中任务的证据完备度(%)与就绪数,一次看清整盘闭环进度。
|
|
136
|
+
纯 SVG 实现(无图表库,保持 bundle 纯净)。
|
|
137
|
+
以及一个 **遥测仪表盘** 页签:把 /stats 的真实调用累计(工作区 × 天滚动)画成纯 SVG 仪表盘——三枚大圆环
|
|
138
|
+
直读 **ESR 主动性**(与 escalate 阈值 0.34 比对,偏低标橙并提示)、**召回命中率**、**detail 转化**,
|
|
139
|
+
五张小指标卡(累计调用 / esr / 记忆 / 平均命中每查询 / 失败),近 14 天 mem-vs-esr 堆叠柱状图 +
|
|
140
|
+
工具调用 Top 8 横向条形图(mem 蓝 / esr 紫,与全文配色一致),20s 自动刷新,样本不足(<10 次)自动标注。
|
|
141
|
+
以及 **详情侧栏**(主从布局):任务 / 记忆 / 节点 / 关系行都可一键在右侧打开详情卡——任务含
|
|
142
|
+
状态徽标、证据进度环、完整 id/时间线、缺口清单、可跳转的记忆引用与「补齐证据→闭环」表单(独立于行内
|
|
143
|
+
表单,成功后联动刷新);记忆含全文、标签、signal/hits/TTL/来源会话元数据;节点含全部入射/出射关系
|
|
144
|
+
(按类型着色 + 方向 + 置信度);点击任务里的记忆引用可直达该记忆详情,找不到时给提示。
|
|
92
145
|
`POST /api/dsh-engram/tasks` 与 `POST /api/dsh-engram/tasks/close`(与 esr_task / esr_close 同一证据门)。
|
|
93
146
|
模型侧的主动行为由 [ENGRAM]/[ESR] 注入块驱动:多步工作即时建任务、反复出现的领域对象即时登记节点、相关任务/节点即时互连。
|
|
94
147
|
|
|
@@ -201,6 +254,9 @@ drill: engram_store (user asks to remember) | engram_recall <query> | engram_det
|
|
|
201
254
|
```
|
|
202
255
|
|
|
203
256
|
前缀:`[D]` 决定 · `[E]` 错误 · `[P]` 流程 · `[F]` 事实 · `[I]` 洞察 · `[H]` 交接 · `[T]` 任务。
|
|
257
|
+
被反复使用(`hits >= promoteHits`)的流程记忆升格为「实证经验」——前缀变 `[P✓]` 且**稳定排在
|
|
258
|
+
索引块最前**(每组内部仍按新旧排序,块保持确定性);这是我们对 TencentDB Agent Memory「Skill=
|
|
259
|
+
经过验证的可执行经验」的零 LLM 对应物。
|
|
204
260
|
入选规则遵循「自动捕获策略」(信号阈值 / 命中晋升 / git 回显守卫),并按配置的行数与字符预算封顶。
|
|
205
261
|
`#` id 通过 `engram_detail` 取完整记录。工作区没有任务时,`[ESR]` 仍会渲染一行点名 `esr_task`/`esr_close`,
|
|
206
262
|
让机制对模型保持可见,而不是整体消失。
|
package/lib/api.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { uuid, shortId } from "./util.js";
|
|
13
|
+
import { collectToolCounts, collectUsageStats } from "./usage.js";
|
|
13
14
|
|
|
14
15
|
/** Route family prefix (kept in sync with the client half). */
|
|
15
16
|
export const API_PREFIX = "/api/dsh-engram";
|
|
@@ -118,6 +119,13 @@ export function makeEngramRoutes(service) {
|
|
|
118
119
|
}
|
|
119
120
|
};
|
|
120
121
|
|
|
122
|
+
const toolstats = (req, res) => {
|
|
123
|
+
const q = readQuery(req);
|
|
124
|
+
const raw = Number(q.get("days") ?? 14);
|
|
125
|
+
const days = Number.isFinite(raw) && raw >= 1 ? Math.min(90, Math.round(raw)) : 14;
|
|
126
|
+
writeJson(res, 200, collectToolCounts({ root: service.statsRoot, days }));
|
|
127
|
+
};
|
|
128
|
+
|
|
121
129
|
const readConfig = (_req, res) => {
|
|
122
130
|
const c = service.config ?? {};
|
|
123
131
|
const payload = {
|
|
@@ -313,6 +321,9 @@ export function makeEngramRoutes(service) {
|
|
|
313
321
|
createdAt: now,
|
|
314
322
|
updatedAt: now,
|
|
315
323
|
stateChangedAt: 0,
|
|
324
|
+
deps: [],
|
|
325
|
+
assignee: null,
|
|
326
|
+
claimedAt: null,
|
|
316
327
|
};
|
|
317
328
|
await domain.putTask(task);
|
|
318
329
|
writeJson(res, 200, { task });
|
|
@@ -341,27 +352,60 @@ export function makeEngramRoutes(service) {
|
|
|
341
352
|
memoryRefs: refs ?? task.memoryRefs ?? [],
|
|
342
353
|
updatedAt: Date.now(),
|
|
343
354
|
};
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
if (next.evaluation === null || next.evaluation === "") gaps.push("evaluation");
|
|
347
|
-
if (!Array.isArray(next.memoryRefs) || next.memoryRefs.length === 0) gaps.push("memory_ref");
|
|
355
|
+
const gate = domain.evidenceGate(workspace, next, service.config ?? {});
|
|
356
|
+
const gaps = gate.gaps;
|
|
348
357
|
if (gaps.length > 0) {
|
|
349
358
|
await domain.putTask(next);
|
|
350
|
-
writeJson(res, 200, { ok: true, state: "active", gaps });
|
|
359
|
+
writeJson(res, 200, { ok: true, state: "active", gaps, artifactReason: gate.artifactReason });
|
|
351
360
|
return;
|
|
352
361
|
}
|
|
353
|
-
const closed = { ...next, state: "stable", stateChangedAt: Date.now() };
|
|
362
|
+
const closed = domain.compactOnClose?.({ ...next, state: "stable", stateChangedAt: Date.now() }) ?? { ...next, state: "stable", stateChangedAt: Date.now() };
|
|
354
363
|
await domain.putTask(closed);
|
|
355
364
|
writeJson(res, 200, { ok: true, state: "stable", task: closed });
|
|
356
365
|
};
|
|
357
366
|
|
|
358
|
-
/**
|
|
359
|
-
const
|
|
367
|
+
/** Exact [ENGRAM] + [ESR] injection blocks for one workspace (GUI preview). */
|
|
368
|
+
const preview = async (req, res) => {
|
|
360
369
|
const domain = await withDomain(res);
|
|
361
370
|
if (domain === void 0) return;
|
|
371
|
+
const params = readQuery(req);
|
|
372
|
+
const workspace = params.get("workspace");
|
|
373
|
+
if (!workspace) {
|
|
374
|
+
writeError(res, 400, "workspace query parameter is required");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const engram = service.renderIndexBlock?.(workspace) ?? "";
|
|
378
|
+
const esrBlock = service.renderEsrBlock?.(workspace) ?? "";
|
|
379
|
+
const counts = domain.workspaceEntries(workspace);
|
|
380
|
+
writeJson(res, 200, {
|
|
381
|
+
workspace,
|
|
382
|
+
engram,
|
|
383
|
+
esr: esrBlock,
|
|
384
|
+
meta: {
|
|
385
|
+
engram: {
|
|
386
|
+
chars: [...engram].length,
|
|
387
|
+
tokens: estimateTokens(engram),
|
|
388
|
+
lines: engram === "" ? 0 : engram.split("\n").length,
|
|
389
|
+
},
|
|
390
|
+
esr: {
|
|
391
|
+
chars: [...esrBlock].length,
|
|
392
|
+
tokens: estimateTokens(esrBlock),
|
|
393
|
+
lines: esrBlock === "" ? 0 : esrBlock.split("\n").length,
|
|
394
|
+
},
|
|
395
|
+
counts: { memories: counts.memories, tasks: counts.tasks, links: counts.links, nodes: counts.nodes },
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
void req;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/** Real agent-behaviour observability: usage rollup + ratios, computed on
|
|
402
|
+
* demand from the session log stream (assessment P3 — no per-call writes). */
|
|
403
|
+
const stats = async (req, res) => {
|
|
362
404
|
const params = readQuery(req);
|
|
363
405
|
const workspace = params.get("workspace") ?? void 0;
|
|
364
|
-
const
|
|
406
|
+
const collected = collectUsageStats({ root: service.statsRoot });
|
|
407
|
+
const allRows = collected.rows;
|
|
408
|
+
const rows = workspace === void 0 ? allRows : allRows.filter((r) => r.workspace === workspace);
|
|
365
409
|
const totals = { counts: {}, failures: 0, recall: {} };
|
|
366
410
|
for (const r of rows) {
|
|
367
411
|
for (const [k, v] of Object.entries(r.counts ?? {})) totals.counts[k] = (totals.counts[k] ?? 0) + v;
|
|
@@ -380,7 +424,11 @@ export function makeEngramRoutes(service) {
|
|
|
380
424
|
const detailFollows = totals.recall.detailFollows ?? 0;
|
|
381
425
|
writeJson(res, 200, {
|
|
382
426
|
workspace: workspace ?? null,
|
|
383
|
-
byDay: rows
|
|
427
|
+
byDay: rows
|
|
428
|
+
.slice()
|
|
429
|
+
.sort((a, b) => (a.day < b.day ? -1 : a.day > b.day ? 1 : 0))
|
|
430
|
+
.slice(-14)
|
|
431
|
+
.reverse(),
|
|
384
432
|
totals,
|
|
385
433
|
ratios: {
|
|
386
434
|
calls,
|
|
@@ -394,6 +442,34 @@ export function makeEngramRoutes(service) {
|
|
|
394
442
|
});
|
|
395
443
|
};
|
|
396
444
|
|
|
445
|
+
const triggerstats = (_req, res) => {
|
|
446
|
+
const q = readQuery(_req);
|
|
447
|
+
const ws = q.get("workspace");
|
|
448
|
+
const stats = service.recorder?.conversionStats?.(ws && ws.length > 0 ? ws : void 0) ?? null;
|
|
449
|
+
writeJson(res, 200, { stats });
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const model = async (req, res) => {
|
|
453
|
+
const domain = await withDomain(res);
|
|
454
|
+
if (domain === void 0) return;
|
|
455
|
+
const ws = readQuery(req).get("workspace") ?? "";
|
|
456
|
+
try {
|
|
457
|
+
const { getModel } = await import("./mental.js");
|
|
458
|
+
const m = await getModel(domain, ws);
|
|
459
|
+
writeJson(res, 200, { model: m });
|
|
460
|
+
} catch (err) {
|
|
461
|
+
writeJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const observations = async (req, res) => {
|
|
466
|
+
const domain = await withDomain(res);
|
|
467
|
+
if (domain === void 0) return;
|
|
468
|
+
const ws = readQuery(req).get("workspace") ?? "";
|
|
469
|
+
const list = domain.listObservations?.(ws) ?? [];
|
|
470
|
+
writeJson(res, 200, { items: list });
|
|
471
|
+
};
|
|
472
|
+
|
|
397
473
|
const make = (method, path, handler) => ({
|
|
398
474
|
kind: "exact",
|
|
399
475
|
path: `${API_PREFIX}${path}`,
|
|
@@ -408,6 +484,11 @@ export function makeEngramRoutes(service) {
|
|
|
408
484
|
make("GET", "/links", links),
|
|
409
485
|
make("GET", "/nodes", entities),
|
|
410
486
|
make("GET", "/stats", stats),
|
|
487
|
+
make("GET", "/toolstats", toolstats),
|
|
488
|
+
make("GET", "/triggerstats", triggerstats),
|
|
489
|
+
make("GET", "/observations", observations),
|
|
490
|
+
make("GET", "/model", model),
|
|
491
|
+
make("GET", "/preview", preview),
|
|
411
492
|
make("GET", "/config", readConfig),
|
|
412
493
|
make("POST", "/memories/archive", archive),
|
|
413
494
|
make("POST", "/memories/delete", remove),
|
package/lib/capture.js
CHANGED
|
@@ -105,20 +105,23 @@ export function makeCaptureHandler(store, config, log, stats) {
|
|
|
105
105
|
|
|
106
106
|
if (git !== null) {
|
|
107
107
|
entry = { kind: "decision", text: git.text, signal: git.signal, tags: git.tags };
|
|
108
|
+
} else if (result?.isError === true) {
|
|
109
|
+
// Errors take precedence over tool-name classification: a failed edit
|
|
110
|
+
// tool is a FAILURE of that file, not an "Edited" event (filePath anchor).
|
|
111
|
+
const errPath = String(args.path ?? args.file_path ?? args.filePath ?? "");
|
|
112
|
+
const msg = errorHead(result);
|
|
113
|
+
if (msg !== null) {
|
|
114
|
+
entry = { kind: "error", text: `${name} failed: ${msg}`, signal: 0.25, tags: ["error", "auto-captured"], filePath: errPath || null };
|
|
115
|
+
}
|
|
108
116
|
} else if (EDIT_TOOLS.has(name)) {
|
|
109
117
|
const path = String(args.path ?? args.file_path ?? args.filePath ?? "");
|
|
110
118
|
if (path.length > 0 && SIGNIFICANT_PATH.some((re) => re.test(path))) {
|
|
111
|
-
entry = { kind: "fact", text: `Edited ${path}`, signal: 0.3, tags: ["file", "edit", "auto-captured", `dir:${dirOf(path)}`] };
|
|
119
|
+
entry = { kind: "fact", text: `Edited ${path}`, signal: 0.3, tags: ["file", "edit", "auto-captured", `dir:${dirOf(path)}`], filePath: path };
|
|
112
120
|
}
|
|
113
121
|
} else if (READ_TOOLS.has(name)) {
|
|
114
122
|
const path = String(args.path ?? args.file_path ?? args.filePath ?? "");
|
|
115
123
|
if (path.length > 0 && /(^|\/)(AGENTS|CLAUDE)\.md$/i.test(path)) {
|
|
116
|
-
entry = { kind: "fact", text: `Read ${path}`, signal: 0.3, tags: ["file", "read", "config", "auto-captured"] };
|
|
117
|
-
}
|
|
118
|
-
} else if (result?.isError === true) {
|
|
119
|
-
const msg = errorHead(result);
|
|
120
|
-
if (msg !== null) {
|
|
121
|
-
entry = { kind: "error", text: `${name} failed: ${msg}`, signal: 0.25, tags: ["error", "auto-captured"] };
|
|
124
|
+
entry = { kind: "fact", text: `Read ${path}`, signal: 0.3, tags: ["file", "read", "config", "auto-captured"], filePath: path };
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
127
|
|
|
@@ -147,6 +150,7 @@ export function makeCaptureHandler(store, config, log, stats) {
|
|
|
147
150
|
text: entry.text,
|
|
148
151
|
tags: entry.tags,
|
|
149
152
|
signal: entry.signal,
|
|
153
|
+
filePath: entry.filePath ?? null,
|
|
150
154
|
sessionId,
|
|
151
155
|
seq: agent.session.events?.length ?? 0,
|
|
152
156
|
},
|