@aiwayds/dsh-topics-memory 0.18.2 → 0.20.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
@@ -38,6 +38,82 @@ This plugin is not trying to be the model's notebook. It is trying to be the mod
38
38
  - **Knowledge as a graph**: `depends` (machine-readable directed edges) plus body `[[wikilinks]]` and markdown links (human-written edges) form one graph; retrieval walks it in both directions (per-level decay, configurable depth) so a single hit pulls in a knowledge subgraph; every write rebuilds the `meta/backlinks.json` reverse index, and `/topics show` lists "who references me, and how" — check the blast radius before changing a conclusion.
39
39
  - **Two-stage observer (M2)**: the main model jots atomic observations with `topic_observe`; a background distill lane (session end + every N turns, model configurable) distills them into formal Topics in batches; when the model itself deems something worth keeping, it `topic_save`s directly.
40
40
 
41
+ ## Jev decision layer (System One, experimental, default off)
42
+
43
+ An optional decision layer on the asynchronous lanes, powered by a System One typed-decision model (jev): it batch-scores slow-lane rerank candidates (replacing the LLM rerank), pre-filters consolidation merge pairs before the LLM gardener sees them, and shadow-audits the fast-lane lexical gate from a turn-end lane. Every verdict is probability-gated, and any failure — timeout, network error, bad answer — falls back to today's pure-local behavior (hardcoded fail-open, no switch key). The injection hot path never makes a remote call (ADR 0016–0019; design doc: `docs/design/2026-09-25-system-one-integration.md`). Off by default: `jevEnabled: false` means zero behavior change.
44
+
45
+ ### Keys
46
+
47
+ | Key | Default | Meaning |
48
+ |---|---|---|
49
+ | `jevEnabled` | `false` | Master switch (rollout: default off → shadow → opt-in per profile → default-on observation); `false` stops the stats too |
50
+ | `jevBackend` | `zen` | `zen` (free) / `native` / `openrouter` |
51
+ | `jevModel` | empty sentinel | Resolved per backend at call time: `jev-1.13-free` (zen) / `jev-1.13.0` (native) / `typesafe/jev-1.13` (openrouter) — pinned, never an alias |
52
+ | `jevTimeoutMs` | `3000` | Per-request timeout; single `AbortSignal.timeout`, no retry (the lane's next cadence retries naturally) |
53
+ | `jevSecretFile` | none | Path to an external secret list; loaded once at boot, re-read when the path is hot-changed |
54
+
55
+ ### Keys provisioning
56
+
57
+ | Backend | Key source |
58
+ |---|---|
59
+ | `zen` (default, free) | `JEV_ZEN_API_KEY` env, or the macOS keychain service `opencode-zen-inference` (default fallback — zero config) |
60
+ | `native` | `TYPESAFE_API_KEY` |
61
+ | `openrouter` | `OPENROUTER_API_KEY`; the keychain service `openrouter-inference` requires an explicit `JEV_KEYCHAIN` |
62
+
63
+ dsh scrubs ambient `KEY|PASSWORD|SECRET|TOKEN` variables from plugin environments, so a key exported in your shell never reaches the plugin — pass it explicitly through the profile patch `env:` block, or keep it out of files entirely via the keychain (`JEV_KEYCHAIN` / zen's default service; neither matches the scrub rules):
64
+
65
+ ```yaml
66
+ # ~/.dsh/cordis.patch.yml — merge into your profile patch. The !!js
67
+ # expression keeps the key out of the file (same convention as dsh-jev-mcp).
68
+ - insert:
69
+ - id: dsh-topics-memory
70
+ name: '@aiwayds/dsh-topics-memory'
71
+ env:
72
+ JEV_ZEN_API_KEY: !!js process.env.JEV_ZEN_API_KEY ?? ''
73
+ # native: TYPESAFE_API_KEY: !!js process.env.TYPESAFE_API_KEY ?? ''
74
+ # openrouter: OPENROUTER_API_KEY: !!js process.env.OPENROUTER_API_KEY ?? ''
75
+ # plus JEV_KEYCHAIN: 'openrouter-inference'
76
+ # (zen needs no JEV_KEYCHAIN — with no env key it reads the
77
+ # default keychain service 'opencode-zen-inference')
78
+ ```
79
+
80
+ ### Thresholds
81
+
82
+ Adopt / record / fallback bands (calibrated offline on a 383-case gold corpus; the numbers are bound to the batch protocol — design doc §5). Record-band verdicts only land in the decision log without affecting behavior. The cluster-level "worth consolidating?" question records without gating in v1:
83
+
84
+ | Seam | Adopt | Record-only | Fallback line |
85
+ |---|---|---|---|
86
+ | Slow-lane rerank (per candidate) | noul ≥ 0.60 → enters picks | 0.10 – 0.60 | < 0.10 → strong veto, lexical-gate behavior |
87
+ | Consolidation merge (per pair) | noul ≥ 0.50 → pair sent to the LLM | 0.15 – 0.50 (pair still evaluated by the LLM) | < 0.15 → pair cut before the LLM |
88
+
89
+ ### Fallback behavior
90
+
91
+ | Seam | On failure / timeout / bad answer | Legacy path |
92
+ |---|---|---|
93
+ | Slow-lane rerank | The round falls back to the old LLM rerank; picks still produced | Old RERANK_PROMPT path kept until rollout step 4 (default-on observation), then removed |
94
+ | Consolidation prefilter | The cluster goes to the LLM in full, exactly as today | none |
95
+ | Fast-lane shadow | Records the outcome only; zero behavior impact | none |
96
+
97
+ **Setting up laya**: the bundled skill `dsh-topics-memory-laya` walks through install (venv, mirror acceleration for CN networks, checkpoint download), starting laya-serve, wiring `jevLayaFallback`, and verifying via decisions.jsonl. Ask the agent about laya, or read `skills/dsh-topics-memory-laya/SKILL.md` directly.
98
+
99
+ **Local laya pace-maker (experimental, off by default).** With `jevLayaFallback: true` and a local [laya](https://github.com/NandhaKishorM/laya) serve running, every jev call fires a parallel local laya request: the primary answer drives the decision, and the laya answer is logged alongside it — a permanently running laya-vs-jev comparison on identical questions. laya is TELEMETRY ONLY: on real queries its within-batch ranking agreed with the primary 0/14 and its negative scores sit at 0.63-0.76 (absolute scores unusable), so a primary failure falls open to the legacy path exactly as without the pace-maker — the comparison rows are the value, not a degraded takeover. The consolidation prefilter deliberately does not ride the pace-maker. laya not running costs a refused connection in milliseconds.
100
+
101
+ Every call and verdict lands in `~/.dsh/topics/meta/decisions.jsonl` — local-only and redacted: slugs, pair hashes, question types, probabilities, latency and token counts; never conversation text or conclusion bodies. It has no config key (it stops together with `jevEnabled: false`); `/topics status` shows a 30-day summary line.
102
+
103
+
104
+ ### Latency benchmarks (measured 2026-09-26, Apple M5, typesafe `native` backend, jev-1.13.0)
105
+
106
+ Real-payload benchmarks and in-sandbox runs — use them to pick `jevTimeoutMs`. Sources vary in shape and load; all are the same 8-candidate shadow batch unless noted:
107
+
108
+ | Source | Shape | p50 | p90 | max | n |
109
+ |---|---|---|---|---|---|
110
+ | Idle benchmark (sequential) | 8-question batch | 742 ms | 1290 ms | 1461 ms | 10 |
111
+ | Idle benchmark (sequential) | 1-question | 367 ms | — | 925 ms | 5 |
112
+ | Threshold-sweep runs (Sept 25) | 20-question batch | 1760 ms avg | — | 8470 ms | 16 |
113
+ | Live headless turns | 8-question batch, **concurrent with the main model streaming** | 327–5698 ms | — | 10619 ms | 3 |
114
+
115
+ Readings that matter: the idle path sits comfortably under the 3000 ms default (≈2× headroom at p90), but a live turn's shadow call shares the network with the main model's streaming response — the 10.6 s outlier above was observed exactly there. A timeout is fail-open: the batch is dropped (slow-lane rerank falls back to the old LLM path), so a tight timeout costs shadow data, never correctness. Keep the 3000 ms default unless `decisions.jsonl` shows a persistent `timeout` share above ~5% (the 30-day summary in `/topics status` surfaces it); weak-network users can raise `jevTimeoutMs` freely. `zen` and `openrouter` are unmeasured here — after enabling, your own decisions.jsonl latency column is the ground truth for your network.
116
+
41
117
  ## Quick start
42
118
 
43
119
  1. Install (command below), restart dsh;
@@ -126,6 +202,13 @@ First-time setup belongs to `/topics onboard`; day-to-day tuning is `/topics set
126
202
  | `deprecatedTtlDays` | `15` | Deprecated topics older than N days are dropped at session start (local rule, no model; each drop is its own git commit — history stays recoverable); `0` disables the sweep |
127
203
  | `usageBoost` | `0.15` | Usage boost (ADR 0015): topics injected/opened in the last 30 days score higher at retrieval (gate-scoped, capped at 0.2, never granted to zero-lexical candidates, structural gate not waived); `0` disables |
128
204
  | `pushDebounceSeconds` | `45` | GitHub-mode debounced push interval |
205
+ | `jevEnabled` | `false` | System One decision layer master switch (experimental): `false` = zero behavior change — see the Jev decision layer section above |
206
+ | `jevBackend` | `zen` | Decision endpoint: `zen` (free) / `native` / `openrouter` |
207
+ | `jevModel` | empty (per backend: `jev-1.13-free` / `jev-1.13.0` / `typesafe/jev-1.13`) | Version-pinned decision model; upgrading is an explicit action |
208
+ | `jevTimeoutMs` | `3000` | Per-request decision timeout; single attempt, no retry |
209
+ | `jevSecretFile` | none | External secret list for the outbound secret gate; re-read when hot-changed |
210
+ | `jevLayaFallback` | `false` | Local laya pace-maker: fire a parallel laya call on every jev request; its answer takes over (degraded, relative-ranking only) when the primary fails |
211
+ | `jevLayaUrl` | `http://127.0.0.1:8000/v1/systemone` | laya-serve endpoint for the pace-maker |
129
212
 
130
213
  ## Acknowledgements
131
214
 
@@ -148,7 +231,7 @@ This project's shape is directly inspired and supported by:
148
231
  ## Design docs
149
232
 
150
233
  - [CONTEXT.md](CONTEXT.md) — domain glossary
151
- - [docs/adr/](docs/adr/) — 0001–0013: OKF compliance, remote shape, sync strategy, two-stage observer, bundle layout, injection defaults, observability & tunables, dual-mode persistence, onboarding wizard, subagent isolation, the include-subagents switch, injection dedup default-on, the rename & migration
234
+ - [docs/adr/](docs/adr/) — 0001–0019: OKF compliance, remote shape, sync strategy, two-stage observer, bundle layout, injection defaults, observability & tunables, dual-mode persistence, onboarding wizard, subagent isolation, the include-subagents switch, injection dedup default-on, the rename & migration, dual-channel injection, the usage boost, direct-HTTP decisions, the fast-lane zero-remote-calls line, hardcoded fail-open, the embedded secret gate
152
235
 
153
236
  ## License
154
237
 
package/README.zh.md CHANGED
@@ -38,6 +38,68 @@ https://github.com/user-attachments/assets/6e9b346d-2ec9-4148-ae62-7087797d3188
38
38
  - **知识连接成图**:`depends`(机器可读的有向依赖边)+ 正文 `[[wikilink]]` 与 markdown 链接(人写边)共同构成图;检索命中后沿图双向游走(每层衰减一半、深度可配),一次命中带入一个知识子图;每次写入自动重建 `meta/backlinks.json` 反向引用索引,`/topics show` 直接列出「谁引用了我、怎么引用的」——改一条结论前先看牵连面。
39
39
  - **两段式观察(M2)**:主模型用 `topic_observe` 随手记原子观察,后台蒸馏 lane(session end + 每 N 轮,模型可配)把观察批量蒸馏成正式 Topic;主模型认为值得记时直接 `topic_save`。
40
40
 
41
+ ## Jev 决策层(System One,实验,default off)
42
+
43
+ 架在异步 lane 上的可选决策层,由 System One typed-decision 模型(jev)驱动:慢车道 rerank 候选改批量 noul 打分(替换 LLM rerank)、整理 lane 的 merge 候选对在 LLM 园丁接手前先做前置过滤、快道词法门由 turn-end lane 做影子对账。每个判定都过概率门控,任何失败——超时、网络错、坏答案——一律回退到今天的纯本地行为(fail-open 硬编码,无开关键)。注入热路径永不发起远程调用(ADR 0016–0019;设计文档:`docs/design/2026-09-25-system-one-integration.md`)。默认关闭:`jevEnabled: false` 即零行为变化。
44
+
45
+ ### 键
46
+
47
+ | 键 | 默认 | 说明 |
48
+ |---|---|---|
49
+ | `jevEnabled` | `false` | 总开关(灰度:default off → shadow → opt-in per profile → 默认开观察);`false` 连统计一起停 |
50
+ | `jevBackend` | `zen` | `zen`(免费)/ `native` / `openrouter` |
51
+ | `jevModel` | 空串哨兵 | 调用时按 backend 解析:`jev-1.13-free`(zen)/ `jev-1.13.0`(native)/ `typesafe/jev-1.13`(openrouter)——钉版本号,永不指向别名 |
52
+ | `jevTimeoutMs` | `3000` | 单请求超时;单发 `AbortSignal.timeout` 不重试(lane 的下一节拍自然重试) |
53
+ | `jevSecretFile` | 无 | 外部 secret 清单路径;启动加载一次,热更换路径即重读 |
54
+
55
+ ### key 配置
56
+
57
+ | Backend | key 来源 |
58
+ |---|---|
59
+ | `zen`(默认,免费) | `JEV_ZEN_API_KEY` env,或 macOS 钥匙串服务 `opencode-zen-inference`(默认回退——零配置) |
60
+ | `native` | `TYPESAFE_API_KEY` |
61
+ | `openrouter` | `OPENROUTER_API_KEY`;钥匙串服务 `openrouter-inference` 需显式 `JEV_KEYCHAIN` |
62
+
63
+ dsh 会清洗插件环境里匹配 `KEY|PASSWORD|SECRET|TOKEN` 的 ambient 变量,shell 里 export 的 key 到不了插件——请走 profile patch 的 `env:` 块显式透传,或用钥匙串(`JEV_KEYCHAIN` / zen 默认服务;都不匹配清洗规则)完全避开文件落 key:
64
+
65
+ ```yaml
66
+ # ~/.dsh/cordis.patch.yml — 并入你的 profile patch。!!js 表达式让 key 不落文件
67
+ # (与 dsh-jev-mcp 同款约定)。
68
+ - insert:
69
+ - id: dsh-topics-memory
70
+ name: '@aiwayds/dsh-topics-memory'
71
+ env:
72
+ JEV_ZEN_API_KEY: !!js process.env.JEV_ZEN_API_KEY ?? ''
73
+ # native: TYPESAFE_API_KEY: !!js process.env.TYPESAFE_API_KEY ?? ''
74
+ # openrouter: OPENROUTER_API_KEY: !!js process.env.OPENROUTER_API_KEY ?? ''
75
+ # 另加 JEV_KEYCHAIN: 'openrouter-inference'
76
+ # (zen 无需 JEV_KEYCHAIN——无 env key 时自动读默认钥匙串服务)
77
+ ```
78
+
79
+ ### 阈值(rerank / merge pair)
80
+
81
+ | 档 | rerank(慢车道) | merge pair(整理前置) | 动作 |
82
+ |---|---|---|---|
83
+ | 采纳 | noul ≥ 0.60 | ≥ 0.50 | 进 picks / 送 LLM 生成 |
84
+ | 记录带 | 0.10 – 0.60 | 0.15 – 0.50 | 只落 decisions.jsonl,不影响行为 |
85
+ | 回退 | < 0.10 | < 0.15 | 强否决,回退词法/现状行为 |
86
+
87
+ 绝对线绑定批量协议(换协议必须重扫)。三缝失败回退行为见 ADR 0018。判定与调用明细落 `meta/decisions.jsonl`(本地 only,脱敏——绝不存 state 原文)。
88
+
89
+
90
+ ### 延迟基准(2026-09-26 实测,Apple M5,typesafe `native` 后端,jev-1.13.0)
91
+
92
+ 真实负载基准 + 沙箱实跑——用来选 `jevTimeoutMs`。各 source 形状与负载不同,除注明外均为同一 8 候选 shadow 批量:
93
+
94
+ | 来源 | 形状 | p50 | p90 | max | n |
95
+ |---|---|---|---|---|---|
96
+ | 空载基准(顺序发) | 8 问批量 | 742 ms | 1290 ms | 1461 ms | 10 |
97
+ | 空载基准(顺序发) | 单问 | 367 ms | — | 925 ms | 5 |
98
+ | 阈值扫描(09-25) | 20 问批量 | 均值 1760 ms | — | 8470 ms | 16 |
99
+ | 真机 headless 轮 | 8 问批量,**与主模型流式并发** | 327–5698 ms | — | 10619 ms | 3 |
100
+
101
+ 关键读数:空载路径远低于 3000 ms 默认线(p90 约 2× 余量);但真机轮的 shadow 调用与主模型的流式响应共享网络——上表 10.6 s 离群值正是在这个位置观测到的。超时即 fail-open:该批数据丢弃(慢车道回退旧 LLM rerank),**紧超时损失的是 shadow 数据,不是正确性**。默认 3000 保持不变;除非 `decisions.jsonl` 显示 `timeout` 占比持续 >5%(`/topics status` 的 30 天摘要会露出),弱网用户可自行上调 `jevTimeoutMs`。`zen` 与 `openrouter` 本次未实测——开启后你自己 decisions.jsonl 的 latency 列就是你网络的真值。
102
+
41
103
  ## 工具与命令
42
104
 
43
105
  | 模型工具 | 用途 |
@@ -126,6 +188,20 @@ dsh plugin --profile <name> remove @aiwayds/dsh-topics-memory
126
188
  | `deprecatedTtlDays` | `15` | deprecated 条目超过 N 天在会话启动时自动删除(本地规则不依赖模型,逐条 git commit 可回溯找回);`0` 关闭清扫 |
127
189
  | `usageBoost` | `0.15` | 使用加成(ADR 0015):近 30 天被注入命中/点开过的 Topic 检索加分(计入门槛分、帽 0.2、零词面相关不加、结构门不豁免);`0` 关闭 |
128
190
  | `pushDebounceSeconds` | `45` | GitHub 模式去抖推送间隔 |
191
+ | `jevEnabled` | `false` | System One 决策层总开关(实验):`false` = 零行为变化——见上方「Jev 决策层」一节 |
192
+ | `jevBackend` | `zen` | 决策端点:`zen`(免费)/ `native` / `openrouter` |
193
+ | `jevModel` | 空(按 backend:`jev-1.13-free` / `jev-1.13.0` / `typesafe/jev-1.13`) | 版本钉定的决策模型;升级是显式动作 |
194
+ | `jevTimeoutMs` | `3000` | 单次决策请求超时;单发不重试 |
195
+ | `jevSecretFile` | 无 | 出网 secret gate 的外部清单路径;热更换路径即重读 |
196
+ | `jevLayaFallback` | `false` | 本地 laya 陪跑:每次 jev 调用并发一路 laya 请求;laya 为纯遥测对照(不接管决策,真实负载排序一致率 0/14) |
197
+ | `jevLayaUrl` | `http://127.0.0.1:8000/v1/systemone` | 陪跑用的 laya-serve 端点 |
198
+ | `jevDebug` | 关 | 诊断日志开关(走宿主 logger,默认静默) |
199
+
200
+ ### laya 本地陪跑(实验,默认关)
201
+
202
+ `jevLayaFallback: true` 且本地跑着 [laya](https://github.com/NandhaKishorM/laya) serve 时,每次 jev 调用并发一路本地 laya 请求:主路答案驱动决策,laya 答案并排落账——一组永久运行的 laya vs jev 同题对照。laya 是**纯遥测**(真实负载实测批内排序与主路一致率 0/14、绝对分不可用),主路失败照旧回退旧路径;laya 没起 = 毫秒级连接拒绝。
203
+
204
+ **laya 安装配置**:随包 skill `dsh-topics-memory-laya` 完整覆盖安装(venv / 国内镜像加速 / checkpoint 下载)、启动 laya-serve、配置 `jevLayaFallback`、用 decisions.jsonl 验证生效。对 agent 说 laya 即可触发,或直接读 `skills/dsh-topics-memory-laya/SKILL.md`。
129
205
 
130
206
  ## Acknowledgements
131
207
 
@@ -148,7 +224,7 @@ dsh plugin --profile <name> remove @aiwayds/dsh-topics-memory
148
224
  ## 设计文档
149
225
 
150
226
  - [CONTEXT.md](CONTEXT.md) — 领域术语表
151
- - [docs/adr/](docs/adr/) — 0001–0013:OKF 合规、Remote 形态、同步策略、两段式 Observer、Bundle 布局、注入默认值、可观测与调参、双模式持久化、配置向导、子代理隔离、include-subagents 开关、注入去重默认开、更名与迁移
227
+ - [docs/adr/](docs/adr/) — 0001–0019:OKF 合规、Remote 形态、同步策略、两段式 Observer、Bundle 布局、注入默认值、可观测与调参、双模式持久化、配置向导、子代理隔离、include-subagents 开关、注入去重默认开、更名与迁移、双通道注入、使用加成、决策层直连 HTTP、快道零远程红线、fail-open 硬编码、secret gate 内嵌
152
228
 
153
229
  ## License
154
230
 
package/lib/commands.js CHANGED
@@ -11,6 +11,7 @@ import { spawn } from 'node:child_process';
11
11
  import { serializeTopicDoc, slugify, firstParagraph } from "./okf.js";
12
12
  import { buildGraph, renderGraphHtml } from "./viz.js";
13
13
  import { CONFIG_KEYS, displayKey, parseConfigValue } from "./config.js";
14
+ import { reloadSecrets } from "./jev/gate.js";
14
15
  import { aggregateStats } from "./ilog.js";
15
16
  import { askDistillSingle, createOnboardHandler, llmDirectoryNotice, makePanel, parseDistillInput, pickLlmDirectory, } from "./onboard.js";
16
17
  export const HELP = [
@@ -582,5 +583,7 @@ async function doSet(service, tokens, mutate, resolveAsk = () => undefined, reso
582
583
  await mutate([{ op: 'set', path: [key], value: parsed }]);
583
584
  if (key === 'repo' || key === 'autoInject')
584
585
  service.invalidate();
586
+ if (key === 'jevSecretFile')
587
+ reloadSecrets(String(parsed)); // hot-swapped list: force re-read
585
588
  return ok(`✅ topics.${displayKey(key)} = ${String(parsed)}`);
586
589
  }
package/lib/config.d.ts CHANGED
@@ -72,6 +72,31 @@ export declare const TopicsConfig: z<Schemastery.ObjectS<NoInfer<{
72
72
  usageBoost: z<number, number, "volatile-defined">;
73
73
  /** Debounced push delay in GitHub mode. */
74
74
  pushDebounceSeconds: z<number, number, "volatile-defined">;
75
+ /** System One decision-model integration (design 2026-09-25), default-off
76
+ * master switch: false means zero behavior change anywhere (the fast lane
77
+ * never calls out regardless — hard guard); the usage stats stop with it. */
78
+ jevEnabled: z<boolean, boolean, "volatile-defined">;
79
+ /** Decision backend: zen (free, opencode.ai) | native (typesafe
80
+ * first-party) | openrouter (decisions protocol). */
81
+ jevBackend: z<string, string, "volatile-defined">;
82
+ /** Pinned model id; '' = resolve the backend default at call time
83
+ * (distillProvider sentinel pattern): jev-1.13-free / jev-1.13.0 /
84
+ * typesafe/jev-1.13 — an upgrade is a deliberate act. */
85
+ jevModel: z<string, string, "volatile-defined">;
86
+ /** Single-request timeout in ms; one AbortSignal, NO retry (the calling
87
+ * lane's next cadence is the natural retry, fail-open hard-coded). */
88
+ jevTimeoutMs: z<number, number, "volatile-defined">;
89
+ /** External secret list for the outbound secret gate (JEV_SECRET_FILE
90
+ * semantics, design §3.3); swapping the path hot reloads the list. */
91
+ jevSecretFile: z<string, string, "volatile-defined">;
92
+ /** Local laya pace-maker (design §3.4, 09-27): fire a parallel laya call on
93
+ * every jev request; its verdict is logged for comparison and takes over
94
+ * ONLY when the primary backend fails (degraded). Default off — zero
95
+ * behavior change, and laya-serve not running costs a refused connection. */
96
+ jevLayaFallback: z<boolean, boolean, "volatile-defined">;
97
+ /** laya-serve systemone endpoint for the pace-maker (default: the local
98
+ * laya-serve bind from the laya repo's serve mode). */
99
+ jevLayaUrl: z<string, string, "volatile-defined">;
75
100
  }>>, Schemastery.ObjectT<NoInfer<{
76
101
  /** GitHub repo `owner/name`; empty = local-only mode (ADR 0008). */
77
102
  repo: z<string, string, "volatile-defined">;
@@ -139,6 +164,31 @@ export declare const TopicsConfig: z<Schemastery.ObjectS<NoInfer<{
139
164
  usageBoost: z<number, number, "volatile-defined">;
140
165
  /** Debounced push delay in GitHub mode. */
141
166
  pushDebounceSeconds: z<number, number, "volatile-defined">;
167
+ /** System One decision-model integration (design 2026-09-25), default-off
168
+ * master switch: false means zero behavior change anywhere (the fast lane
169
+ * never calls out regardless — hard guard); the usage stats stop with it. */
170
+ jevEnabled: z<boolean, boolean, "volatile-defined">;
171
+ /** Decision backend: zen (free, opencode.ai) | native (typesafe
172
+ * first-party) | openrouter (decisions protocol). */
173
+ jevBackend: z<string, string, "volatile-defined">;
174
+ /** Pinned model id; '' = resolve the backend default at call time
175
+ * (distillProvider sentinel pattern): jev-1.13-free / jev-1.13.0 /
176
+ * typesafe/jev-1.13 — an upgrade is a deliberate act. */
177
+ jevModel: z<string, string, "volatile-defined">;
178
+ /** Single-request timeout in ms; one AbortSignal, NO retry (the calling
179
+ * lane's next cadence is the natural retry, fail-open hard-coded). */
180
+ jevTimeoutMs: z<number, number, "volatile-defined">;
181
+ /** External secret list for the outbound secret gate (JEV_SECRET_FILE
182
+ * semantics, design §3.3); swapping the path hot reloads the list. */
183
+ jevSecretFile: z<string, string, "volatile-defined">;
184
+ /** Local laya pace-maker (design §3.4, 09-27): fire a parallel laya call on
185
+ * every jev request; its verdict is logged for comparison and takes over
186
+ * ONLY when the primary backend fails (degraded). Default off — zero
187
+ * behavior change, and laya-serve not running costs a refused connection. */
188
+ jevLayaFallback: z<boolean, boolean, "volatile-defined">;
189
+ /** laya-serve systemone endpoint for the pace-maker (default: the local
190
+ * laya-serve bind from the laya repo's serve mode). */
191
+ jevLayaUrl: z<string, string, "volatile-defined">;
142
192
  }>>, "plain">;
143
193
  export type TopicsConfigValue = {
144
194
  repo: string;
@@ -167,8 +217,19 @@ export type TopicsConfigValue = {
167
217
  deprecatedTtlDays: number;
168
218
  usageBoost: number;
169
219
  pushDebounceSeconds: number;
220
+ /** System One integration keys (design 2026-09-25 §4). Optional in this TS
221
+ * view only: index.ts's bare-harness DEFAULTS literal predates them and
222
+ * schemastery applies the runtime defaults on real hosts — consumers must
223
+ * treat undefined as the documented default. */
224
+ jevEnabled?: boolean;
225
+ jevBackend?: 'zen' | 'native' | 'openrouter';
226
+ jevModel?: string;
227
+ jevTimeoutMs?: number;
228
+ jevSecretFile?: string;
229
+ jevLayaFallback?: boolean;
230
+ jevLayaUrl?: string;
170
231
  };
171
- export declare const CONFIG_KEYS: readonly ["repo", "autoInject", "injectDedup", "suppressEcho", "topK", "perTopicBudget", "totalBudget", "matchThreshold", "tagBoost", "injectMode", "qualityLane", "graphDepth", "recencyWindowDays", "autoObserve", "includeSubagents", "observationMaxChars", "distillEveryTurns", "distillOnSessionEnd", "distillProvider", "distillModel", "distillBatchSize", "distillMaxModelCalls", "consolidateCadence", "deprecatedTtlDays", "usageBoost", "pushDebounceSeconds"];
232
+ export declare const CONFIG_KEYS: readonly ["repo", "autoInject", "injectDedup", "suppressEcho", "topK", "perTopicBudget", "totalBudget", "matchThreshold", "tagBoost", "injectMode", "qualityLane", "graphDepth", "recencyWindowDays", "autoObserve", "includeSubagents", "observationMaxChars", "distillEveryTurns", "distillOnSessionEnd", "distillProvider", "distillModel", "distillBatchSize", "distillMaxModelCalls", "consolidateCadence", "deprecatedTtlDays", "usageBoost", "pushDebounceSeconds", "jevEnabled", "jevBackend", "jevModel", "jevTimeoutMs", "jevSecretFile", "jevLayaFallback", "jevLayaUrl"];
172
233
  export type ConfigKey = (typeof CONFIG_KEYS)[number];
173
234
  /** CamelCase → dash-display (topK → top-k) for command output. */
174
235
  export declare function displayKey(key: string): string;
package/lib/config.js CHANGED
@@ -72,6 +72,31 @@ export const TopicsConfig = z.object({
72
72
  usageBoost: z.number().default(0.15).volatile(),
73
73
  /** Debounced push delay in GitHub mode. */
74
74
  pushDebounceSeconds: z.number().default(45).volatile(),
75
+ /** System One decision-model integration (design 2026-09-25), default-off
76
+ * master switch: false means zero behavior change anywhere (the fast lane
77
+ * never calls out regardless — hard guard); the usage stats stop with it. */
78
+ jevEnabled: z.boolean().default(false).volatile(),
79
+ /** Decision backend: zen (free, opencode.ai) | native (typesafe
80
+ * first-party) | openrouter (decisions protocol). */
81
+ jevBackend: z.string().default('zen').volatile(),
82
+ /** Pinned model id; '' = resolve the backend default at call time
83
+ * (distillProvider sentinel pattern): jev-1.13-free / jev-1.13.0 /
84
+ * typesafe/jev-1.13 — an upgrade is a deliberate act. */
85
+ jevModel: z.string().default('').volatile(),
86
+ /** Single-request timeout in ms; one AbortSignal, NO retry (the calling
87
+ * lane's next cadence is the natural retry, fail-open hard-coded). */
88
+ jevTimeoutMs: z.number().default(3000).volatile(),
89
+ /** External secret list for the outbound secret gate (JEV_SECRET_FILE
90
+ * semantics, design §3.3); swapping the path hot reloads the list. */
91
+ jevSecretFile: z.string().default('').volatile(),
92
+ /** Local laya pace-maker (design §3.4, 09-27): fire a parallel laya call on
93
+ * every jev request; its verdict is logged for comparison and takes over
94
+ * ONLY when the primary backend fails (degraded). Default off — zero
95
+ * behavior change, and laya-serve not running costs a refused connection. */
96
+ jevLayaFallback: z.boolean().default(false).volatile(),
97
+ /** laya-serve systemone endpoint for the pace-maker (default: the local
98
+ * laya-serve bind from the laya repo's serve mode). */
99
+ jevLayaUrl: z.string().default('http://127.0.0.1:8000/v1/systemone').volatile(),
75
100
  });
76
101
  export const CONFIG_KEYS = [
77
102
  'repo',
@@ -100,6 +125,13 @@ export const CONFIG_KEYS = [
100
125
  'deprecatedTtlDays',
101
126
  'usageBoost',
102
127
  'pushDebounceSeconds',
128
+ 'jevEnabled',
129
+ 'jevBackend',
130
+ 'jevModel',
131
+ 'jevTimeoutMs',
132
+ 'jevSecretFile',
133
+ 'jevLayaFallback',
134
+ 'jevLayaUrl',
103
135
  ];
104
136
  /** CamelCase → dash-display (topK → top-k) for command output. */
105
137
  export function displayKey(key) {
@@ -119,13 +151,19 @@ export function parseConfigValue(key, raw) {
119
151
  case 'suppressEcho':
120
152
  case 'autoObserve':
121
153
  case 'distillOnSessionEnd':
122
- case 'includeSubagents': {
154
+ case 'includeSubagents':
155
+ case 'jevEnabled': {
123
156
  if (raw === 'on' || raw === 'true')
124
157
  return true;
125
158
  if (raw === 'off' || raw === 'false')
126
159
  return false;
127
160
  return { error: `${key} 取值 on|off` };
128
161
  }
162
+ case 'jevBackend': {
163
+ if (raw === 'zen' || raw === 'native' || raw === 'openrouter')
164
+ return raw;
165
+ return { error: 'jev-backend 取值 zen|native|openrouter' };
166
+ }
129
167
  case 'topK':
130
168
  case 'perTopicBudget':
131
169
  case 'totalBudget':
@@ -136,7 +174,8 @@ export function parseConfigValue(key, raw) {
136
174
  case 'distillBatchSize':
137
175
  case 'distillMaxModelCalls':
138
176
  case 'deprecatedTtlDays':
139
- case 'pushDebounceSeconds': {
177
+ case 'pushDebounceSeconds':
178
+ case 'jevTimeoutMs': {
140
179
  const n = Number(raw);
141
180
  if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n))
142
181
  return { error: `${key} 需要非负整数` };
@@ -167,6 +206,16 @@ export function parseConfigValue(key, raw) {
167
206
  }
168
207
  case 'distillProvider':
169
208
  case 'distillModel':
209
+ case 'jevModel':
210
+ case 'jevSecretFile':
211
+ case 'jevLayaUrl':
170
212
  return raw;
213
+ case 'jevLayaFallback': {
214
+ if (raw === 'on' || raw === 'true')
215
+ return true;
216
+ if (raw === 'off' || raw === 'false')
217
+ return false;
218
+ return { error: 'jev-laya-fallback 取值 on|off' };
219
+ }
171
220
  }
172
221
  }
@@ -23,11 +23,19 @@
23
23
  * - the cadence stamp (meta/consolidate-state.json) advances only when the
24
24
  * model actually evaluated at least one cluster — a run that died on the
25
25
  * first call leaves the stamp alone, and the next session start retries.
26
+ * - jev prefilter (design 2026-09-25 §2, default-off): with jevEnabled the
27
+ * cluster first answers to ONE cheap System One batch (≤6 questions: the
28
+ * top-5 similar pairs each ask 「同一问题吗」, plus a record-only
29
+ * cluster-level 「值得动吗」). Only when ≥1 pair reaches the adopt band
30
+ * (≥0.50) does the cluster go to the LLM — an all-non-adopt cluster skips
31
+ * the model call entirely. Any jev failure fails open to today's behavior;
32
+ * the merge conclusion generation itself stays LLM.
26
33
  *
27
34
  * @module consolidate
28
35
  */
29
36
  import type { TopicsService } from './service.ts';
30
37
  import { type ModelCaller } from './distill.ts';
38
+ import { jevAsk } from './jev/client.ts';
31
39
  /** Clusters are capped — a giant cluster drowns the model in candidates. */
32
40
  export declare const MAX_CLUSTER_SIZE = 6;
33
41
  export interface ConsolidateOp {
@@ -79,10 +87,21 @@ export interface ClusterEntry {
79
87
  description?: string;
80
88
  conclusion: string;
81
89
  }
90
+ /** One similar pair inside a cluster — the jev prefilter's question units. */
91
+ export interface ClusterPair {
92
+ /** Indices into the cluster's `entries`. */
93
+ i: number;
94
+ j: number;
95
+ /** Pairwise Jaccard from clustering — REUSED for the prefilter's pair
96
+ * selection (top-k at ask time), never recomputed. */
97
+ similarity: number;
98
+ }
82
99
  export interface TopicCluster {
83
100
  entries: ClusterEntry[];
84
101
  /** Peak pairwise similarity inside the cluster — runs process peaks first. */
85
102
  similarity: number;
103
+ /** Similar pairs (edges ≥ threshold) inside the cluster, best first. */
104
+ pairs: ClusterPair[];
86
105
  }
87
106
  /**
88
107
  * Group entries whose title+description+tags tokens clear the pairwise
@@ -91,6 +110,9 @@ export interface TopicCluster {
91
110
  * {@link MAX_CLUSTER_SIZE} keep their most-connected members.
92
111
  */
93
112
  export declare function clusterTopics(entries: readonly ClusterEntry[], threshold?: number): TopicCluster[];
113
+ /** The jev seam — injectable so tests run hermetic (same pattern as
114
+ * ModelCaller: production never passes it, the real jevAsk is the default). */
115
+ export type JevAsker = typeof jevAsk;
94
116
  /**
95
117
  * Drop `deprecated` topics older than `ttlDays` — pure housekeeping, never
96
118
  * touches the model. `generated.at` is the right clock: every save restamps
@@ -105,7 +127,11 @@ export declare class Consolidator {
105
127
  private inFlight;
106
128
  private readonly service;
107
129
  private readonly caller;
108
- constructor(service: TopicsService, caller: ModelCaller | undefined);
130
+ private readonly askJev;
131
+ /** Best-effort host-logger sink (warn-style); undefined = stay silent,
132
+ * aligned with the lane's quiet discipline for success/no-clusters. */
133
+ private readonly note;
134
+ constructor(service: TopicsService, caller: ModelCaller | undefined, askJev?: JevAsker, note?: (message: string) => void);
109
135
  get configured(): boolean;
110
136
  /** Cadence days for a config value; undefined = off (or unrecognized). */
111
137
  static cadenceDays(value: string | undefined): number | undefined;
@@ -125,6 +151,18 @@ export declare class Consolidator {
125
151
  private runInner;
126
152
  /** Read the active pool and cluster it lexically. */
127
153
  private buildClusters;
154
+ /**
155
+ * One jev request per cluster (design 2026-09-25 §2 整理 lane 前置). The
156
+ * top-k similar pairs (k ≤ {@link PREFILTER_TOP_PAIRS}, edges reused from
157
+ * clustering) each get a pair noul question 「同一问题吗」; one record-only
158
+ * cluster-level 「值得动吗」 rides along (固定 band='record', never gates).
159
+ * Gate: ≥1 pair in the adopt band → the cluster goes to the LLM; all pairs
160
+ * below → skipped (省调用). Any jev failure proceeds fail-open — the
161
+ * call-layer telemetry row (outcome !== 'ok' marks the fail-open) was already written by jevAsk
162
+ * itself; only the verdict layer is ours here (agree='n/a', nothing lexical
163
+ * to reconcile against on this lane).
164
+ */
165
+ private prefilterCluster;
128
166
  /** One model call over one cluster, ops shape-validated but not yet applied. */
129
167
  private runCluster;
130
168
  /**