dsh-reference-session-collector 0.2.0 → 0.2.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.de.md CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-reference-session-collector (DRS) — Sitzungs-Projektionssynchronisierer
1
+ # dsh-reference-session-collector (DRSC) — Sitzungs-Projektionssynchronisierer
2
2
 
3
3
  **DeepSeek-Harness(DSH)-Plugin**: Liest die Sitzungsprotokolle unter
4
4
  `~/.dsh/sessions/**` **nur lesend**, dekodiert sie und projiziert sie in eine
@@ -50,7 +50,7 @@ Memory-Anker-Rückfüllung) und Effizienz-Benchmark T9.1.
50
50
  `~/.dsh-reference-session-collector/projection.db`;
51
51
  - pro Sitzung vollständiger Neu-Schreib-Ersatz (DELETE+INSERT in einer
52
52
  Transaktion), idempotent;
53
- - **Maskierung liegt nicht bei DRS**: Rohtext-Speicherung,
53
+ - **Maskierung liegt nicht bei DRSC**: Rohtext-Speicherung,
54
54
  Expositionsfläche = die Sitzungsdateien selbst.
55
55
 
56
56
  ## Umgebungsvariablen
package/README.en.md CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-reference-session-collector (DRS)
1
+ # dsh-reference-session-collector (DRSC)
2
2
 
3
3
  **Session reference projection synchronizer** — a DeepSeek Harness (DSH) plugin.
4
4
 
@@ -9,8 +9,8 @@ replacement markers, recall pointers, and an FTS5 full-text index — so context
9
9
  compression (cci), the session steward, and prime-memory all read the same
10
10
  projection instead of each decoding logs on their own.
11
11
 
12
- > Naming: `dsh-reference-session-collector` abbreviates to **DRS**, which is also the
13
- > first three letters of drscrewdriver.
12
+ > Naming: `dsh-reference-session-collector` abbreviates to **DRSC** — the initials of
13
+ > dsh-**R**eference-**S**ession-**C**ollector.
14
14
 
15
15
  ## Status
16
16
 
@@ -69,7 +69,7 @@ missing (fail-soft).
69
69
  and it has no write counterpart — ever. The plugin writes only to
70
70
  `~/.dsh-reference-session-collector/projection.db`. That database holds **unsanitized
71
71
  original text**; its exposure equals the session files themselves. Any
72
- "sanitized corpus" requirement belongs to consumers, not DRS.
72
+ "sanitized corpus" requirement belongs to consumers, not DRSC.
73
73
 
74
74
  ## Configuration (v0)
75
75
 
@@ -83,16 +83,16 @@ No settings face yet (lands with phase-2 wiring). Environment variables:
83
83
 
84
84
  ## Roadmap
85
85
 
86
- 1. **Phase 1 · cci reconciliation**: monitor/verify gains a DRS recall column —
86
+ 1. **Phase 1 · cci reconciliation**: monitor/verify gains a DRSC recall column —
87
87
  claims vs formula vs projection, three-column accounting (read-only HTTP).
88
88
  2. **Phase 2 · steward corpus switch**: the steward's inlined search engine
89
- reads DRS (segments.time/on_surface/search_text + last_time_ms/archived
89
+ reads DRSC (segments.time/on_surface/search_text + last_time_ms/archived
90
90
  cover its whole hit face; field mapping in
91
91
  `.agents/plans/drs-start/findings.md`).
92
92
  3. **Phase 3 · memory history projection (read-only)**: the l0 write path does
93
93
  **not** move (sanitization/redaction/record_id ownership stays in memory's
94
94
  capture side; it is semantically incompatible with full-session rewrite).
95
- DRS only serves historical anchor backfill and corpus. Vector layer
95
+ DRSC only serves historical anchor backfill and corpus. Vector layer
96
96
  (BGE-M3 int8) is a separate optional batch job.
97
97
 
98
98
  ## Development
package/README.es.md CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-reference-session-collector (DRS) — sincronizador de proyección de sesiones
1
+ # dsh-reference-session-collector (DRSC) — sincronizador de proyección de sesiones
2
2
 
3
3
  **Complemento de DeepSeek Harness (DSH)**: escanea **solo en lectura** los
4
4
  registros de sesión en `~/.dsh/sessions/**`, los decodifica y los proyecta
@@ -49,7 +49,7 @@ fase 3 relleno de anclas de memory) y banco de pruebas de eficiencia T9.1.
49
49
  `~/.dsh-reference-session-collector/projection.db`;
50
50
  - reemplazo de reescritura completa por sesión (DELETE+INSERT en una
51
51
  transacción), idempotente;
52
- - **la desensibilización no es responsabilidad de DRS**: almacena texto
52
+ - **la desensibilización no es responsabilidad de DRSC**: almacena texto
53
53
  original; la superficie de exposición son los propios archivos de sesión.
54
54
 
55
55
  ## Variables de entorno
package/README.fr.md CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-reference-session-collector (DRS) — Synchroniseur de projection de sessions
1
+ # dsh-reference-session-collector (DRSC) — Synchroniseur de projection de sessions
2
2
 
3
3
  **Plugin DeepSeek Harness (DSH)** : balaie les journaux de session sous
4
4
  `~/.dsh/sessions/**` en **lecture seule**, les décode et les projette dans
@@ -49,7 +49,7 @@ d'efficacité T9.1.
49
49
  `~/.dsh-reference-session-collector/projection.db` ;
50
50
  - remplacement réécriture-complète par session (DELETE+INSERT dans une
51
51
  transaction), idempotent ;
52
- - **l'anonymisation n'est pas la responsabilité de DRS** : texte brut
52
+ - **l'anonymisation n'est pas la responsabilité de DRSC** : texte brut
53
53
  stocké, surface d'exposition = les fichiers de session eux-mêmes.
54
54
 
55
55
  ## Variables d'environnement
package/README.it.md CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-reference-session-collector (DRS) — sincronizzatore di proiezione delle sessioni
1
+ # dsh-reference-session-collector (DRSC) — sincronizzatore di proiezione delle sessioni
2
2
 
3
3
  **Plugin DeepSeek Harness (DSH)**: scansisce **in sola lettura** i log di
4
4
  sessione in `~/.dsh/sessions/**`, li decodifica e li proietta in una
@@ -49,7 +49,7 @@ steward, fase 3 riempimento ancore memory) e benchmark di efficienza T9.1.
49
49
  `~/.dsh-reference-session-collector/projection.db`;
50
50
  - sostituzione con riscrittura completa per sessione (DELETE+INSERT in una
51
51
  transazione), idempotente;
52
- - **la sanitizzazione non è responsabilità di DRS**: memorizza testo grezzo,
52
+ - **la sanitizzazione non è responsabilità di DRSC**: memorizza testo grezzo,
53
53
  superficie di esposizione = i file di sessione stessi.
54
54
 
55
55
  ## Variabili d'ambiente
package/README.ja.md CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-reference-session-collector (DRS) — セッション原文プロジェクション同期器
1
+ # dsh-reference-session-collector (DRSC) — セッション原文プロジェクション同期器
2
2
 
3
3
  **DeepSeek Harness (DSH) プラグイン**: `~/.dsh/sessions/**` のセッション
4
4
  ログを**読み取り専用**で走査し、復号後に独自 SQLite プロジェクション
@@ -44,7 +44,7 @@ HTTP ルート。ゲートチェーン `pnpm check` 全緑(9スイート / 66
44
44
  - セッションファイルと `workspace.json` は常時読み取り専用;
45
45
  - 本プラグインの全書き込みは `~/.dsh-reference-session-collector/projection.db` にのみ;
46
46
  - セッション単位の全量書換式置換(トランザクション内 DELETE+INSERT)、冪等;
47
- - **マスキング責任は DRS にない**: 原文を保存、露出面=セッションファイル自体。
47
+ - **マスキング責任は DRSC にない**: 原文を保存、露出面=セッションファイル自体。
48
48
 
49
49
  ## 環境変数
50
50
 
package/README.ko.md CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-reference-session-collector (DRS) — 세션 원문 투영 동기화기
1
+ # dsh-reference-session-collector (DRSC) — 세션 원문 투영 동기화기
2
2
 
3
3
  **DeepSeek Harness(DSH) 플러그인**: `~/.dsh/sessions/**` 세션 로그를
4
4
  **읽기 전용**으로 스캔하여 디코딩 후 자체 SQLite 투영
@@ -44,7 +44,7 @@
44
44
  - 세션 파일과 `workspace.json`은 상시 읽기 전용;
45
45
  - 모든 기록은 `~/.dsh-reference-session-collector/projection.db`에만;
46
46
  - 세션 단위 전체 재작성 치환(트랜잭션 내 DELETE+INSERT), 멱등;
47
- - **마스킹 책임은 DRS에 없음**: 원문 저장, 노출면=세션 파일 자체.
47
+ - **마스킹 책임은 DRSC에 없음**: 원문 저장, 노출면=세션 파일 자체.
48
48
 
49
49
  ## 환경 변수
50
50
 
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
- # dsh-reference-session-collector (DRS) — 会话原文投影同步器
1
+ # dsh-reference-session-collector (DRSC) — 会话原文投影同步器
2
2
 
3
3
  > 只读扫描 `~/.dsh/sessions/**` 会话日志,解码后投影进插件自有 SQLite
4
4
  > (segments / replaces / FTS5),经单条 fenced HTTP 路由对外提供
5
5
  > surface 标记、召回对账与中文可用的内容检索面。**绝不写会话文件。**
6
6
 
7
- # Session Reference Projector (DRS) — English summary
7
+ # Session Reference Projector (DRSC) — English summary
8
8
 
9
- DRS is a read-only projector over the host's session logs. It walks
9
+ DRSC is a read-only projector over the host's session logs. It walks
10
10
  `~/.dsh/sessions/**`, decodes zstd/teiled-frame logs with a vendored leaf
11
11
  decoder, and rewrites per-session projections into
12
12
  `~/.dsh-reference-session-collector/projection.db` (SQLite via `node:sqlite`, four
@@ -15,12 +15,12 @@ tables + one FTS5 virtual table). A single fenced HTTP route
15
15
  per-session segment windows, the surface ledger, recall/compress pair
16
16
  accounting and full-text search. Token counts share the exact same
17
17
  DeepSeek-V4 tokenizer and formula as CCI's `verify`, falling back to a
18
- labelled `chars/4` basis when assets are absent. DRS never writes to any
18
+ labelled `chars/4` basis when assets are absent. DRSC never writes to any
19
19
  session file; its only writes land in its own data directory.
20
20
 
21
21
  ## 定位 / Positioning
22
22
 
23
- | 消费方 | 用 DRS 做什么 | 分期 |
23
+ | 消费方 | 用 DRSC 做什么 | 分期 |
24
24
  |---|---|---|
25
25
  | CCI(context-compression) | surface 标记(compressed/surface_text/replaced_by)、召回对账(Σb 回吐、Σ(a−b) 毛节省)、verify 免全量解码 | 一期 |
26
26
  | steward(会话管家) | 内容搜索语料源切换(search_text/time/on_surface 检索面) | 二期 |
@@ -68,7 +68,7 @@ session file; its only writes land in its own data directory.
68
68
  - 会话文件(`~/.dsh/sessions/**`)与 `workspace.json` 一律只读;
69
69
  - 插件的全部写入落在 `~/.dsh-reference-session-collector/projection.db`;
70
70
  - 每 session 全量重写式替换(事务内 DELETE+INSERT),幂等可重放;
71
- - **脱敏责任不在 DRS**:projection.db 存原文,泄露面=会话文件本身。
71
+ - **脱敏责任不在 DRSC**:projection.db 存原文,泄露面=会话文件本身。
72
72
 
73
73
  ## 分期路线
74
74
 
package/README.ru.md CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-reference-session-collector (DRS) — синхронизатор проекции сессий
1
+ # dsh-reference-session-collector (DRSC) — синхронизатор проекции сессий
2
2
 
3
3
  **Плагин DeepSeek Harness (DSH)**: сканирует **только на чтение** журналы
4
4
  сессий в `~/.dsh/sessions/**`, декодирует их и проецирует в собственную
@@ -48,7 +48,7 @@ SQLite-проекцию (segments / replaces / FTS5). Одна защищённ
48
48
  - все записи плагина идут исключительно в
49
49
  `~/.dsh-reference-session-collector/projection.db`;
50
50
  - полная перезапись-замена на сессию (DELETE+INSERT в транзакции), идемпотентно;
51
- - **ответственность за обезличивание не на DRS**: хранится исходный текст,
51
+ - **ответственность за обезличивание не на DRSC**: хранится исходный текст,
52
52
  площадь утечки = сами файлы сессий.
53
53
 
54
54
  ## Переменные окружения
package/dsh.plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dsh-reference-session-collector",
3
3
  "name": "dsh-reference-session-collector",
4
- "version": "0.2.0-beta.0",
4
+ "version": "0.2.2",
5
5
  "description": {
6
6
  "en": "Session reference projector (DRS): read-only scan of ~/.dsh/sessions logs, decoded into a private SQLite (segments/replaces/FTS5) with surface flags, recall-pair accounting and a Chinese-capable search face, served via /api/dsh-reference-session-collector. Never writes session files.",
7
7
  "zh": "会话原文投影同步器(DRS):只读扫描 ~/.dsh/sessions 会话日志,解码后投影进自有 SQLite(segments/replaces/FTS5),经 /api/dsh-reference-session-collector 提供 surface 标记、召回对账与中文可用的内容检索面。绝不写会话文件。"
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "license": "MIT",
15
15
  "engines": {
16
- "dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2"
16
+ "dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3"
17
17
  },
18
18
  "components": {
19
19
  "host": "lib/index.js",
package/lib/index.d.ts CHANGED
@@ -690,12 +690,14 @@ interface DrsHandlerDeps {
690
690
  trustedHosts?: readonly string[];
691
691
  }
692
692
  /**
693
- * op 分发:GET `?op=…`;POST `{op:'sync'}` 触发后台同步。
693
+ * op 分发:GET `?op=…`;POST `?op=sync` 触发后台同步。
694
+ * POST 的 op 只从 **query** 读(`{op:'sync'}` 落进 body 会 400 unknown POST op)。
694
695
  * 未知 op / 缺参 → 400(不留空壳)。
695
696
  */
696
697
  declare function handleDrsRequest(deps: DrsHandlerDeps, req: IncomingMessage, res: ServerResponse): Promise<void>;
697
698
  /**
698
- * 插件主体:开库 → 装配 sync(启动延迟首轮 + interval 轮)→ 双通道注册路由。
699
+ * 插件主体:apply() 立即返回,重活(开库/建 sync/注册路由)推迟到 setImmediate。
700
+ * 宿主插件装载串行同步——apply 不返回则整列插件都装不上,前端一直 Loading plugins…。
699
701
  * @param ctx - host 插件上下文(webServer 声明注入;webStartup/webRuntime 回退臂)。
700
702
  */
701
703
  declare function apply(ctx: Context): void;
package/lib/index.js CHANGED
@@ -2234,7 +2234,8 @@ function parseTail(url) {
2234
2234
  return Math.floor(parsed);
2235
2235
  }
2236
2236
  /**
2237
- * op 分发:GET `?op=…`;POST `{op:'sync'}` 触发后台同步。
2237
+ * op 分发:GET `?op=…`;POST `?op=sync` 触发后台同步。
2238
+ * POST 的 op 只从 **query** 读(`{op:'sync'}` 落进 body 会 400 unknown POST op)。
2238
2239
  * 未知 op / 缺参 → 400(不留空壳)。
2239
2240
  */
2240
2241
  async function handleDrsRequest(deps, req, res) {
@@ -2245,6 +2246,13 @@ async function handleDrsRequest(deps, req, res) {
2245
2246
  });
2246
2247
  return;
2247
2248
  }
2249
+ if (deps.query === void 0) {
2250
+ writeJson(res, 503, {
2251
+ ok: false,
2252
+ error: "initializing"
2253
+ });
2254
+ return;
2255
+ }
2248
2256
  let url;
2249
2257
  try {
2250
2258
  url = new URL(req.url ?? "/api/dsh-reference-session-collector", "http://localhost");
@@ -2277,7 +2285,8 @@ async function handleDrsRequest(deps, req, res) {
2277
2285
  case "status":
2278
2286
  writeJson(res, 200, {
2279
2287
  ok: true,
2280
- ...status(deps.query, deps.sync.lastSyncResult ?? null)
2288
+ ...status(deps.query, deps.sync.lastSyncResult ?? null),
2289
+ syncing: deps.sync.isSyncing
2281
2290
  });
2282
2291
  return;
2283
2292
  case "sessions": {
@@ -2428,7 +2437,8 @@ function safeReadService(ctx, name) {
2428
2437
  }
2429
2438
  }
2430
2439
  /**
2431
- * 插件主体:开库 → 装配 sync(启动延迟首轮 + interval 轮)→ 双通道注册路由。
2440
+ * 插件主体:apply() 立即返回,重活(开库/建 sync/注册路由)推迟到 setImmediate。
2441
+ * 宿主插件装载串行同步——apply 不返回则整列插件都装不上,前端一直 Loading plugins…。
2432
2442
  * @param ctx - host 插件上下文(webServer 声明注入;webStartup/webRuntime 回退臂)。
2433
2443
  */
2434
2444
  function apply(ctx) {
@@ -2440,43 +2450,12 @@ function apply(ctx) {
2440
2450
  else console[level]("[dsh-reference-session-collector]", message, ...args);
2441
2451
  } catch {}
2442
2452
  };
2443
- const storeDir = resolveIndexDir();
2444
2453
  let store;
2445
2454
  let query;
2446
- try {
2447
- store = openProjectionStore(storeDir);
2448
- query = createQueryContext(store);
2449
- } catch (error) {
2450
- log("warn", "projection store 不可用(node:sqlite 缺席/目录不可写):插件停用—— %s", String(error?.message ?? error));
2451
- return;
2452
- }
2453
- const sync = new ProjectionSync({
2454
- store: createStorePort(store),
2455
- project: (events, options) => projectSessionEvents(events, {
2456
- dirId: options.dirId,
2457
- ...options.header === void 0 ? {} : { header: options.header },
2458
- ...options.exactCounter === void 0 ? {} : { exactCounter: (text) => {
2459
- const count = options.exactCounter?.(text);
2460
- return count !== void 0 && count.kind === "exact-tokenizer" ? count.tokens : Math.ceil(text.length / 4);
2461
- } }
2462
- }),
2463
- log: (level, message, ...args) => log(level, message, ...args)
2464
- });
2455
+ let sync;
2465
2456
  let bootTimer;
2466
2457
  let disposeInterval = () => {};
2467
- ctx.effect(() => {
2468
- bootTimer = setTimeout(() => {
2469
- sync.syncAll().then((result) => {
2470
- log("info", "boot sync done: updated=%d skipped=%d basis=%s %dms", result.updated, result.skipped, result.basis, result.durationMs);
2471
- }).catch((error) => log("warn", "boot sync failed: %s", String(error?.message ?? error)));
2472
- }, BOOT_SYNC_DELAY_MS);
2473
- disposeInterval = startIntervalSync(sync);
2474
- return () => {
2475
- if (bootTimer !== void 0) clearTimeout(bootTimer);
2476
- disposeInterval();
2477
- store.close();
2478
- };
2479
- }, "dsh-reference-session-collector: projection lifecycle");
2458
+ let registered = false;
2480
2459
  const readStartupFace = () => {
2481
2460
  const direct = safeReadService(ctx, "webStartup") ?? safeReadService(ctx, "webRuntime");
2482
2461
  return direct !== null && typeof direct === "object" ? direct : void 0;
@@ -2489,7 +2468,6 @@ function apply(ctx) {
2489
2468
  ...startup?.trustedHosts !== void 0 ? { trustedHosts: startup.trustedHosts } : {}
2490
2469
  };
2491
2470
  };
2492
- let registered = false;
2493
2471
  const register = (webServer, channel) => {
2494
2472
  if (registered) return;
2495
2473
  try {
@@ -2510,24 +2488,58 @@ function apply(ctx) {
2510
2488
  log("warn", "drs api route registration failed (%s): %s", channel, String(error?.message ?? error));
2511
2489
  }
2512
2490
  };
2513
- const readService = (name) => safeReadService(ctx, name);
2514
- const active = asWebServer(readService("webServer"));
2515
- if (active !== void 0) {
2516
- register(active, "direct");
2517
- if (registered) return;
2518
- }
2519
- try {
2520
- ctx.inject(["webServer"], (injected) => {
2521
- const webServer = asWebServer(injected.webServer);
2522
- if (webServer === void 0) {
2523
- log("warn", "webServer exposes no register() — drs api route not registered (projection sync keeps running)");
2524
- return;
2491
+ ctx.effect(() => {
2492
+ return () => {
2493
+ if (bootTimer !== void 0) clearTimeout(bootTimer);
2494
+ disposeInterval();
2495
+ store?.close();
2496
+ };
2497
+ }, "dsh-reference-session-collector: projection lifecycle");
2498
+ setImmediate(() => {
2499
+ try {
2500
+ store = openProjectionStore(resolveIndexDir());
2501
+ query = createQueryContext(store);
2502
+ sync = new ProjectionSync({
2503
+ store: createStorePort(store),
2504
+ project: (events, options) => projectSessionEvents(events, {
2505
+ dirId: options.dirId,
2506
+ ...options.header === void 0 ? {} : { header: options.header },
2507
+ ...options.exactCounter === void 0 ? {} : { exactCounter: (text) => {
2508
+ const count = options.exactCounter?.(text);
2509
+ return count !== void 0 && count.kind === "exact-tokenizer" ? count.tokens : Math.ceil(text.length / 4);
2510
+ } }
2511
+ }),
2512
+ log: (level, message, ...args) => log(level, message, ...args)
2513
+ });
2514
+ bootTimer = setTimeout(() => {
2515
+ sync.syncAll().then((result) => {
2516
+ log("info", "boot sync done: updated=%d skipped=%d basis=%s %dms", result.updated, result.skipped, result.basis, result.durationMs);
2517
+ }).catch((error) => log("warn", "boot sync failed: %s", String(error?.message ?? error)));
2518
+ }, BOOT_SYNC_DELAY_MS);
2519
+ disposeInterval = startIntervalSync(sync);
2520
+ const readService = (name) => safeReadService(ctx, name);
2521
+ const active = asWebServer(readService("webServer"));
2522
+ if (active !== void 0) {
2523
+ register(active, "direct");
2524
+ if (registered) return;
2525
2525
  }
2526
- register(webServer, "inject");
2527
- });
2528
- } catch (error) {
2529
- log("warn", "webServer inject arm unavailable: %s — HTTP 面缺席,同步本体照常", String(error?.message ?? error));
2530
- }
2526
+ try {
2527
+ ctx.inject(["webServer"], (injected) => {
2528
+ const webServer = asWebServer(injected.webServer);
2529
+ if (webServer === void 0) {
2530
+ log("warn", "webServer exposes no register() — drs api route not registered (projection sync keeps running)");
2531
+ return;
2532
+ }
2533
+ register(webServer, "inject");
2534
+ });
2535
+ } catch (error) {
2536
+ log("warn", "webServer inject arm unavailable: %s — HTTP 面缺席,同步本体照常", String(error?.message ?? error));
2537
+ }
2538
+ log("info", "deferred init done (store opened, sync started, routes registered)");
2539
+ } catch (error) {
2540
+ log("warn", "deferred init failed — projection store 不可用(node:sqlite 缺席/目录不可写):插件停用—— %s", String(error?.message ?? error));
2541
+ }
2542
+ });
2531
2543
  }
2532
2544
  //#endregion
2533
2545
  export { BOOT_SYNC_DELAY_MS, DEFAULT_SYNC_INTERVAL_MS, DISCOVERY_LIMIT, DRS_API_PATH, INJECTED_BLOCK_TAGS, LEDGER_LRU_CAP, LedgerCache, PROJECTION_SCHEMA_VERSION, ProjectionSync, SYNC_INTERVAL_ENV, apply, buildSearchSnippet, classifySessionHeader, createQueryContext, createStorePort, decodeSessionLogBytes, decodeSessionLogFile, discoverSessionLogs, extractSearchEventText, extractTitleFromEvents, generationLogFilename, handleDrsRequest, inject, listSessions, openProjectionStore, projectSessionEvents, readArchivedSessionIds, readSessionGenerations, recallReport, reloadSummaries, resolveDshHome, resolveExactCounter, resolveIndexDir, resolveSyncIntervalMs, sanitizeFtsQuery, search, searchGrouped, segmentForIndex, segmentQueryTerm, sessionDetail, startIntervalSync, status, stripInjectedBlocks, surfaceLedger };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-reference-session-collector",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "tag": "beta",
@@ -72,7 +72,7 @@
72
72
  ],
73
73
  "engines": {
74
74
  "node": ">=22",
75
- "dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2"
75
+ "dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3"
76
76
  },
77
77
  "license": "MIT",
78
78
  "dependencies": {
package/src/index.ts CHANGED
@@ -195,7 +195,8 @@ function parseTail(url: URL): number | undefined {
195
195
  }
196
196
 
197
197
  /**
198
- * op 分发:GET `?op=…`;POST `{op:'sync'}` 触发后台同步。
198
+ * op 分发:GET `?op=…`;POST `?op=sync` 触发后台同步。
199
+ * POST 的 op 只从 **query** 读(`{op:'sync'}` 落进 body 会 400 unknown POST op)。
199
200
  * 未知 op / 缺参 → 400(不留空壳)。
200
201
  */
201
202
  export async function handleDrsRequest(deps: DrsHandlerDeps, req: IncomingMessage, res: ServerResponse): Promise<void> {
@@ -203,6 +204,11 @@ export async function handleDrsRequest(deps: DrsHandlerDeps, req: IncomingMessag
203
204
  writeJson(res, 403, { ok: false, error: 'forbidden' })
204
205
  return
205
206
  }
207
+ // 延迟初始化未完成——投影面不可用(503)。
208
+ if (deps.query === undefined) {
209
+ writeJson(res, 503, { ok: false, error: 'initializing' })
210
+ return
211
+ }
206
212
  let url: URL
207
213
  try {
208
214
  url = new URL(req.url ?? DRS_API_PATH, 'http://localhost')
@@ -227,7 +233,9 @@ export async function handleDrsRequest(deps: DrsHandlerDeps, req: IncomingMessag
227
233
 
228
234
  switch (op) {
229
235
  case 'status': {
230
- writeJson(res, 200, { ok: true, ...status(deps.query, deps.sync.lastSyncResult ?? null) })
236
+ // syncing:真实在跑旗标(单次触发即回、后台跑完才落 last_sync),
237
+ // 消费方(dsh-search-index 深度同步)靠它判断慢扫何时落定。
238
+ writeJson(res, 200, { ok: true, ...status(deps.query, deps.sync.lastSyncResult ?? null), syncing: deps.sync.isSyncing })
231
239
  return
232
240
  }
233
241
  case 'sessions': {
@@ -345,7 +353,8 @@ function safeReadService(ctx: Context, name: string): unknown {
345
353
  }
346
354
 
347
355
  /**
348
- * 插件主体:开库 → 装配 sync(启动延迟首轮 + interval 轮)→ 双通道注册路由。
356
+ * 插件主体:apply() 立即返回,重活(开库/建 sync/注册路由)推迟到 setImmediate。
357
+ * 宿主插件装载串行同步——apply 不返回则整列插件都装不上,前端一直 Loading plugins…。
349
358
  * @param ctx - host 插件上下文(webServer 声明注入;webStartup/webRuntime 回退臂)。
350
359
  */
351
360
  export function apply(ctx: Context): void {
@@ -358,53 +367,13 @@ export function apply(ctx: Context): void {
358
367
  } catch { /* 日志面永不致命 */ }
359
368
  }
360
369
 
361
- const storeDir = resolveIndexDir()
362
- let store: ProjectionStore
363
- let query: QueryContext
364
- try {
365
- store = openProjectionStore(storeDir)
366
- query = createQueryContext(store)
367
- } catch (error) {
368
- log('warn', 'projection store 不可用(node:sqlite 缺席/目录不可写):插件停用—— %s',
369
- String((error as Error)?.message ?? error))
370
- return
371
- }
372
-
373
- const sync = new ProjectionSync({
374
- store: createStorePort(store),
375
- project: (events, options) => projectSessionEvents(events, {
376
- dirId: options.dirId,
377
- ...(options.header === undefined ? {} : { header: options.header }),
378
- // 端口计数器返 TokenCount;投影只需数值臂(CCI verify 同款降级)。
379
- ...(options.exactCounter === undefined ? {} : {
380
- exactCounter: (text: string): number => {
381
- const count = options.exactCounter?.(text)
382
- return count !== undefined && count.kind === 'exact-tokenizer'
383
- ? count.tokens
384
- : Math.ceil(text.length / 4)
385
- },
386
- }),
387
- }),
388
- log: (level, message, ...args) => log(level, message, ...args),
389
- })
390
-
391
- // 生命周期:dispose 清定时器 + 关库。
370
+ // 延迟初始化状态(setImmediate 臂赋值;请求面兜底 503)。
371
+ let store: ProjectionStore | undefined
372
+ let query: QueryContext | undefined
373
+ let sync: ProjectionSync | undefined
392
374
  let bootTimer: ReturnType<typeof setTimeout> | undefined
393
375
  let disposeInterval: () => void = () => {}
394
- ctx.effect(() => {
395
- bootTimer = setTimeout(() => {
396
- void sync.syncAll().then(result => {
397
- log('info', 'boot sync done: updated=%d skipped=%d basis=%s %dms',
398
- result.updated, result.skipped, result.basis, result.durationMs)
399
- }).catch((error: unknown) => log('warn', 'boot sync failed: %s', String((error as Error)?.message ?? error)))
400
- }, BOOT_SYNC_DELAY_MS)
401
- disposeInterval = startIntervalSync(sync)
402
- return () => {
403
- if (bootTimer !== undefined) clearTimeout(bootTimer)
404
- disposeInterval()
405
- store.close()
406
- }
407
- }, 'dsh-reference-session-collector: projection lifecycle')
376
+ let registered = false
408
377
 
409
378
  // trustedHosts best-effort(双名回退链,属性/get 两层 safe;缺席=放行)。
410
379
  const readStartupFace = (): WebStartupLike | undefined => {
@@ -415,13 +384,12 @@ export function apply(ctx: Context): void {
415
384
  const depsFor = (): DrsHandlerDeps => {
416
385
  const startup = readStartupFace()
417
386
  return {
418
- query,
419
- sync,
387
+ query: query!,
388
+ sync: sync!,
420
389
  ...(startup?.trustedHosts !== undefined ? { trustedHosts: startup.trustedHosts } : {}),
421
390
  }
422
391
  }
423
392
 
424
- let registered = false
425
393
  const register = (webServer: WebServerLike, channel: 'direct' | 'inject'): void => {
426
394
  if (registered) return
427
395
  try {
@@ -445,24 +413,72 @@ export function apply(ctx: Context): void {
445
413
  }
446
414
  }
447
415
 
448
- const readService = (name: string): unknown => safeReadService(ctx, name)
416
+ // 生命周期:dispose 清定时器 + 关库(同步登记,cleanup 在 dispose 时执行)。
417
+ ctx.effect(() => {
418
+ return () => {
419
+ if (bootTimer !== undefined) clearTimeout(bootTimer)
420
+ disposeInterval()
421
+ store?.close()
422
+ }
423
+ }, 'dsh-reference-session-collector: projection lifecycle')
449
424
 
450
- const active = asWebServer(readService('webServer'))
451
- if (active !== undefined) {
452
- register(active, 'direct')
453
- if (registered) return
454
- }
425
+ // 延迟初始化:开库/建 sync/注册路由全部推迟,保证 apply 立即返回。
426
+ setImmediate(() => {
427
+ try {
428
+ const storeDir = resolveIndexDir()
429
+ store = openProjectionStore(storeDir)
430
+ query = createQueryContext(store)
455
431
 
456
- try {
457
- ctx.inject(['webServer'], (injected) => {
458
- const webServer = asWebServer((injected as { webServer?: unknown }).webServer)
459
- if (webServer === undefined) {
460
- log('warn', 'webServer exposes no register() — drs api route not registered (projection sync keeps running)')
461
- return
432
+ sync = new ProjectionSync({
433
+ store: createStorePort(store),
434
+ project: (events, options) => projectSessionEvents(events, {
435
+ dirId: options.dirId,
436
+ ...(options.header === undefined ? {} : { header: options.header }),
437
+ ...(options.exactCounter === undefined ? {} : {
438
+ exactCounter: (text: string): number => {
439
+ const count = options.exactCounter?.(text)
440
+ return count !== undefined && count.kind === 'exact-tokenizer'
441
+ ? count.tokens
442
+ : Math.ceil(text.length / 4)
443
+ },
444
+ }),
445
+ }),
446
+ log: (level, message, ...args) => log(level, message, ...args),
447
+ })
448
+
449
+ // 启动延迟首轮 sync + interval 轮。
450
+ bootTimer = setTimeout(() => {
451
+ void sync!.syncAll().then(result => {
452
+ log('info', 'boot sync done: updated=%d skipped=%d basis=%s %dms',
453
+ result.updated, result.skipped, result.basis, result.durationMs)
454
+ }).catch((error: unknown) => log('warn', 'boot sync failed: %s', String((error as Error)?.message ?? error)))
455
+ }, BOOT_SYNC_DELAY_MS)
456
+ disposeInterval = startIntervalSync(sync)
457
+
458
+ // 双通道注册路由。
459
+ const readService = (name: string): unknown => safeReadService(ctx, name)
460
+ const active = asWebServer(readService('webServer'))
461
+ if (active !== undefined) {
462
+ register(active, 'direct')
463
+ if (registered) return
462
464
  }
463
- register(webServer, 'inject')
464
- })
465
- } catch (error) {
466
- log('warn', 'webServer inject arm unavailable: %s — HTTP 面缺席,同步本体照常', String((error as Error)?.message ?? error))
467
- }
465
+ try {
466
+ ctx.inject(['webServer'], (injected) => {
467
+ const webServer = asWebServer((injected as { webServer?: unknown }).webServer)
468
+ if (webServer === undefined) {
469
+ log('warn', 'webServer exposes no register() — drs api route not registered (projection sync keeps running)')
470
+ return
471
+ }
472
+ register(webServer, 'inject')
473
+ })
474
+ } catch (error) {
475
+ log('warn', 'webServer inject arm unavailable: %s — HTTP 面缺席,同步本体照常', String((error as Error)?.message ?? error))
476
+ }
477
+
478
+ log('info', 'deferred init done (store opened, sync started, routes registered)')
479
+ } catch (error) {
480
+ log('warn', 'deferred init failed — projection store 不可用(node:sqlite 缺席/目录不可写):插件停用—— %s',
481
+ String((error as Error)?.message ?? error))
482
+ }
483
+ })
468
484
  }