@dsh-cc/cache-health 0.6.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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm check:readme --write
5
+ README.md: 7f024789a8e72beb7f633b8952f0ba96cce13663
6
+ README.zh.md: 46bd7294b1478a80a73b3f86914b5e8cd3cb7603
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # @dsh-cc/cache-health
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Passive prompt-cache prefix-stability observer with the `/cache-health` slash command. A `llm/stream` waterfall listener tracks which part of each outbound model request's prefix (system → tools → messages, in order) is volatile across calls within a session; the command joins the observation ledger with provider-metered cache usage (`cacheReadTokens` / `cacheWriteTokens`) from session events. Detector-only: it reports, never rewrites — no request rewriting, no provider-call changes, no harness changes.
6
+
7
+ ## Command contract
8
+
9
+ | Input | Result |
10
+ |---|---|
11
+ | `/cache-health` | Show the current stable prefix (segment count, estimated tokens, changed-since-last-call flag), the drift table (only rows whose prefix changed, with redacted excerpts), per-call and session-total cache read/write token ratios, front-loaded suspects (drift at segment ≤ 2: system / tools / first message — where cwd and `DSH_SESSION_*` volatility lives), and tail appends listed explicitly as NOT suspects. |
12
+
13
+ Cache metering numbers are provider-metered; zero-metered upstreams produce zeros, not evidence of misses.
14
+
15
+ ## Ledger
16
+
17
+ One JSONL row per model call at `<dshHome>/cache-health/<projectKey>/<sessionId>.jsonl`, where `projectKey` is a short hash of the session cwd (the context-crusher idiom). Rows carry `{ts, seq, provider, model, stableSegments, stablePrefixHash, stablePrefixTokensEst, prefixChanged, driftSegmentIndex?, driftExcerpt?, callPurpose?}` — `stablePrefixHash` is a fingerprint of the stable prefix (hash over its segment hashes), stable across calls sharing the prefix and comparable with the offline `cache-trajectory` analyzer. Appends are fire-and-forget — observation never adds latency to model calls. The file is capped at 2000 rows (oldest trimmed on overflow).
18
+
19
+ Rows reflect the raw pre-middleware view of `llm/stream` options, not a wire-faithful rendering: a future middleware rewriting options in `llm/stream` would make this ledger under-report churn. Excerpts are whitespace-collapsed, redacted (`sk-…` keys, `Bearer` tokens, opaque runs ≥ 32 chars), and truncated to 80 characters. `stablePrefixTokensEst` (canonical length / 4) is an estimate, not a token count.
20
+
21
+ ## Configuration
22
+
23
+ ```yaml
24
+ - id: cache-health
25
+ name: '@dsh-cc/cache-health'
26
+ config:
27
+ enabled: true # default; set false to disable the listener and the command
28
+ ```
29
+
30
+ ## Composition
31
+
32
+ The CC preset (`@dsh-cc/preset-cc`) mounts the plugin as a slash-command row (`cache-health` in its `agent.cordis.yml`). The plugin declares `inject = ['commands', 'sessions']` and reads `dshHomePath` defensively; a host without `dshHomePath` force-disables the observer.
package/README.zh.md ADDED
@@ -0,0 +1,32 @@
1
+ # @dsh-cc/cache-health
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 被动的提示词缓存前缀稳定性观察器,附带 `/cache-health` 斜杠命令。一个 `llm/stream` 瀑布监听器跟踪每次出站模型请求的前缀(system → tools → messages,按序)在会话内哪些部分是易变的;该命令将观察账本与会话事件中的提供商计量缓存用量(`cacheReadTokens` / `cacheWriteTokens`)连接起来。仅探测:只报告,从不改写——不改写请求、不改提供商调用、不改 harness。
6
+
7
+ ## 命令契约
8
+
9
+ | 输入 | 结果 |
10
+ |---|---|
11
+ | `/cache-health` | 显示当前稳定前缀(段数、估算 token 数、自上次调用是否变化)、漂移表(仅列出前缀变化的行,附脱敏摘录)、每次调用与会话总计的缓存读/写 token 比例、前置可疑项(漂移段 ≤ 2:system / tools / 首条消息——cwd 与 `DSH_SESSION_*` 易变性所在),并明确列出尾部追加不属于可疑项。 |
12
+
13
+ 缓存计量数字来自提供商计量;零计量的上游产生的是零,而不是未命中的证据。
14
+
15
+ ## 账本
16
+
17
+ 每次模型调用一行 JSONL,位于 `<dshHome>/cache-health/<projectKey>/<sessionId>.jsonl`,其中 `projectKey` 是会话 cwd 的短哈希(context-crusher 惯例)。行包含 `{ts, seq, provider, model, stableSegments, stablePrefixHash, stablePrefixTokensEst, prefixChanged, driftSegmentIndex?, driftExcerpt?, callPurpose?}`——`stablePrefixHash` 是稳定前缀的指纹(对其各段哈希再哈希),共享前缀的调用间保持稳定,可与离线 `cache-trajectory` 分析器对齐比对。追加是即发即忘的——观察绝不会给模型调用增加延迟。文件上限 2000 行(溢出时裁剪最旧的)。
18
+
19
+ 行反映的是 `llm/stream` options 的中间件之前原始视图,而非线上忠实呈现:未来若有中间件在 `llm/stream` 中改写 options,该账本将低估抖动。摘录会折叠空白、脱敏(`sk-…` 密钥、`Bearer` 令牌、≥ 32 字符的不透明串)并截断到 80 字符。`stablePrefixTokensEst`(规范化长度 / 4)是估算值,不是 token 计数。
20
+
21
+ ## 配置
22
+
23
+ ```yaml
24
+ - id: cache-health
25
+ name: '@dsh-cc/cache-health'
26
+ config:
27
+ enabled: true # 默认;设为 false 可同时禁用监听器与命令
28
+ ```
29
+
30
+ ## 组装
31
+
32
+ CC 预设(`@dsh-cc/preset-cc`)将本插件作为斜杠命令行挂载(其 `agent.cordis.yml` 中的 `cache-health` 行)。插件声明 `inject = ['commands', 'sessions']` 并防御性读取 `dshHomePath`;没有 `dshHomePath` 的宿主会强制禁用观察器。
package/lib/index.d.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Passive prompt-cache health observer. A `llm/stream` waterfall listener
3
+ * tracks which part of each outbound request's prefix (system → tools →
4
+ * messages) is volatile across calls within a session, and a `/cache-health`
5
+ * slash command joins the observation ledger with provider-metered cache
6
+ * usage from session events.
7
+ *
8
+ * Detector-only by design: the observer never rewrites options, never calls
9
+ * providers, and never touches the harness. Every observation failure is
10
+ * logged and swallowed — a bug here must never break a model call.
11
+ *
12
+ * @module @dsh-cc/cache-health
13
+ */
14
+ import type { Context } from '@deepseek-ai/cordis';
15
+ import z from '@deepseek-ai/schemastery';
16
+ import type { GenerateOptions } from '@deepseek-ai/dsh-llm';
17
+ import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session';
18
+ import { CacheHealthLedger } from './ledger.ts';
19
+ import { PrefixTracker } from './tracker.ts';
20
+ export { CacheHealthLedger, LEDGER_MAX_ROWS, type LedgerRow } from './ledger.ts';
21
+ export { buildReport, CACHE_METER_DISCLAIMER, foldUsage, renderReport, TAIL_APPEND_NOTE, type CacheHealthReport, type UsageFold, } from './report.ts';
22
+ export { canonicalJson, excerpt, PrefixTracker, sha256Hex, type PrefixObservation } from './tracker.ts';
23
+ export declare const name = "cache-health";
24
+ export declare const inject: string[];
25
+ /** `/cache-health` configuration: a single on/off switch (default on). */
26
+ export interface Config {
27
+ /** Set to false to disable both the llm/stream listener and the command. */
28
+ readonly enabled: boolean;
29
+ }
30
+ export declare const Config: z<Schemastery.ObjectS<{
31
+ enabled: z<boolean, boolean>;
32
+ }>, Schemastery.ObjectT<{
33
+ enabled: z<boolean, boolean>;
34
+ }>>;
35
+ declare module '@deepseek-ai/cordis' {
36
+ interface Context {
37
+ /** Harness-home path resolver, provided by @deepseek-ai/dsh-app-boot at boot. Optional in tests. */
38
+ dshHomePath?: (...segments: string[]) => string;
39
+ }
40
+ }
41
+ /**
42
+ * The minimal session surface the observer needs (structural subset of the
43
+ * dsh-session Session, so tests can pass real sessions or stubs).
44
+ */
45
+ interface ObservedSession {
46
+ readonly id: SessionId;
47
+ readonly header: {
48
+ readonly cwd?: string;
49
+ };
50
+ snapshotEvents(): readonly SessionEvent[];
51
+ }
52
+ /** Dependencies for the testable observe path. */
53
+ export interface CacheHealthDeps {
54
+ /** Session store lookup; a miss skips the observation. */
55
+ readonly sessions?: {
56
+ get(id: SessionId): ObservedSession | undefined;
57
+ };
58
+ readonly ledger: CacheHealthLedger;
59
+ readonly tracker: PrefixTracker;
60
+ /** Warning sink for swallowed observer failures. */
61
+ readonly warn: (message: string) => void;
62
+ }
63
+ /**
64
+ * Observe one model request. Read-only on `options`; all failures — including
65
+ * throws from session lookup or hashing — are caught, warned, and swallowed.
66
+ * Skips: auxiliary calls (`compaction`, `session-title`), requests without a
67
+ * sessionId, and unknown sessions.
68
+ */
69
+ export declare function observeRequest(deps: CacheHealthDeps, options: GenerateOptions): void;
70
+ /**
71
+ * Register the cache-health observer: the `llm/stream` listener and the
72
+ * `/cache-health` command.
73
+ * @param ctx - context carrying the command registry and session store.
74
+ * @param config - plugin config (enabled flag).
75
+ */
76
+ export declare function apply(ctx: Context, config: Config): void;
77
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAExC,OAAO,KAAK,EAAE,eAAe,EAAe,MAAM,sBAAsB,CAAA;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AAEvE,OAAO,EAAE,iBAAiB,EAAkB,MAAM,aAAa,CAAA;AAE/D,OAAO,EAAE,aAAa,EAAa,MAAM,cAAc,CAAA;AAEvD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAA;AAChF,OAAO,EACL,WAAW,EACX,sBAAsB,EACtB,SAAS,EACT,YAAY,EACZ,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,KAAK,SAAS,GACf,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAEvG,eAAO,MAAM,IAAI,iBAAiB,CAAA;AAClC,eAAO,MAAM,MAAM,UAA2B,CAAA;AAE9C,0EAA0E;AAC1E,MAAM,WAAW,MAAM;IACrB,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED,eAAO,MAAM,MAAM;;;;GAEjB,CAAA;AAKF,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,oGAAoG;QACpG,WAAW,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,KAAK,MAAM,CAAA;KAChD;CACF;AAUD;;;GAGG;AACH,UAAU,eAAe;IACvB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1C,cAAc,IAAI,SAAS,YAAY,EAAE,CAAA;CAC1C;AAED,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,EAAE,SAAS,GAAG,eAAe,GAAG,SAAS,CAAA;KAAE,CAAA;IACvE,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;IAClC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;IAC/B,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACzC;AAOD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAgCpF;AAWD;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAwBxD"}
package/lib/index.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Passive prompt-cache health observer. A `llm/stream` waterfall listener
3
+ * tracks which part of each outbound request's prefix (system → tools →
4
+ * messages) is volatile across calls within a session, and a `/cache-health`
5
+ * slash command joins the observation ledger with provider-metered cache
6
+ * usage from session events.
7
+ *
8
+ * Detector-only by design: the observer never rewrites options, never calls
9
+ * providers, and never touches the harness. Every observation failure is
10
+ * logged and swallowed — a bug here must never break a model call.
11
+ *
12
+ * @module @dsh-cc/cache-health
13
+ */
14
+ import z from '@deepseek-ai/schemastery';
15
+ import { helpable } from '@dsh-cc/command-usage';
16
+ import { CacheHealthLedger } from "./ledger.js";
17
+ import { buildReport, renderReport } from "./report.js";
18
+ import { PrefixTracker, shortHash } from "./tracker.js";
19
+ export { CacheHealthLedger, LEDGER_MAX_ROWS } from "./ledger.js";
20
+ export { buildReport, CACHE_METER_DISCLAIMER, foldUsage, renderReport, TAIL_APPEND_NOTE, } from "./report.js";
21
+ export { canonicalJson, excerpt, PrefixTracker, sha256Hex } from "./tracker.js";
22
+ export const name = 'cache-health';
23
+ export const inject = ['commands', 'sessions'];
24
+ export const Config = z.object({
25
+ enabled: z.boolean().default(true),
26
+ });
27
+ function dshHomeFn(ctx) {
28
+ try {
29
+ return ctx.dshHomePath;
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ /** projectKey = shortHash of the session cwd (context-crusher idiom). */
36
+ function projectKeyOf(session) {
37
+ return shortHash(session.header.cwd ?? process.cwd());
38
+ }
39
+ /**
40
+ * Observe one model request. Read-only on `options`; all failures — including
41
+ * throws from session lookup or hashing — are caught, warned, and swallowed.
42
+ * Skips: auxiliary calls (`compaction`, `session-title`), requests without a
43
+ * sessionId, and unknown sessions.
44
+ */
45
+ export function observeRequest(deps, options) {
46
+ try {
47
+ if (options.purpose === 'compaction' || options.purpose === 'session-title')
48
+ return;
49
+ if (options.sessionId === undefined)
50
+ return;
51
+ const session = deps.sessions?.get(options.sessionId);
52
+ if (session === undefined)
53
+ return;
54
+ const observation = deps.tracker.observe(String(session.id), options);
55
+ const events = session.snapshotEvents();
56
+ const row = {
57
+ ts: new Date().toISOString(),
58
+ seq: events.length > 0 ? events[events.length - 1].seq : 0,
59
+ provider: options.provider,
60
+ model: options.model,
61
+ stableSegments: observation.stableSegments,
62
+ stablePrefixHash: observation.stablePrefixHash,
63
+ stablePrefixTokensEst: observation.stablePrefixTokensEst,
64
+ prefixChanged: observation.prefixChanged,
65
+ ...(observation.driftSegmentIndex !== undefined
66
+ ? { driftSegmentIndex: observation.driftSegmentIndex }
67
+ : {}),
68
+ ...(observation.driftExcerpt !== undefined
69
+ ? { driftExcerpt: observation.driftExcerpt }
70
+ : {}),
71
+ };
72
+ deps.ledger.append(projectKeyOf(session), String(session.id), row);
73
+ }
74
+ catch (error) {
75
+ try {
76
+ deps.warn(`cache-health: observation failed: ${error instanceof Error ? error.message : String(error)}`);
77
+ }
78
+ catch {
79
+ // never propagate
80
+ }
81
+ }
82
+ }
83
+ /** Execute `/cache-health` against the invocation's own session ledger. */
84
+ async function execute(invocation, ledger) {
85
+ const session = invocation.agent.session;
86
+ const projectKey = shortHash(session.header.cwd ?? process.cwd());
87
+ const rows = await ledger.read(projectKey, String(session.id));
88
+ const report = buildReport(rows, session.snapshotEvents());
89
+ return { kind: 'success', text: renderReport(report) };
90
+ }
91
+ /**
92
+ * Register the cache-health observer: the `llm/stream` listener and the
93
+ * `/cache-health` command.
94
+ * @param ctx - context carrying the command registry and session store.
95
+ * @param config - plugin config (enabled flag).
96
+ */
97
+ export function apply(ctx, config) {
98
+ if (!config.enabled)
99
+ return;
100
+ const home = dshHomeFn(ctx);
101
+ if (home === undefined) {
102
+ ctx.logger.warn('cache-health: no dshHomePath on the host context; force-disabled');
103
+ return;
104
+ }
105
+ const ledger = new CacheHealthLedger(home('cache-health'), (error) => {
106
+ ctx.logger.warn(`cache-health: ledger write failed: ${error instanceof Error ? error.message : String(error)}`);
107
+ });
108
+ const tracker = new PrefixTracker();
109
+ const sessions = ctx.sessions;
110
+ ctx.on('llm/stream', (options, next) => {
111
+ observeRequest({ sessions, ledger, tracker, warn: (message) => ctx.logger.warn(message) }, options);
112
+ return next();
113
+ }, { global: true, prepend: true });
114
+ ctx.commands.register(helpable({
115
+ name: 'cache-health',
116
+ description: 'show prompt-cache prefix stability and provider-metered cache usage for this session',
117
+ handler: invocation => execute(invocation, ledger),
118
+ }));
119
+ }
120
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * JSONL ledger for prefix observations: one row per model call, appended to
3
+ * `<dshHome>/cache-health/<projectKey>/<sessionId>.jsonl`. All I/O is
4
+ * fire-and-forget — the llm/stream path must never await on it — and every
5
+ * error is reported through the injected sink, never thrown.
6
+ *
7
+ * @module @dsh-cc/cache-health/ledger
8
+ */
9
+ /** Hard cap of rows per session file; older rows are trimmed on overflow. */
10
+ export declare const LEDGER_MAX_ROWS = 2000;
11
+ /** One ledger row (one JSON object per jsonl line). */
12
+ export interface LedgerRow {
13
+ /** ISO timestamp of the observation. */
14
+ readonly ts: string;
15
+ /** Seq of the last session event at observe time (the log's throughSeq). */
16
+ readonly seq: number;
17
+ readonly provider: string;
18
+ readonly model: string;
19
+ readonly stableSegments: number;
20
+ /** Fingerprint of the stable prefix — hash over its segment hashes. */
21
+ readonly stablePrefixHash: string;
22
+ readonly stablePrefixTokensEst: number;
23
+ readonly prefixChanged: boolean;
24
+ readonly driftSegmentIndex?: number;
25
+ readonly driftExcerpt?: string;
26
+ /** When set, the call is an auxiliary one (compaction / session-title). */
27
+ readonly callPurpose?: string;
28
+ }
29
+ /** Error sink (the plugin passes a warn-logger); never throws. */
30
+ export type LedgerErrorSink = (error: unknown) => void;
31
+ /**
32
+ * Append-only ledger store. One instance per mounted plugin; `root` is
33
+ * `dshHomePath('cache-health')`. Row counts are tracked in memory from
34
+ * process start (an on-disk file longer than the cap is trimmed on the
35
+ * first post-start overflow check).
36
+ */
37
+ export declare class CacheHealthLedger {
38
+ readonly root: string;
39
+ private readonly onError;
40
+ private readonly counts;
41
+ /** Per-file write chain: serializes appends so rows keep call order (still floating). */
42
+ private readonly pending;
43
+ constructor(root: string, onError?: LedgerErrorSink);
44
+ /** Ledger file path for one session within a project. */
45
+ pathFor(projectKey: string, sessionId: string): string;
46
+ /** Append one row as a FLOATING promise chained per file (call order preserved). Never await on the llm/stream path. */
47
+ append(projectKey: string, sessionId: string, row: LedgerRow): void;
48
+ /** Read all rows of one session ledger; missing/corrupt file → []. */
49
+ read(projectKey: string, sessionId: string): Promise<LedgerRow[]>;
50
+ /** Rewrite the file keeping only the newest LEDGER_MAX_ROWS rows. */
51
+ private trim;
52
+ }
53
+ //# sourceMappingURL=ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,6EAA6E;AAC7E,eAAO,MAAM,eAAe,OAAO,CAAA;AAEnC,uDAAuD;AACvD,MAAM,WAAW,SAAS;IACxB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;IACtC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;IAC/B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IACnC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,kEAAkE;AAClE,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;AAEtD;;;;;GAKG;AAGH,qBAAa,iBAAiB;IAM1B,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD,yFAAyF;IACzF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;gBAGhD,IAAI,EAAE,MAAM,EACJ,OAAO,GAAE,eAA0B;IAGtD,yDAAyD;IACzD,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM;IAItD,wHAAwH;IACxH,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,IAAI;IAqBnE,sEAAsE;IAChE,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAmBvE,qEAAqE;YACvD,IAAI;CAOnB"}
package/lib/ledger.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * JSONL ledger for prefix observations: one row per model call, appended to
3
+ * `<dshHome>/cache-health/<projectKey>/<sessionId>.jsonl`. All I/O is
4
+ * fire-and-forget — the llm/stream path must never await on it — and every
5
+ * error is reported through the injected sink, never thrown.
6
+ *
7
+ * @module @dsh-cc/cache-health/ledger
8
+ */
9
+ import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
10
+ import { dirname, join } from 'node:path';
11
+ /** Hard cap of rows per session file; older rows are trimmed on overflow. */
12
+ export const LEDGER_MAX_ROWS = 2000;
13
+ /**
14
+ * Append-only ledger store. One instance per mounted plugin; `root` is
15
+ * `dshHomePath('cache-health')`. Row counts are tracked in memory from
16
+ * process start (an on-disk file longer than the cap is trimmed on the
17
+ * first post-start overflow check).
18
+ */
19
+ // ponytail: in-memory row count ignores pre-existing file length until the
20
+ // first overflow; initialize from disk on first append if that ever matters.
21
+ export class CacheHealthLedger {
22
+ root;
23
+ onError;
24
+ counts = new Map();
25
+ /** Per-file write chain: serializes appends so rows keep call order (still floating). */
26
+ pending = new Map();
27
+ constructor(root, onError = () => { }) {
28
+ this.root = root;
29
+ this.onError = onError;
30
+ }
31
+ /** Ledger file path for one session within a project. */
32
+ pathFor(projectKey, sessionId) {
33
+ return join(this.root, projectKey, `${sessionId}.jsonl`);
34
+ }
35
+ /** Append one row as a FLOATING promise chained per file (call order preserved). Never await on the llm/stream path. */
36
+ append(projectKey, sessionId, row) {
37
+ const file = this.pathFor(projectKey, sessionId);
38
+ const write = async () => {
39
+ await mkdir(dirname(file), { recursive: true });
40
+ await appendFile(file, `${JSON.stringify(row)}\n`, 'utf8');
41
+ const count = (this.counts.get(file) ?? 0) + 1;
42
+ this.counts.set(file, count);
43
+ if (count > LEDGER_MAX_ROWS)
44
+ await this.trim(file);
45
+ };
46
+ const prev = this.pending.get(file) ?? Promise.resolve();
47
+ const next = prev.then(write, write);
48
+ this.pending.set(file, next);
49
+ next.catch((error) => {
50
+ try {
51
+ this.onError(error);
52
+ }
53
+ catch {
54
+ // sink failure must never propagate
55
+ }
56
+ });
57
+ }
58
+ /** Read all rows of one session ledger; missing/corrupt file → []. */
59
+ async read(projectKey, sessionId) {
60
+ const file = this.pathFor(projectKey, sessionId);
61
+ try {
62
+ const text = await readFile(file, 'utf8');
63
+ const rows = [];
64
+ for (const line of text.split('\n')) {
65
+ if (line.trim() === '')
66
+ continue;
67
+ try {
68
+ rows.push(JSON.parse(line));
69
+ }
70
+ catch {
71
+ // skip corrupt line, keep the rest
72
+ }
73
+ }
74
+ return rows;
75
+ }
76
+ catch {
77
+ return [];
78
+ }
79
+ }
80
+ /** Rewrite the file keeping only the newest LEDGER_MAX_ROWS rows. */
81
+ async trim(file) {
82
+ const text = await readFile(file, 'utf8');
83
+ const lines = text.split('\n').filter(line => line.trim() !== '');
84
+ const kept = lines.slice(-LEDGER_MAX_ROWS);
85
+ await writeFile(file, kept.length > 0 ? `${kept.join('\n')}\n` : '', 'utf8');
86
+ this.counts.set(file, kept.length);
87
+ }
88
+ }
89
+ //# sourceMappingURL=ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.js","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Pure /cache-health fold and render: joins ledger rows with session-event
3
+ * usage records and renders a plain-text report. No cordis or fs imports —
4
+ * unit-testable in isolation.
5
+ *
6
+ * @module @dsh-cc/cache-health/report
7
+ */
8
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
9
+ import type { LedgerRow } from './ledger.ts';
10
+ /** Usage folded onto one ledger row. */
11
+ export interface UsageFold {
12
+ readonly cacheReadTokens: number;
13
+ readonly cacheWriteTokens: number;
14
+ }
15
+ /** The full /cache-health report before rendering. */
16
+ export interface CacheHealthReport {
17
+ /** Latest row's stable-prefix state (undefined with no rows). */
18
+ readonly current?: {
19
+ readonly stableSegments: number;
20
+ readonly stablePrefixHash: string;
21
+ readonly stablePrefixTokensEst: number;
22
+ readonly changedSinceLastCall: boolean;
23
+ };
24
+ /** Rows whose prefix changed, in seq order. */
25
+ readonly driftRows: readonly LedgerRow[];
26
+ /** Drift rows at segment index <= 2 (system/tools/first message volatility). */
27
+ readonly frontLoadedSuspects: readonly LedgerRow[];
28
+ /** Drift rows at later indexes — tail appends, not prefix busters. */
29
+ readonly tailAppends: readonly LedgerRow[];
30
+ /** Per-row usage ratio plus session totals. */
31
+ readonly usage: {
32
+ readonly perRow: readonly (readonly [LedgerRow, UsageFold])[];
33
+ readonly totalCacheReadTokens: number;
34
+ readonly totalCacheWriteTokens: number;
35
+ };
36
+ }
37
+ /** Disclaimer for provider-metered cache counters (zero ≠ miss). */
38
+ export declare const CACHE_METER_DISCLAIMER = "provider-metered; zero-metered upstreams (e.g. glm-5.2 via llmbox) produce zeros, not evidence of misses";
39
+ /** Disclaimer for tail-appends (they extend, not bust, the prefix). */
40
+ export declare const TAIL_APPEND_NOTE = "tail appends extend the prefix rather than busting it";
41
+ /**
42
+ * Fold session-event usage onto ledger rows by the interval rule: with rows
43
+ * sorted by seq, a usage event at seq s accrues to row i when
44
+ * `rows[i].seq < s <= rows[i+1].seq` (the last row takes all remaining
45
+ * usage); events at or before the first row's seq are dropped. Rows sharing
46
+ * a seq (same throughSeq) therefore split the boundary: only the LAST row
47
+ * with that seq can receive events strictly after it.
48
+ * @param rows - ledger rows in append order (any order is tolerated).
49
+ * @param events - the session's durable event log, in sequence order.
50
+ */
51
+ export declare function foldUsage(rows: readonly LedgerRow[], events: readonly SessionEvent[]): readonly (readonly [LedgerRow, UsageFold])[];
52
+ /**
53
+ * Build the report: join rows with usage, split drift rows into
54
+ * front-loaded suspects (driftSegmentIndex <= 2: system/tools/first message
55
+ * carry cwd and DSH_SESSION_* volatility) and tail appends.
56
+ */
57
+ export declare function buildReport(rows: readonly LedgerRow[], events: readonly SessionEvent[]): CacheHealthReport;
58
+ /**
59
+ * Render the report as human shell text in the /cost style.
60
+ * @param report - the fold result.
61
+ * @returns the multi-line report text.
62
+ */
63
+ export declare function renderReport(report: CacheHealthReport): string;
64
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAE5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,wCAAwC;AACxC,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;CAClC;AAED,sDAAsD;AACtD,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,QAAQ,CAAC,OAAO,CAAC,EAAE;QACjB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;QAC/B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;QACjC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;QACtC,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAA;KACvC,CAAA;IACD,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,EAAE,SAAS,SAAS,EAAE,CAAA;IACxC,gFAAgF;IAChF,QAAQ,CAAC,mBAAmB,EAAE,SAAS,SAAS,EAAE,CAAA;IAClD,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,SAAS,SAAS,EAAE,CAAA;IAC1C,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,EAAE,CAAA;QAC7D,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;QACrC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;KACvC,CAAA;CACF;AAED,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,6GAC2E,CAAA;AAE9G,uEAAuE;AACvE,eAAO,MAAM,gBAAgB,0DAC8B,CAAA;AAQ3D;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,SAAS,SAAS,EAAE,EAC1B,MAAM,EAAE,SAAS,YAAY,EAAE,GAC9B,SAAS,CAAC,SAAS,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,EAAE,CAuB9C;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,SAAS,SAAS,EAAE,EAC1B,MAAM,EAAE,SAAS,YAAY,EAAE,GAC9B,iBAAiB,CA0BnB;AASD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAgD9D"}
package/lib/report.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Pure /cache-health fold and render: joins ledger rows with session-event
3
+ * usage records and renders a plain-text report. No cordis or fs imports —
4
+ * unit-testable in isolation.
5
+ *
6
+ * @module @dsh-cc/cache-health/report
7
+ */
8
+ /** Disclaimer for provider-metered cache counters (zero ≠ miss). */
9
+ export const CACHE_METER_DISCLAIMER = 'provider-metered; zero-metered upstreams (e.g. glm-5.2 via llmbox) produce zeros, not evidence of misses';
10
+ /** Disclaimer for tail-appends (they extend, not bust, the prefix). */
11
+ export const TAIL_APPEND_NOTE = 'tail appends extend the prefix rather than busting it';
12
+ function isTokenUsage(value) {
13
+ return typeof value === 'object' && value !== null
14
+ && typeof value.inputTokens === 'number'
15
+ && typeof value.outputTokens === 'number';
16
+ }
17
+ /**
18
+ * Fold session-event usage onto ledger rows by the interval rule: with rows
19
+ * sorted by seq, a usage event at seq s accrues to row i when
20
+ * `rows[i].seq < s <= rows[i+1].seq` (the last row takes all remaining
21
+ * usage); events at or before the first row's seq are dropped. Rows sharing
22
+ * a seq (same throughSeq) therefore split the boundary: only the LAST row
23
+ * with that seq can receive events strictly after it.
24
+ * @param rows - ledger rows in append order (any order is tolerated).
25
+ * @param events - the session's durable event log, in sequence order.
26
+ */
27
+ export function foldUsage(rows, events) {
28
+ const sorted = [...rows].sort((a, b) => a.seq - b.seq);
29
+ const folds = sorted.map(() => ({ cacheReadTokens: 0, cacheWriteTokens: 0 }));
30
+ for (const event of events) {
31
+ if (event.type !== 'assistant/message')
32
+ continue;
33
+ const usage = event.data.usage;
34
+ if (usage === undefined || !isTokenUsage(usage))
35
+ continue;
36
+ // Usage belongs to the row that CLOSED the interval
37
+ // (rows[i].seq < s <= rows[i+1].seq): the last row with seq < s, or none
38
+ // when s <= rows[0].seq.
39
+ let target = -1;
40
+ for (let i = 0; i < sorted.length; i++) {
41
+ const row = sorted[i];
42
+ if (row !== undefined && row.seq < event.seq)
43
+ target = i;
44
+ else
45
+ break;
46
+ }
47
+ if (target === -1)
48
+ continue;
49
+ const fold = folds[target];
50
+ if (fold === undefined)
51
+ continue; // unreachable: target < sorted.length
52
+ fold.cacheReadTokens += usage.cacheReadTokens ?? 0;
53
+ fold.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
54
+ }
55
+ return sorted.map((row, i) => [row, folds[i]]);
56
+ }
57
+ /**
58
+ * Build the report: join rows with usage, split drift rows into
59
+ * front-loaded suspects (driftSegmentIndex <= 2: system/tools/first message
60
+ * carry cwd and DSH_SESSION_* volatility) and tail appends.
61
+ */
62
+ export function buildReport(rows, events) {
63
+ const perRow = foldUsage(rows, events);
64
+ const driftRows = [...rows].sort((a, b) => a.seq - b.seq).filter(row => row.prefixChanged);
65
+ const last = [...rows].sort((a, b) => a.seq - b.seq).at(-1);
66
+ let totalRead = 0;
67
+ let totalWrite = 0;
68
+ for (const [, fold] of perRow) {
69
+ totalRead += fold.cacheReadTokens;
70
+ totalWrite += fold.cacheWriteTokens;
71
+ }
72
+ return {
73
+ ...(last !== undefined
74
+ ? {
75
+ current: {
76
+ stableSegments: last.stableSegments,
77
+ stablePrefixHash: last.stablePrefixHash,
78
+ stablePrefixTokensEst: last.stablePrefixTokensEst,
79
+ changedSinceLastCall: last.prefixChanged,
80
+ },
81
+ }
82
+ : {}),
83
+ driftRows,
84
+ frontLoadedSuspects: driftRows.filter(row => (row.driftSegmentIndex ?? Number.MAX_SAFE_INTEGER) <= 2),
85
+ tailAppends: driftRows.filter(row => (row.driftSegmentIndex ?? Number.MAX_SAFE_INTEGER) > 2),
86
+ usage: { perRow, totalCacheReadTokens: totalRead, totalCacheWriteTokens: totalWrite },
87
+ };
88
+ }
89
+ /** Ratio display: read share of metered cache traffic (read / (read + write)). */
90
+ function readShare(fold) {
91
+ const total = fold.cacheReadTokens + fold.cacheWriteTokens;
92
+ if (total === 0)
93
+ return 'n/a';
94
+ return `${Math.round((fold.cacheReadTokens / total) * 100)}%`;
95
+ }
96
+ /**
97
+ * Render the report as human shell text in the /cost style.
98
+ * @param report - the fold result.
99
+ * @returns the multi-line report text.
100
+ */
101
+ export function renderReport(report) {
102
+ const lines = [];
103
+ if (report.current === undefined) {
104
+ return 'No cache-health data yet; no model call has been observed for this session.';
105
+ }
106
+ lines.push('Prompt-cache health', '');
107
+ lines.push(`Current stable prefix: ${report.current.stableSegments} segments,`
108
+ + ` ~${report.current.stablePrefixTokensEst} tokens (estimate),`
109
+ + ` hash ${report.current.stablePrefixHash.slice(0, 16)},`
110
+ + ` changed since last call: ${report.current.changedSinceLastCall ? 'yes' : 'no'}`);
111
+ lines.push('');
112
+ if (report.driftRows.length === 0) {
113
+ lines.push('No prefix drift observed.');
114
+ }
115
+ else {
116
+ lines.push('Prefix drift (changed segments):');
117
+ for (const row of report.driftRows) {
118
+ lines.push(` seq ${row.seq} ${row.provider}/${row.model}`
119
+ + ` segment ${row.driftSegmentIndex ?? '?'}: ${row.driftExcerpt ?? '(n/a)'}`);
120
+ }
121
+ }
122
+ lines.push('');
123
+ if (report.frontLoadedSuspects.length > 0) {
124
+ lines.push('Front-loaded suspects (drift at segment <= 2: system/tools/first message):');
125
+ for (const row of report.frontLoadedSuspects) {
126
+ lines.push(` seq ${row.seq} ${row.provider}/${row.model} segment ${row.driftSegmentIndex ?? '?'}`);
127
+ }
128
+ }
129
+ else {
130
+ lines.push('No front-loaded suspects.');
131
+ }
132
+ lines.push(`Not suspects: ${report.tailAppends.length} drift row(s) at later indexes — ${TAIL_APPEND_NOTE}.`);
133
+ lines.push('');
134
+ lines.push('Provider-metered cache usage per call:');
135
+ for (const [row, fold] of report.usage.perRow) {
136
+ lines.push(` seq ${row.seq} ${row.provider}/${row.model}:`
137
+ + ` read ${fold.cacheReadTokens}, write ${fold.cacheWriteTokens} (read share ${readShare(fold)})`);
138
+ }
139
+ lines.push(`Session totals: read ${report.usage.totalCacheReadTokens},`
140
+ + ` write ${report.usage.totalCacheWriteTokens}`);
141
+ lines.push(`Note: ${CACHE_METER_DISCLAIMER}.`);
142
+ return lines.join('\n');
143
+ }
144
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Pure prefix-stability tracking for the prompt-cache health observer.
3
+ *
4
+ * The wire prefix of a model request is system → tools → messages, in order.
5
+ * Each segment is canonical-JSON serialized (recursive key sort) and hashed
6
+ * with sha256; per session we keep the previous call's hash list and report
7
+ * the longest common prefix. Hashing stops at the first difference (early
8
+ * exit), so a drift near the front of a long conversation is cheap.
9
+ *
10
+ * No cordis, fs, or dsh imports beyond dsh-llm types — fully unit-testable.
11
+ *
12
+ * @module @dsh-cc/cache-health/tracker
13
+ */
14
+ import type { GenerateOptions } from '@deepseek-ai/dsh-llm';
15
+ /** Injectable hash function signature (tests count calls to verify early exit). */
16
+ export type HashFn = (text: string) => string;
17
+ /** sha256 hex, first 16 chars — the shared project-key shape (context-crusher idiom). */
18
+ export declare function shortHash(input: string): string;
19
+ /** Default segment hash: sha256 hex. */
20
+ export declare const sha256Hex: HashFn;
21
+ /**
22
+ * Canonical JSON: deterministic, key-sorted serialization for hashing.
23
+ * Objects sort keys lexicographically; arrays keep order; `undefined`
24
+ * properties are dropped (JSON semantics). No dependency — ~20 lines.
25
+ */
26
+ export declare function canonicalJson(value: unknown): string;
27
+ /**
28
+ * Build the drift excerpt: canonical-serialize, collapse whitespace runs,
29
+ * redact secret-shaped runs, truncate to 80 chars.
30
+ * @param text - the canonical serialization of the drifted segment source.
31
+ */
32
+ export declare function excerpt(text: string): string;
33
+ /**
34
+ * One observed model request's prefix-stability verdict against the session's
35
+ * previous call.
36
+ *
37
+ * Note: rows reflect the RAW PRE-MIDDLEWARE view of `llm/stream` options, not
38
+ * a wire-faithful rendering. A future middleware rewriting options in
39
+ * `llm/stream` would make this ledger under-report churn.
40
+ */
41
+ export interface PrefixObservation {
42
+ /** Number of leading segments whose hash equals the previous call's. */
43
+ readonly stableSegments: number;
44
+ /** Fingerprint of the stable prefix (hash over its segment hashes); stable across calls sharing the prefix, comparable against the offline cache-trajectory analyzer. */
45
+ readonly stablePrefixHash: string;
46
+ /** Estimated tokens of the stable prefix (canonical length / 4, rounded). */
47
+ readonly stablePrefixTokensEst: number;
48
+ /** Whether a previously-stable segment changed (prefix bust, not a tail append). */
49
+ readonly prefixChanged: boolean;
50
+ /** Zero-based index of the first differing segment (system=0, tools=1, messages 2+). */
51
+ readonly driftSegmentIndex?: number;
52
+ /** Redacted, truncated serialization of the drifted segment source. */
53
+ readonly driftExcerpt?: string;
54
+ }
55
+ /**
56
+ * Per-session in-memory prefix tracker. One instance per mounted plugin.
57
+ * Segment 0 = system, 1 = tools, 2+ = messages in order. On drift the hash
58
+ * list stops at the drifted index, so the NEXT call verifies only that
59
+ * (shorter) prefix — stableSegments may temporarily under-count after a
60
+ * drift; `prefixChanged` stays correct.
61
+ */
62
+ export declare class PrefixTracker {
63
+ private readonly hash;
64
+ private readonly prev;
65
+ constructor(hash?: HashFn);
66
+ /**
67
+ * Observe one model request for a session. Purely read-only on `options`;
68
+ * never mutates the request.
69
+ */
70
+ observe(sessionId: string, options: GenerateOptions): PrefixObservation;
71
+ }
72
+ //# sourceMappingURL=tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../src/tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAE3D,mFAAmF;AACnF,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;AAE7C,yFAAyF;AACzF,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wCAAwC;AACxC,eAAO,MAAM,SAAS,EAAE,MAA0E,CAAA;AAElG;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAeD;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO5C;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,yKAAyK;IACzK,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,6EAA6E;IAC7E,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;IACtC,oFAAoF;IACpF,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;IAC/B,wFAAwF;IACxF,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IACnC,uEAAuE;IACvE,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAC/B;AAED;;;;;;GAMG;AAGH,qBAAa,aAAa;IAGZ,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuC;gBAE/B,IAAI,GAAE,MAAkB;IAErD;;;OAGG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,iBAAiB;CAuCxE"}
package/lib/tracker.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Pure prefix-stability tracking for the prompt-cache health observer.
3
+ *
4
+ * The wire prefix of a model request is system → tools → messages, in order.
5
+ * Each segment is canonical-JSON serialized (recursive key sort) and hashed
6
+ * with sha256; per session we keep the previous call's hash list and report
7
+ * the longest common prefix. Hashing stops at the first difference (early
8
+ * exit), so a drift near the front of a long conversation is cheap.
9
+ *
10
+ * No cordis, fs, or dsh imports beyond dsh-llm types — fully unit-testable.
11
+ *
12
+ * @module @dsh-cc/cache-health/tracker
13
+ */
14
+ import { createHash } from 'node:crypto';
15
+ /** sha256 hex, first 16 chars — the shared project-key shape (context-crusher idiom). */
16
+ export function shortHash(input) {
17
+ return createHash('sha256').update(input, 'utf8').digest('hex').slice(0, 16);
18
+ }
19
+ /** Default segment hash: sha256 hex. */
20
+ export const sha256Hex = (text) => createHash('sha256').update(text, 'utf8').digest('hex');
21
+ /**
22
+ * Canonical JSON: deterministic, key-sorted serialization for hashing.
23
+ * Objects sort keys lexicographically; arrays keep order; `undefined`
24
+ * properties are dropped (JSON semantics). No dependency — ~20 lines.
25
+ */
26
+ export function canonicalJson(value) {
27
+ return serialize(value);
28
+ }
29
+ function serialize(value) {
30
+ if (value === null || typeof value !== 'object')
31
+ return JSON.stringify(value) ?? 'null';
32
+ if (Array.isArray(value))
33
+ return `[${value.map(serialize).join(',')}]`;
34
+ const keys = Object.keys(value).sort();
35
+ const parts = [];
36
+ for (const key of keys) {
37
+ const item = value[key];
38
+ if (item === undefined)
39
+ continue;
40
+ parts.push(`${JSON.stringify(key)}:${serialize(item)}`);
41
+ }
42
+ return `{${parts.join(',')}}`;
43
+ }
44
+ /**
45
+ * Build the drift excerpt: canonical-serialize, collapse whitespace runs,
46
+ * redact secret-shaped runs, truncate to 80 chars.
47
+ * @param text - the canonical serialization of the drifted segment source.
48
+ */
49
+ export function excerpt(text) {
50
+ const collapsed = text.replace(/\s+/g, ' ');
51
+ const redacted = collapsed
52
+ .replace(/sk-[A-Za-z0-9]{8,}/g, '[redacted]')
53
+ .replace(/Bearer\s+\S+/gi, '[redacted]')
54
+ .replace(/[A-Za-z0-9+/=_-]{32,}/g, '[redacted]');
55
+ return redacted.length > 80 ? redacted.slice(0, 80) : redacted;
56
+ }
57
+ /**
58
+ * Per-session in-memory prefix tracker. One instance per mounted plugin.
59
+ * Segment 0 = system, 1 = tools, 2+ = messages in order. On drift the hash
60
+ * list stops at the drifted index, so the NEXT call verifies only that
61
+ * (shorter) prefix — stableSegments may temporarily under-count after a
62
+ * drift; `prefixChanged` stays correct.
63
+ */
64
+ // ponytail: truncating the stored hash list after drift under-counts the next
65
+ // call's stableSegments; store unhashed stubs if that ever matters.
66
+ export class PrefixTracker {
67
+ hash;
68
+ prev = new Map();
69
+ constructor(hash = sha256Hex) {
70
+ this.hash = hash;
71
+ }
72
+ /**
73
+ * Observe one model request for a session. Purely read-only on `options`;
74
+ * never mutates the request.
75
+ */
76
+ observe(sessionId, options) {
77
+ const sources = [options.system ?? '', options.tools ?? [], ...options.messages];
78
+ const prev = this.prev.get(sessionId) ?? [];
79
+ const hashes = [];
80
+ let stable = 0;
81
+ let tokens = 0;
82
+ let prefixChanged = false;
83
+ let driftSegmentIndex;
84
+ let driftExcerpt;
85
+ for (let i = 0; i < sources.length; i++) {
86
+ const text = canonicalJson(sources[i]);
87
+ if (i < prev.length) {
88
+ const h = this.hash(text);
89
+ if (h === prev[i]) {
90
+ stable += 1;
91
+ tokens += text.length;
92
+ hashes.push(h);
93
+ continue;
94
+ }
95
+ // First difference: report and stop hashing (early exit).
96
+ prefixChanged = true;
97
+ driftSegmentIndex = i;
98
+ driftExcerpt = excerpt(text);
99
+ break;
100
+ }
101
+ // New tail segment beyond the previous list: extend, no bust.
102
+ hashes.push(this.hash(text));
103
+ }
104
+ this.prev.set(sessionId, hashes);
105
+ const stableCount = prefixChanged ? stable : hashes.length;
106
+ return {
107
+ stableSegments: stableCount,
108
+ stablePrefixHash: this.hash(hashes.slice(0, stableCount).join('\n')),
109
+ stablePrefixTokensEst: Math.round(tokens / 4),
110
+ prefixChanged,
111
+ ...(driftSegmentIndex !== undefined ? { driftSegmentIndex } : {}),
112
+ ...(driftExcerpt !== undefined ? { driftExcerpt } : {}),
113
+ };
114
+ }
115
+ }
116
+ //# sourceMappingURL=tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker.js","sourceRoot":"","sources":["../src/tracker.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@dsh-cc/cache-health",
3
+ "description": "Passive prompt-cache prefix-stability observer with the /cache-health slash command",
4
+ "version": "0.6.2",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/dsh-cc/dsh-cc.git",
8
+ "directory": "packages/observability/cache-health"
9
+ },
10
+ "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./lib/index.d.ts",
14
+ "default": "./lib/index.js"
15
+ },
16
+ "./tracker": {
17
+ "types": "./lib/tracker.d.ts",
18
+ "default": "./lib/tracker.js"
19
+ },
20
+ "./ledger": {
21
+ "types": "./lib/ledger.d.ts",
22
+ "default": "./lib/ledger.js"
23
+ },
24
+ "./report": {
25
+ "types": "./lib/report.d.ts",
26
+ "default": "./lib/report.js"
27
+ },
28
+ "./src/*": "./src/*",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "files": [
32
+ "lib"
33
+ ],
34
+ "license": "Apache-2.0",
35
+ "dependencies": {
36
+ "@dsh-cc/command-usage": "^0.6.2"
37
+ },
38
+ "peerDependencies": {
39
+ "@deepseek-ai/cordis": ">=0.1.2-rc.1",
40
+ "@deepseek-ai/dsh-commands": ">=0.1.2-rc.1",
41
+ "@deepseek-ai/dsh-llm": ">=0.1.2-rc.1",
42
+ "@deepseek-ai/dsh-session": ">=0.1.2-rc.1",
43
+ "@deepseek-ai/schemastery": ">=0.1.2-rc.1"
44
+ },
45
+ "devDependencies": {
46
+ "@deepseek-ai/cordis": "link:../../../../deepseek-harness/vendor/cordis",
47
+ "@deepseek-ai/cordis-plugin-loader": "link:../../../../deepseek-harness/vendor/loader",
48
+ "@deepseek-ai/dsh-agent": "link:../../../../deepseek-harness/packages/core/agent",
49
+ "@deepseek-ai/dsh-commands": "link:../../../../deepseek-harness/packages/interaction/commands",
50
+ "@deepseek-ai/dsh-llm": "link:../../../../deepseek-harness/packages/llm/llm",
51
+ "@deepseek-ai/dsh-session": "link:../../../../deepseek-harness/packages/core/session",
52
+ "@deepseek-ai/schemastery": "link:../../../../deepseek-harness/vendor/schemastery"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ }
57
+ }