@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 +201 -0
- package/README.i18n.yaml +6 -0
- package/README.md +32 -0
- package/README.zh.md +32 -0
- package/lib/index.d.ts +77 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +120 -0
- package/lib/index.js.map +1 -0
- package/lib/ledger.d.ts +53 -0
- package/lib/ledger.d.ts.map +1 -0
- package/lib/ledger.js +89 -0
- package/lib/ledger.js.map +1 -0
- package/lib/report.d.ts +64 -0
- package/lib/report.d.ts.map +1 -0
- package/lib/report.js +144 -0
- package/lib/report.js.map +1 -0
- package/lib/tracker.d.ts +72 -0
- package/lib/tracker.d.ts.map +1 -0
- package/lib/tracker.js +116 -0
- package/lib/tracker.js.map +1 -0
- package/package.json +57 -0
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.
|
package/README.i18n.yaml
ADDED
|
@@ -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
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/ledger.d.ts
ADDED
|
@@ -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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"}
|
package/lib/report.d.ts
ADDED
|
@@ -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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"}
|
package/lib/tracker.d.ts
ADDED
|
@@ -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
|
+
}
|