dsh-engram 0.2.0 → 0.3.0

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 CHANGED
@@ -15,11 +15,24 @@ 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.
23
36
  - **Memory GC (pi-esr constraints)** — a scheduled, mechanical, archive-only
24
37
  sweep: TTL-expired memories are archived, over-cap workspaces evict the
25
38
  lowest-value entries, stable tasks past their retention window leave the
@@ -98,6 +111,41 @@ scripts/setup-links.mjs --check` prints the state without writing anything.
98
111
 
99
112
  After restart, inside the **native** DSH settings surface:
100
113
 
114
+ - **Sidebar "ESR 看板" entry + full-screen kanban** — one more row under
115
+ New Session with a **live active-task badge** (polled from /overview every
116
+ 30s across all workspaces). Clicking it opens a full-screen board in the
117
+ center column: **草稿 / 进行中(gaps) / 就绪(evidence ready) / 已闭环** columns,
118
+ workspace filter + search, an inline create form, and per-card
119
+ "补齐证据 → 关闭" closure forms sharing the esr_close gates (artifact +
120
+ evaluation + memory_refs). A **看板 / 图谱 (board/graph)** toggle in the
121
+ header reuses the full relation graph (esr_node/esr_link force-directed SVG:
122
+ entity circles + task check badges, drag/zoom/select for relation details),
123
+ following the workspace filter and refreshed by the same 20s polling.
124
+ Following the task-board precedent, the entry and
125
+ the board are DOM-mounted and self-heal (MutationObserver re-inserts on shell
126
+ re-renders), with cross-panel exclusivity against task-board/ssh (opening one
127
+ evicts the others; clicking a sidebar session/workspace row hands the center
128
+ column back to the conversation). The conversation subtree stays mounted
129
+ underneath and is hidden by `html[data-dsh-engram-board-active]`, so toggling loses
130
+ no state.
131
+
132
+
133
+ - **Unified task strip above the composer** — the conversation dock that DSH
134
+ ships for its built-in todo tool is taken over (same `conversation.input.dock`
135
+ cell / `id: todo` at a lower priority) and **merged** into one modern control:
136
+ the session's current plan (`todo_write`'s `todos` projection) plus the
137
+ workspace's persistent **ESR tasks** (with evidence-gap badges and an inline
138
+ "补齐证据 → close" form) plus the **relation graph** rendered as
139
+ node → relation → node chips with entity/task names resolved. It only shows
140
+ while there is something to show, stays live with 15s polling, and the built-in
141
+ plan still renders (without the ESR parts) if the loopback-fenced API is
142
+ unreachable. A **workspace-switcher chip** leads the strip: it defaults to
143
+ following the current session (the tooltip says so), and its dropdown pins the
144
+ ESR task/relation source to any workspace (✓ marks the active pin; the × or
145
+ "follow session" entry reverts). Switching refetches immediately and is a pure
146
+ UI focus change — the model's session context and the per-session frozen
147
+ injection blocks are untouched.
148
+
101
149
  - **Settings → Engram Memory** — a standalone first-class settings section
102
150
  (right after the Plugins section, not a child tab of it). Default
103
151
  "All workspaces" view shows every workspace's memories/tasks/links
@@ -111,7 +159,41 @@ After restart, inside the **native** DSH settings surface:
111
159
  with an inline "new task" form and a per-task "fill evidence to close"
112
160
  (artifact / evaluation / memory_ref → STABLE, same gates as esr_close),
113
161
  a node + relation list (nodes are domain objects the model registers via
114
- esr_node — package/service/repo/concept; relations via esr_link), and a
162
+ esr_node — package/service/repo/concept; relations via esr_link), a separate
163
+ **relation-graph** tab (hand-rolled force-directed SVG, no chart library so
164
+ the bundle stays pure: entities as circles, tasks as check badges, relations
165
+ colored per type with direction arrows; drag nodes, pan, wheel-zoom, re-layout,
166
+ hover highlights the neighborhood, clicking a node pops a floating panel with
167
+ its incident relations and linked objects; dangling links are counted and
168
+ warned about), an **injection-preview** tab that renders the exact
169
+ `[ENGRAM]` index block (order 40) and `[ESR]` task/closure block (order 41)
170
+ the model sees each session — same pure functions as the system-prompt
171
+ sections — as two terminal-style panes with per-line coloring (block
172
+ headers, task lines, drill hint, and the data-driven `escalate:` reminder
173
+ highlighted), line/char/~token cost chips plus memory/task/link/node count
174
+ chips, 20s auto-refresh and one-click copy of the raw block text (backed by
175
+ the new `GET /api/dsh-engram/preview?workspace=…` route). Every ESR task
176
+ card (board and ESR tab) carries an **evidence-progress ring** — a small
177
+ three-arc SVG donut mapping artifact · evaluation · memory_ref, all green
178
+ when closure-ready, amber while gapped, gray with no evidence yet; the
179
+ board header adds an **aggregate ring** showing overall evidence
180
+ completeness (%) plus how many in-progress tasks are closure-ready. Pure
181
+ SVG, no chart library, bundle stays clean. A **telemetry dashboard** tab
182
+ turns the /stats usage rollup (workspace × day) into a pure-SVG dashboard:
183
+ three gauges for **ESR proactivity** (benchmarked against the 0.34 escalate
184
+ threshold, amber + hint when low), **recall hit rate** and **detail
185
+ follow-through**, five stat cards (total / esr / memory calls, avg hits per
186
+ query, failures), a 14-day mem-vs-esr stacked bar chart and a Top-8 tool
187
+ breakdown (mem blue / esr purple), 20s auto-refresh with an automatic
188
+ small-sample (<10 calls) warning. A **details sidebar** (master–detail)
189
+ opens on the right when you click any task / memory / node / relation
190
+ row: task cards show state badge, evidence ring, full id + timeline,
191
+ gap list, clickable memory references and an inline "fill evidence to
192
+ close" form that refreshes the lists on success; memory cards show full
193
+ text, tags, signal/hits/TTL and provenance metadata; node cards list all
194
+ incident relations (typed, colored, with direction + confidence); clicking
195
+ a task's memory reference jumps straight to that memory, with a hint when
196
+ it is not in the loaded set. And a
115
197
  memory-GC panel (dry-run toggle + run button + pointer report). The GUI
116
198
  create/close use the host's new `POST /api/dsh-engram/tasks` and
117
199
  `POST /api/dsh-engram/tasks/close` routes. Model-side proactivity is driven
@@ -228,8 +310,12 @@ drill: engram_store (user asks to remember) | engram_recall <query> | engram_det
228
310
  ```
229
311
 
230
312
  Prefixes: `[D]` decision · `[E]` error · `[P]` procedure · `[F]` fact ·
231
- `[I]` insight · `[H]` handoff · `[T]` task. Membership follows the
232
- *Auto-capture policy* (signal threshold / recall promotion / git-echo guard),
313
+ `[I]` insight · `[H]` handoff · `[T]` task. A procedure re-used enough times
314
+ (`hits >= promoteHits`) becomes a "proven experience" — its prefix upgrades to
315
+ `[P✓]` and it is stably ranked first in the block (newest-first within each
316
+ group, keeping the block deterministic): our zero-LLM counterpart to TencentDB
317
+ Agent Memory's "Skill = validated, executable experience". Membership follows
318
+ the *Auto-capture policy* (signal threshold / recall promotion / git-echo guard),
233
319
  bounded by the configured line and character budgets. `#` ids address the full
234
320
  records via `engram_detail`. When a workspace has no tasks, `[ESR]` still renders
235
321
  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,18 @@
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`),口径绝不会漂移。
18
26
  - **记忆 GC(pi-esr 约束)** — 定时、机械、只归档的回收:TTL 过期记忆归档、超容量工作区淘汰低价值条目、
19
27
  stable 任务超保留窗离开 `[ESR]` 表面、悬空链接边清理。工作集(active 任务引用 / 任务记忆 / 已入索引命中)
20
28
  永不触碰;**不硬删任何东西**——归档条目保留 id、始终可重取。
@@ -80,6 +88,18 @@ node scripts/setup-links.mjs # 把 @deepseek-ai 工作区包软链进 node_m
80
88
 
81
89
  重启后,全部落在 **DSH 原生**设置界面里:
82
90
 
91
+ - **侧边栏「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]` 控制显隐,切换零状态丢失。
92
+
93
+ - **输入框上方的「任务」统一条** — 接管 DSH 内建 todo 工具的自己同款 dock 槽位
94
+ (同一个 `conversation.input.dock` 单元格 / `id: todo`、更低 `priority`,从而遮蔽内建
95
+ TodoPanel),把两套任务平面**合并成一个**现代化控件:会话当前计划(`todo_write` 的
96
+ `todos` 投影)+ 工作区持久 **ESR 任务**(证据缺口徽标 + 内联「补齐证据 → 关闭」表单)
97
+ + **关系图**(以 节点 → 关系 → 节点 芯片呈现,实体/任务名自动解析)。有内容才显示,
98
+ 15s 轮询保持实时;若 loopback 围栏的 API 不可达,内建计划仍照常渲染(只是不显示 ESR 部分)。
99
+ 条首有**工作区切换 chip**:默认跟随当前会话(标题注明),下拉可把 ESR 任务/关系的来源**固定到任意
100
+ 工作区**(打 ✓ 标记),再点 × 或「跟随会话」即恢复;切换即时重取,内置 todo 仍属本会话——纯 UI 焦点
101
+ 切换,不动模型会话上下文(注入块按会话冻结,前缀稳定)。
102
+
83
103
  - **设置 → Engram 记忆** — 独立的一级设置页签(位于「插件」之后),不再是「插件」页里的子 tab;默认
84
104
  「全部工作区」视图完整展示所有工作区的记忆/任务/关系(按工作区分组,工作区下拉 + 上一/下一工作区
85
105
  翻页;记忆表格另行 10 条/页分页 + 跳页下拉,仅「类型 / 内容 / 操作」三列——正文列占满,时间、
@@ -88,7 +108,26 @@ node scripts/setup-links.mjs # 把 @deepseek-ai 工作区包软链进 node_m
88
108
  token 估算、GC 累计统计)、可搜索/可过滤的记忆表格(含归档与删除操作)、ESR 任务看板(「新建任务」
89
109
  表单 + 点击「填写证据关闭…」补 artifact/evaluation/memory_ref 转 STABLE)、节点与关系清单
90
110
  (节点 = 模型用 esr_node 登记的领域对象,如包/服务/仓库/概念;关系 = esr_link),
91
- 以及记忆 GC 面板(dry-run 开关 + 运行按钮 + 指针报告)。ESR 的 GUI 新建/关闭走宿主新增的
111
+ 以及一个独立的 **关系图谱** 页签:手写 SVG 力导向图(无第三方图库,保持 bundle 纯净),
112
+ 实体为圆形节点、任务为勾选徽标、关系按类型着色带方向箭头;支持拖拽节点/平移/滚轮缩放/重组,
113
+ 悬停高亮邻域、点选节点在悬浮面板查看其全部关系与关联对象,悬空链接(端点缺失)单独计数提示。
114
+ 以及一个 **注入预览** 页签:用与 systemPrompt 完全相同的纯函数实时渲染模型每个会话看到的
115
+ `[ENGRAM]` 索引块(order 40)与 `[ESR]` 任务/闭环块(order 41),终端风双栏展示(行级着色:
116
+ 块头/任务行/drill 行/escalate 提醒高亮),附行数·字符·~tokens 成本与记忆/任务/关系/节点计数芯片,
117
+ 每 20s 自动刷新、可一键复制注入块原文(新增 `GET /api/dsh-engram/preview?workspace=…`)。
118
+ 以及记忆 GC 面板(dry-run 开关 + 运行按钮 + 指针报告)。
119
+ 任务卡片(看板与 ESR 页)都带 **证据进度环**:一个三弧 SVG 圆环对应
120
+ artifact · evaluation · memory_ref 三道闭环门——全绿=证据齐可闭环、琥珀=有缺口、灰=尚无证据;
121
+ 看板头部还有一个**聚合环**,显示全部进行中任务的证据完备度(%)与就绪数,一次看清整盘闭环进度。
122
+ 纯 SVG 实现(无图表库,保持 bundle 纯净)。
123
+ 以及一个 **遥测仪表盘** 页签:把 /stats 的真实调用累计(工作区 × 天滚动)画成纯 SVG 仪表盘——三枚大圆环
124
+ 直读 **ESR 主动性**(与 escalate 阈值 0.34 比对,偏低标橙并提示)、**召回命中率**、**detail 转化**,
125
+ 五张小指标卡(累计调用 / esr / 记忆 / 平均命中每查询 / 失败),近 14 天 mem-vs-esr 堆叠柱状图 +
126
+ 工具调用 Top 8 横向条形图(mem 蓝 / esr 紫,与全文配色一致),20s 自动刷新,样本不足(<10 次)自动标注。
127
+ 以及 **详情侧栏**(主从布局):任务 / 记忆 / 节点 / 关系行都可一键在右侧打开详情卡——任务含
128
+ 状态徽标、证据进度环、完整 id/时间线、缺口清单、可跳转的记忆引用与「补齐证据→闭环」表单(独立于行内
129
+ 表单,成功后联动刷新);记忆含全文、标签、signal/hits/TTL/来源会话元数据;节点含全部入射/出射关系
130
+ (按类型着色 + 方向 + 置信度);点击任务里的记忆引用可直达该记忆详情,找不到时给提示。
92
131
  `POST /api/dsh-engram/tasks` 与 `POST /api/dsh-engram/tasks/close`(与 esr_task / esr_close 同一证据门)。
93
132
  模型侧的主动行为由 [ENGRAM]/[ESR] 注入块驱动:多步工作即时建任务、反复出现的领域对象即时登记节点、相关任务/节点即时互连。
94
133
 
@@ -201,6 +240,9 @@ drill: engram_store (user asks to remember) | engram_recall <query> | engram_det
201
240
  ```
202
241
 
203
242
  前缀:`[D]` 决定 · `[E]` 错误 · `[P]` 流程 · `[F]` 事实 · `[I]` 洞察 · `[H]` 交接 · `[T]` 任务。
243
+ 被反复使用(`hits >= promoteHits`)的流程记忆升格为「实证经验」——前缀变 `[P✓]` 且**稳定排在
244
+ 索引块最前**(每组内部仍按新旧排序,块保持确定性);这是我们对 TencentDB Agent Memory「Skill=
245
+ 经过验证的可执行经验」的零 LLM 对应物。
204
246
  入选规则遵循「自动捕获策略」(信号阈值 / 命中晋升 / git 回显守卫),并按配置的行数与字符预算封顶。
205
247
  `#` id 通过 `engram_detail` 取完整记录。工作区没有任务时,`[ESR]` 仍会渲染一行点名 `esr_task`/`esr_close`,
206
248
  让机制对模型保持可见,而不是整体消失。
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 gaps = [];
345
- if (next.artifact === null || next.artifact === "") gaps.push("artifact");
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
- /** Real agent-behaviour observability: per-(workspace, day) usage rollup + ratios. */
359
- const stats = async (req, res) => {
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 rows = domain.usageRows(workspace);
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.slice(-14).reverse(),
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,27 @@ export function makeEngramRoutes(service) {
394
442
  });
395
443
  };
396
444
 
445
+ const model = async (req, res) => {
446
+ const domain = await withDomain(res);
447
+ if (domain === void 0) return;
448
+ const ws = readQuery(req).get("workspace") ?? "";
449
+ try {
450
+ const { getModel } = await import("./mental.js");
451
+ const m = await getModel(domain, ws);
452
+ writeJson(res, 200, { model: m });
453
+ } catch (err) {
454
+ writeJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
455
+ }
456
+ };
457
+
458
+ const observations = async (req, res) => {
459
+ const domain = await withDomain(res);
460
+ if (domain === void 0) return;
461
+ const ws = readQuery(req).get("workspace") ?? "";
462
+ const list = domain.listObservations?.(ws) ?? [];
463
+ writeJson(res, 200, { items: list });
464
+ };
465
+
397
466
  const make = (method, path, handler) => ({
398
467
  kind: "exact",
399
468
  path: `${API_PREFIX}${path}`,
@@ -408,6 +477,10 @@ export function makeEngramRoutes(service) {
408
477
  make("GET", "/links", links),
409
478
  make("GET", "/nodes", entities),
410
479
  make("GET", "/stats", stats),
480
+ make("GET", "/toolstats", toolstats),
481
+ make("GET", "/observations", observations),
482
+ make("GET", "/model", model),
483
+ make("GET", "/preview", preview),
411
484
  make("GET", "/config", readConfig),
412
485
  make("POST", "/memories/archive", archive),
413
486
  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
  },