dsh-prime-memory 0.26.20 → 0.26.22

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # 更新日志(Changelog)
2
2
 
3
+ ## [0.26.22] — 2026-11-02
4
+
5
+ - **#27 真机通过**:用户在 desktop 上 `register(work/dsh-host)` → `list` 显示「**现有 59 条**」,与注册前自生长清单里的 `work/dsh-host(59)` 逐位一致;`?? 0` 分支也未被误伤(有绑定即显示真数)。
6
+ - **修掉这次验证顺带查出的口径缺陷**(`memory_room_admin` 的 `action=list`):`grownOnly` 原先用 `roomRegistry.list()`(**含 retired**)排除自生长项,而归类词表面(`room-review.ts`)用 `listActive()`。两侧口径不一致的后果是——**一个 slug 退役后,它既不在注册表活跃词表里、又被从「自生长未注册」清单排除,若它还挂着记录,那些记录的 Room 归属就在两个面上同时隐形**,且没有任何动作能把它找回来(工具面只有 `retire`,无 un-retire;恢复只能走 RPC `dsh-memory/room-admin` 的 `action=retire, active:true`,工具没暴露该参数)。现改为按 `activeSlugs` 排除,退役项回到自生长面继续可见可计数。
7
+ - **额度显示同步**:`注册表 N 条(上限 200)` 原先把 retired 条目也计入 N(占额度视觉位),现 N = 活跃条目数,并在存在退役项时追加「已退役 M 条(不占额度、可用面板「恢复」切回 active)」。退役条目本身仍列在明细里并标 `[source/retired]`,便于恢复。
8
+ - **测试**:新增 `tests/room-admin-list.test.ts` 3 例(注册项真计数 / 退役后两侧口径一致 / 零绑定仍显示 0)。**红绿已验**:把排除集退回修复前的 `registry.some(...)` 写法后 1 例转红,恢复即全绿。全量 **1901 例 / 190 文件**。
9
+ - **冒烟脚本去钉版**:`scripts/verify-desktop-smoke.mjs` 的版本断言从等值改为 `0.26.` 族前缀(上一版每发一次就得改脚本,是自我制造的维护债);同时清掉两个未使用变量使 lint 归零。
10
+
11
+ ## [0.26.21] — 2026-11-02
12
+
13
+ - **#20 追加修复:DRSC 读面从 HTTP 改为进程内直调**。桌面宿主在**任何**注册路由之前有一道 pre-dispatch 鉴权(`isAuthenticated` 只认由 `requestAudience(Host)` 派生的签名 cookie,豁免分支仅 `GET /?token=` 与已认证的 `GET /`),而插件服务端 `fetch` 既无 cookie 也无可取用 launch token 的服务面(该字段是宿主 `BrowserAuth` 私有)。真机实测确认:基址修对后请求确实发出、参数形状正确,但仍被回 `401` —— 即上一版只解决了「打错端口」,没解决「打不通」。本版新增 `src/clients/drsc-inproc.ts`,直调 collector 导出的只读查询面(`resolveIndexDir` → `openProjectionStore` → `createQueryContext` → `search` / `sessionDetail` / `listSessions`),全程不碰 `ProjectionSync` 写侧引擎,故不会与 collector 自身的同步器抢水位、也不会多挂一个 interval;索引库为 WAL(实测同目录有 `-wal`/`-shm`),宿主持写连接时本侧只读句柄可并发读,且句柄只开一次(常驻 summaries 是内存 Map,每请求重开等于重扫全表)。
14
+ - **传输分流规则**:只有「显式 HTTP 意图」才走 fetch —— 即配了 `baseUrl`(跨机/自托管 collector)或调用方注入 `fetchImpl`。两者皆未给时走进程内。四个读点仍汇聚在 `getJson`,查询串形状与旧 HTTP 逐字一致,因此字段映射、「`session=null` → 空数组」「limit 钳位」「`redact` 硬臂」这些口径全部不变;进程内读失败同样包成 `DrscClientError`,调用方的「显式报错 vs 空窗降级」分面处理不受影响。
15
+ - **真数据端到端自证**:以真实 collector 模块 + 真实索引库驱动,直调与走适配器的 `tail:4` / 全量两条臂返回**逐位一致的 seq 序列**(4 行 / 1770 行);`DrscClient.sessionTail` 得到 4 行且 `role/kind/time/text` 映射正常,`search('端口')` 命中 3 条含 snippet,`listSessions` total=103。新增 `tests/drsc-inproc.test.ts`(参数映射 + 句柄单次开启 + 载入失败不缓存 rejection 以自愈)与 `drsc-client.test.ts` 的传输分流用例,合计 12 例;全量 **1898 例 / 189 文件**。
16
+ - **过程中修掉两处自身缺陷**:① `{ limit: undefined }` 会让 `'limit' in opts` 为真,而 collector 可能据此走钳位分支 —— 改为非法/缺省时整个键缺席;② 适配器误把 `openProjectionStore` 的返回值丢弃、试图从模块上读一个不存在的属性取句柄 —— 已改回持有返回值。
17
+
3
18
  ## [0.26.20] — 2026-11-02
4
19
 
5
20
  - **内部命名与计划对齐**:共用取数助手按其计划原名 `mergeRoomCatalog()` 落地(0.26.18/0.26.19 线上产物里叫 `roomCountsBySlug()`,功能与形状不变,纯内部导出名,无外部消费方)。同时重写该函数的文档注释:原先「四处同形中三处已正确」的表述不准确,现按实证写明 rooms-export / room-admin list / 候选词表三处已接,而 rooms-get 端点除计数外还要把注册表 `source`/`label` 覆盖回已存在条目,属不同形状,有意不接(该处就地留了说明注释)。
@@ -0,0 +1,50 @@
1
+ /**
2
+ * DRSC 进程内读适配器(issue #20 的追加修复)。
3
+ *
4
+ * 为什么要换掉 HTTP:桌面宿主在**任何**注册路由之前有一道 pre-dispatch 鉴权
5
+ * (`isAuthenticated` 只认由 `requestAudience(Host)` 派生的签名 cookie,豁免分支
6
+ * 只有 `GET /?token=` 与已认证的 `GET /`)。插件服务端 `fetch` 没有 cookie,也没有
7
+ * 任何可取用该 token 的服务面(`launchToken` 是宿主 `BrowserAuth` 的私有字段),
8
+ * 于是基址修对了也只会拿到 401 —— 真机已实测(见 #20 评论)。
9
+ *
10
+ * 本模块改为直调 collector 导出的**只读查询面**:
11
+ * resolveIndexDir() → openProjectionStore(dir) → createQueryContext(store)
12
+ * → search / sessionDetail / listSessions
13
+ * 全程不碰 `ProjectionSync`(写侧引擎),因此不会与 collector 自己的同步器抢水位、
14
+ * 也不会多挂一个 interval。索引库是 WAL(实测同目录有 -wal/-shm),宿主持写连接时
15
+ * 本侧只读句柄可并发读。
16
+ *
17
+ * 包名用变量做动态 import:本仓 devDeps 没有该包,静态 import 会让 typecheck 挂;
18
+ * 运行期它由 profile 顶层 node_modules 提供(本插件已把它列为 dependency 与
19
+ * pluginDependencies,装载序在前,故首次读时必已就位)。
20
+ */
21
+ /** 只用到读面,形状按 collector 的 d.ts 手工声明,不引它的类型(避免跨包类型依赖)。 */
22
+ export interface CollectorReadModule {
23
+ resolveIndexDir: (explicit?: string, env?: NodeJS.ProcessEnv) => string;
24
+ openProjectionStore: (dir: string) => unknown;
25
+ createQueryContext: (store: unknown) => unknown;
26
+ search: (ctx: unknown, query: string, opts?: {
27
+ session?: string;
28
+ limit?: number;
29
+ redact?: boolean;
30
+ }) => Record<string, unknown>;
31
+ sessionDetail: (ctx: unknown, sessionId: string, opts?: {
32
+ turn?: string;
33
+ fields?: string;
34
+ tail?: number;
35
+ redact?: boolean;
36
+ }) => Record<string, unknown>;
37
+ listSessions: (ctx: unknown, offset?: number, limit?: number) => Record<string, unknown>;
38
+ }
39
+ /** 注入面(单测);缺省按裸名动态 import。 */
40
+ export type CollectorLoader = () => Promise<CollectorReadModule>;
41
+ /** 清掉缓存(单测用;也供 collector 换装后强制重开)。 */
42
+ export declare function resetCollectorReadState(): void;
43
+ /**
44
+ * 执行一次读,返回与 HTTP 包络同形的对象(`ok:true` + 各 op 的字段)。
45
+ *
46
+ * 保持同形是有意的:`DrscClient` 的四个读点全部经由 `getJson`,字段映射、
47
+ * 「session=null → 空数组」「limit 钳位」这些口径都已在那边测过;这里只负责
48
+ * 把传输从 fetch 换成函数调用,不顺手改任何语义。
49
+ */
50
+ export declare function callCollectorRead(query: string, load?: CollectorLoader): Promise<Record<string, unknown>>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * DRSC 进程内读适配器(issue #20 的追加修复)。
3
+ *
4
+ * 为什么要换掉 HTTP:桌面宿主在**任何**注册路由之前有一道 pre-dispatch 鉴权
5
+ * (`isAuthenticated` 只认由 `requestAudience(Host)` 派生的签名 cookie,豁免分支
6
+ * 只有 `GET /?token=` 与已认证的 `GET /`)。插件服务端 `fetch` 没有 cookie,也没有
7
+ * 任何可取用该 token 的服务面(`launchToken` 是宿主 `BrowserAuth` 的私有字段),
8
+ * 于是基址修对了也只会拿到 401 —— 真机已实测(见 #20 评论)。
9
+ *
10
+ * 本模块改为直调 collector 导出的**只读查询面**:
11
+ * resolveIndexDir() → openProjectionStore(dir) → createQueryContext(store)
12
+ * → search / sessionDetail / listSessions
13
+ * 全程不碰 `ProjectionSync`(写侧引擎),因此不会与 collector 自己的同步器抢水位、
14
+ * 也不会多挂一个 interval。索引库是 WAL(实测同目录有 -wal/-shm),宿主持写连接时
15
+ * 本侧只读句柄可并发读。
16
+ *
17
+ * 包名用变量做动态 import:本仓 devDeps 没有该包,静态 import 会让 typecheck 挂;
18
+ * 运行期它由 profile 顶层 node_modules 提供(本插件已把它列为 dependency 与
19
+ * pluginDependencies,装载序在前,故首次读时必已就位)。
20
+ */
21
+ /** collector 包入口(裸名,由 profile 顶层 node_modules 解析)。 */
22
+ const COLLECTOR_PACKAGE = 'dsh-reference-session-collector';
23
+ const defaultLoader = async () => (await import(/* @vite-ignore */ COLLECTOR_PACKAGE));
24
+ let modulePromise;
25
+ let queryContext;
26
+ /**
27
+ * 只读句柄(`openProjectionStore` 的返回值)。常驻 summaries 是内存 Map,
28
+ * 每请求重开等于重扫全表,故只开一次;undefined 是合法的未开哨兵
29
+ * (句柄真为 undefined 时 createQueryContext 会在首读即报错,不会静默重开)。
30
+ */
31
+ let storeHandle;
32
+ /** 清掉缓存(单测用;也供 collector 换装后强制重开)。 */
33
+ export function resetCollectorReadState() {
34
+ modulePromise = undefined;
35
+ queryContext = undefined;
36
+ storeHandle = undefined;
37
+ }
38
+ /** 载入并缓存 collector 模块;失败不缓存 rejection,否则一次抖动会永久打死后续所有读。 */
39
+ async function moduleOf(load) {
40
+ const pending = (modulePromise ??= load().catch((err) => {
41
+ if (modulePromise === pending)
42
+ modulePromise = undefined;
43
+ throw err;
44
+ }));
45
+ return pending;
46
+ }
47
+ async function contextFor(load) {
48
+ if (queryContext !== undefined)
49
+ return queryContext;
50
+ const mod = await moduleOf(load);
51
+ if (storeHandle === undefined) {
52
+ storeHandle = mod.openProjectionStore(mod.resolveIndexDir());
53
+ }
54
+ queryContext = mod.createQueryContext(storeHandle);
55
+ return queryContext;
56
+ }
57
+ /** 把 HTTP 查询串拆成参数表(与 collector 路由同源解析口径:它也只读 searchParams)。 */
58
+ function paramsOf(query) {
59
+ return new URLSearchParams(query.startsWith('?') ? query.slice(1) : query);
60
+ }
61
+ /**
62
+ * 执行一次读,返回与 HTTP 包络同形的对象(`ok:true` + 各 op 的字段)。
63
+ *
64
+ * 保持同形是有意的:`DrscClient` 的四个读点全部经由 `getJson`,字段映射、
65
+ * 「session=null → 空数组」「limit 钳位」这些口径都已在那边测过;这里只负责
66
+ * 把传输从 fetch 换成函数调用,不顺手改任何语义。
67
+ */
68
+ export async function callCollectorRead(query, load = defaultLoader) {
69
+ const ctx = await contextFor(load);
70
+ const mod = await moduleOf(load);
71
+ const p = paramsOf(query);
72
+ const op = p.get('op') ?? '';
73
+ // collector 的 redact 臂:HTTP 侧是 `redact=1`;缺省是原文臂。我们所有读点都硬要求脱敏。
74
+ const redact = p.get('redact') !== null && p.get('redact') !== '0';
75
+ if (op === 'search') {
76
+ const limit = positiveInt(p.get('limit'));
77
+ return {
78
+ ok: true,
79
+ ...mod.search(ctx, p.get('q') ?? '', {
80
+ // 非法/缺省时整个键缺席,而不是传 `limit: undefined` —— `'limit' in opts`
81
+ // 与 `opts.limit === undefined` 不是一回事,对端可能按前者走钳位分支。
82
+ ...(limit === undefined ? {} : { limit }),
83
+ redact,
84
+ }),
85
+ };
86
+ }
87
+ if (op === 'session') {
88
+ const sessionId = p.get('session');
89
+ if (sessionId === null || sessionId === '') {
90
+ return { ok: true, session: null, rows: [] };
91
+ }
92
+ const tail = positiveInt(p.get('tail'));
93
+ const detail = mod.sessionDetail(ctx, sessionId, {
94
+ // collector 侧:tail 为正整数才切尾窗,键缺席即全量(#14-B 的整会话臂依赖这点)。
95
+ ...(tail === undefined ? {} : { tail }),
96
+ redact,
97
+ });
98
+ return { ok: true, ...detail };
99
+ }
100
+ if (op === 'sessions') {
101
+ // 这两个是位置参数,undefined 与“没传”在 JS 里同义,无需上面对象字面量那套处置。
102
+ const offset = positiveInt(p.get('offset')) ?? 0;
103
+ const limit = positiveInt(p.get('limit'));
104
+ return { ok: true, ...mod.listSessions(ctx, offset, limit) };
105
+ }
106
+ throw new Error(`collector 未知 op: ${op === '' ? '(missing)' : op}`);
107
+ }
108
+ /** 只接受「正整数」形式的参数;非法/缺省一律 undefined(交由调用点决定缺席语义)。 */
109
+ function positiveInt(raw) {
110
+ if (raw === null || raw.trim() === '')
111
+ return undefined;
112
+ const n = Number(raw);
113
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
114
+ }
@@ -1,30 +1,3 @@
1
- /**
2
- * DrscClient(T8.4 读切,drsc-redact-memory-wiring task_18):memory 读历史
3
- * 面的 dsh-reference-session-collector HTTP 客户端。
4
- *
5
- * 两个读点(R7):
6
- * - 蒸馏背景窗(recentBySession 等价供给)→ `op=session&tail=N&redact=1`
7
- * (collector task_11 尾窗升序 + task_12 redact 臂);
8
- * - 工具面 conversation_search → `op=search&redact=1`(命中基于原文 fts,
9
- * 返回 snippet 取脱敏锚定文本)。
10
- *
11
- * 加上 issue #14-B 的**重建事实源**:`op=sessions` 枚举 + `op=session`(**不带 tail**)
12
- * 逐会话拉全量——对全量重建来说“尾窗”不够用,而 collector 侧不带 tail 即 `ORDER BY seq ASC`
13
- * 无 LIMIT。此路径同样恒带 `redact=1`。
14
- *
15
- * 硬依赖纪律(对齐 T7.3 裁定):collector 缺席/超时/非 2xx/ok=false 一律抛
16
- * {@link DrscClientError}——**绝不回退本地 l0**(表已退役,无退路);调用方
17
- * 分口径处理:蒸馏背景窗空窗降级(runner),工具面显式报错(tools)。
18
- *
19
- * 同源 Host 解析(steward drs-source 的 resolveDrsBaseUrl 同款):
20
- * 显式 baseUrl > 首个 trustedHosts 条目(补 http://)> 宿主实际监听 origin
21
- * (issue #20:Desktop App 走随机端口,写死 8080 永远对不上)> `http://127.0.0.1:8080`
22
- * (宿主缺省口)。Host 头**只回显被选作基址的那个 trustedHosts 条目**:collector
23
- * 信任门是精确匹配,条目省略端口而 URL 带端口会失配——所以基址来自 origin 兜底时
24
- * 索性不回显(等价于旧版 trustedHosts 为空时的形状),而不是拿另一个 authority 去撞门。
25
- * trustedHosts / origin 均惰性读取(webStartup、webServer 可能晚于本插件就绪),
26
- * 每次请求现算,无缓存陈旧面。
27
- */
28
1
  import type { ConversationMessage } from '../types.js';
29
2
  /** collector API 路径(与其 DRS_API_PATH 字面一致;不跨包 import)。 */
30
3
  export declare const DRSC_API_PATH = "/api/dsh-reference-session-collector";
@@ -105,8 +78,17 @@ export interface DrscClientOptions {
105
78
  * 下线,故与 trustedHosts 同款每次现取;取不到即返回 undefined → 落回 8080 老行为。
106
79
  */
107
80
  origin?: () => string | undefined;
108
- /** 注入面(单测);缺省全局 fetch。 */
81
+ /**
82
+ * 注入面(单测);缺省全局 fetch。**显式传入即表示要走 HTTP**(见 getJson 分流)。
83
+ */
109
84
  fetchImpl?: typeof fetch;
85
+ /**
86
+ * 进程内读面(issue #20 追加修复,缺省 {@link callCollectorRead}):直调
87
+ * collector 导出的只读查询函数。宿主对所有注册路由有 pre-dispatch 鉴权
88
+ * (插件服务端无 cookie 可用,真机实测 401),HTTP 路在桌面宿主上走不通;
89
+ * 只有显式给了 `baseUrl`(跨机/外部 collector)时才保留 HTTP 退路。
90
+ */
91
+ read?: (query: string) => Promise<Record<string, unknown>>;
110
92
  /** 单请求超时 ms;缺省 {@link DRSC_DEFAULT_TIMEOUT_MS}。 */
111
93
  timeoutMs?: number;
112
94
  }
@@ -119,6 +101,11 @@ export declare class DrscClient {
119
101
  private base;
120
102
  /** origin 探针失败(服务未就绪/属性读抛)不得影响请求:视作取不到,落老行为。 */
121
103
  private safeOrigin;
104
+ /**
105
+ * 一次读:优先进程内直调,失败/缺席一律包成 DrscClientError(与 HTTP 路同口径,
106
+ * 调用方的「显式报错 vs 空窗降级」分面处理不变)。
107
+ */
108
+ private readInproc;
122
109
  /** 一个 GET 的 JSON 包络(非 2xx / ok!==true / 超时 / 断网一律抛 DrscClientError)。 */
123
110
  private getJson;
124
111
  /**
@@ -1,3 +1,31 @@
1
+ /**
2
+ * DrscClient(T8.4 读切,drsc-redact-memory-wiring task_18):memory 读历史
3
+ * 面的 dsh-reference-session-collector HTTP 客户端。
4
+ *
5
+ * 两个读点(R7):
6
+ * - 蒸馏背景窗(recentBySession 等价供给)→ `op=session&tail=N&redact=1`
7
+ * (collector task_11 尾窗升序 + task_12 redact 臂);
8
+ * - 工具面 conversation_search → `op=search&redact=1`(命中基于原文 fts,
9
+ * 返回 snippet 取脱敏锚定文本)。
10
+ *
11
+ * 加上 issue #14-B 的**重建事实源**:`op=sessions` 枚举 + `op=session`(**不带 tail**)
12
+ * 逐会话拉全量——对全量重建来说“尾窗”不够用,而 collector 侧不带 tail 即 `ORDER BY seq ASC`
13
+ * 无 LIMIT。此路径同样恒带 `redact=1`。
14
+ *
15
+ * 硬依赖纪律(对齐 T7.3 裁定):collector 缺席/超时/非 2xx/ok=false 一律抛
16
+ * {@link DrscClientError}——**绝不回退本地 l0**(表已退役,无退路);调用方
17
+ * 分口径处理:蒸馏背景窗空窗降级(runner),工具面显式报错(tools)。
18
+ *
19
+ * 同源 Host 解析(steward drs-source 的 resolveDrsBaseUrl 同款):
20
+ * 显式 baseUrl > 首个 trustedHosts 条目(补 http://)> 宿主实际监听 origin
21
+ * (issue #20:Desktop App 走随机端口,写死 8080 永远对不上)> `http://127.0.0.1:8080`
22
+ * (宿主缺省口)。Host 头**只回显被选作基址的那个 trustedHosts 条目**:collector
23
+ * 信任门是精确匹配,条目省略端口而 URL 带端口会失配——所以基址来自 origin 兜底时
24
+ * 索性不回显(等价于旧版 trustedHosts 为空时的形状),而不是拿另一个 authority 去撞门。
25
+ * trustedHosts / origin 均惰性读取(webStartup、webServer 可能晚于本插件就绪),
26
+ * 每次请求现算,无缓存陈旧面。
27
+ */
28
+ import { callCollectorRead } from './drsc-inproc.js';
1
29
  /** collector API 路径(与其 DRS_API_PATH 字面一致;不跨包 import)。 */
2
30
  export const DRSC_API_PATH = '/api/dsh-reference-session-collector';
3
31
  /** 客户端缺省超时:loopback 正常 <10ms,5s 是给冷启动 GC/断点的天花板。 */
@@ -125,8 +153,36 @@ export class DrscClient {
125
153
  return undefined;
126
154
  }
127
155
  }
156
+ /**
157
+ * 一次读:优先进程内直调,失败/缺席一律包成 DrscClientError(与 HTTP 路同口径,
158
+ * 调用方的「显式报错 vs 空窗降级」分面处理不变)。
159
+ */
160
+ async readInproc(query) {
161
+ const read = this.options.read ?? callCollectorRead;
162
+ try {
163
+ const body = await read(query);
164
+ // body 已是 Record<string, unknown>,不必也不应再转 DrscEnvelope(TS2352)。
165
+ if (body === null || typeof body !== 'object' || body.ok !== true) {
166
+ throw new Error('返回包络缺 ok:true');
167
+ }
168
+ return body;
169
+ }
170
+ catch (err) {
171
+ if (err instanceof DrscClientError)
172
+ throw err;
173
+ throw new DrscClientError(`collector 进程内读失败(${query}): ${err instanceof Error ? err.message : String(err)}`, {
174
+ cause: err,
175
+ });
176
+ }
177
+ }
128
178
  /** 一个 GET 的 JSON 包络(非 2xx / ok!==true / 超时 / 断网一律抛 DrscClientError)。 */
129
179
  async getJson(query) {
180
+ // 分流规则:只有「显式 HTTP 意图」才走 fetch —— 即配了 baseUrl(跨机/自托管
181
+ // collector)或调用方自己注入了 fetchImpl(单测面)。两者都没给时走进程内:
182
+ // 桌面宿主上插件服务端的 HTTP 必被 pre-dispatch 鉴权拦成 401(#20 真机实证)。
183
+ const explicit = this.options.baseUrl?.trim() ?? '';
184
+ if (explicit === '' && this.options.fetchImpl === undefined)
185
+ return this.readInproc(query);
130
186
  const { url, hostHeader } = this.base();
131
187
  let response;
132
188
  try {
@@ -390,9 +390,17 @@ graphWrite) {
390
390
  // 线性 find,上限 200 条注册表 × N 个 Room——语义相同、形状不同就是分叉位。
391
391
  const countOf = mergeRoomCatalog(counts);
392
392
  const regLines = registry.map((e) => `- ${e.slug}${e.label ? `(${e.label})` : ''} [${e.source}/${e.status}] 现有 ${countOf.get(e.slug) ?? 0} 条${e.description ? ` — ${e.description}` : ''}`);
393
- const grownOnly = counts.filter((r) => !registry.some((e) => e.slug === r.room));
393
+ // 口径必须跟词表一致(`room-review.ts` 用 listActive):已退役条目不再进归类词表,
394
+ // 那它的 slug 就应回到「自生长未注册」面。原先用全量 registry 排除,使退役后的
395
+ // slug 两边都不显示(带记录时那些记录在词表上隐形)——退役不可逆地捐掉可见性。
396
+ const activeSlugs = new Set(registry.filter((e) => e.status === 'active').map((e) => e.slug));
397
+ const grownOnly = counts.filter((r) => !activeSlugs.has(r.room));
398
+ const activeCount = activeSlugs.size;
399
+ const retiredCount = registry.length - activeCount;
394
400
  return {
395
- notice: `注册表 ${registry.length} 条(上限 200),自生长未注册 ${grownOnly.length} 个。\n` +
401
+ notice:
402
+ // 额度与词表同步:上限 200 算的是活跃条目,退役项不占额度(另计)。
403
+ `注册表 ${activeCount} 条(上限 200)${retiredCount > 0 ? `,已退役 ${retiredCount} 条(不占额度、可用面板「恢复」切回 active)` : ''},自生长未注册 ${grownOnly.length} 个。\n` +
396
404
  (regLines.join('\n') || '(注册表为空——用 action=register 预注册)') +
397
405
  `\n自生长未注册:${grownOnly.map((r) => `${r.room}(${r.count})`).join(", ") || "无"}`,
398
406
  rooms: `${registry.length}+${grownOnly.length}`,
package/dsh.plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dsh-prime-memory",
3
3
  "name": "dsh-prime-memory",
4
- "version": "0.26.20",
4
+ "version": "0.26.22",
5
5
  "description": {
6
6
  "en": "Layered distilled memory plugin (L0 capture → L1 atomic memory → L2 scenes → L3 persona) with automatic recall injection before each model step.",
7
7
  "zh": "L0~L3 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-prime-memory",
3
- "version": "0.26.20",
3
+ "version": "0.26.22",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public",