dsh-context-mode 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/lib/types/cjk.d.ts +11 -7
- package/lib/types/cjk.d.ts.map +1 -1
- package/lib/types/cjk.js +10 -6
- package/lib/types/precompact.d.ts +29 -0
- package/lib/types/precompact.d.ts.map +1 -1
- package/lib/types/precompact.js +77 -11
- package/package.json +2 -1
- package/scripts/cleanup-injected.mjs +135 -0
- package/vendor/context-mode/server.bundle.mjs +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,38 @@ only accepts its own platform ids, and `pi` is its neutral MCP-only id — every
|
|
|
92
92
|
store stays DSH-owned: `CONTEXT_MODE_DIR` isolates DSH data and
|
|
93
93
|
`CONTEXT_MODE_PROJECT_DIR` pins project hashing to the configured workspace.
|
|
94
94
|
|
|
95
|
+
## Compaction archiving
|
|
96
|
+
|
|
97
|
+
Compaction replaces the live conversation with a generated summary and prunes
|
|
98
|
+
the events behind it, so anything the summary omits leaves the model's reach.
|
|
99
|
+
The plugin listens for `compaction/start` and files the transcript into the
|
|
100
|
+
knowledge base first, where `ctx_search` can still reach it afterwards.
|
|
101
|
+
|
|
102
|
+
Storage is layered rather than filtered — every transcript event is archived,
|
|
103
|
+
and the layers differ only in the `source` label they carry, so precision is
|
|
104
|
+
chosen at query time instead of at write time:
|
|
105
|
+
|
|
106
|
+
| Source | Contents |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `session/<id>/constraint` | user messages — requirements, decisions, limits |
|
|
109
|
+
| `session/<id>/finding` | tool results and assistant prose stating a concrete value |
|
|
110
|
+
| `session/<id>/narrative` | remaining assistant prose — reasoning, plans |
|
|
111
|
+
|
|
112
|
+
Nothing is dropped at write, so a misclassification costs a query's precision
|
|
113
|
+
rather than the content itself. Harness-injected blocks (`<active_memory>`,
|
|
114
|
+
`<current_runtime_context>`, `<system-reminder>`, `<resume_snapshot>`) are the
|
|
115
|
+
one exception: they ride on `user/message`, are per-turn runtime noise rather
|
|
116
|
+
than transcript, and are discarded before layering. Set `precompact: false` to
|
|
117
|
+
turn archiving off entirely.
|
|
118
|
+
|
|
119
|
+
Archives written before 0.3.2 may contain those injected blocks. A one-shot
|
|
120
|
+
cleanup script removes them and leaves everything else alone:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
node node_modules/dsh-context-mode/scripts/cleanup-injected.mjs --db <path> # report only
|
|
124
|
+
node node_modules/dsh-context-mode/scripts/cleanup-injected.mjs --db <path> --apply # delete
|
|
125
|
+
```
|
|
126
|
+
|
|
95
127
|
## Development
|
|
96
128
|
|
|
97
129
|
```sh
|
package/lib/types/cjk.d.ts
CHANGED
|
@@ -14,13 +14,17 @@
|
|
|
14
14
|
*
|
|
15
15
|
* - writes segment CJK runs into single characters separated by spaces,
|
|
16
16
|
* which makes `unicode61` emit one token per character;
|
|
17
|
-
* - queries segment identically
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Phrase semantics
|
|
22
|
-
*
|
|
23
|
-
*
|
|
17
|
+
* - queries segment identically, so "缓存方案" becomes the tokens
|
|
18
|
+
* `缓 存 方 案` and the searcher can match them.
|
|
19
|
+
*
|
|
20
|
+
* The query side deliberately does NOT wrap the segmented run in a phrase.
|
|
21
|
+
* Phrase semantics were tried first and made retrieval worse: a document
|
|
22
|
+
* saying "缓存走本地文件" and a query saying "缓存方案用什么" share the
|
|
23
|
+
* prefix but diverge immediately, so an adjacency requirement rejects the
|
|
24
|
+
* result a caller actually wanted. Emitting single tokens instead lets
|
|
25
|
+
* upstream's `sanitizeQuery` build an AND expression, and BM25 ranks the
|
|
26
|
+
* document that shares more characters first, which is the ranking a
|
|
27
|
+
* character-based index can honestly provide. See {@link buildCjkQuery}.
|
|
24
28
|
*/
|
|
25
29
|
/** Return whether a string contains any character that needs segmentation. */
|
|
26
30
|
export declare function hasCjk(value: string): boolean;
|
package/lib/types/cjk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cjk.d.ts","sourceRoot":"","sources":["../../src/cjk.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cjk.d.ts","sourceRoot":"","sources":["../../src/cjk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAOH,8EAA8E;AAC9E,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGhD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD"}
|
package/lib/types/cjk.js
CHANGED
|
@@ -14,13 +14,17 @@
|
|
|
14
14
|
*
|
|
15
15
|
* - writes segment CJK runs into single characters separated by spaces,
|
|
16
16
|
* which makes `unicode61` emit one token per character;
|
|
17
|
-
* - queries segment identically
|
|
18
|
-
*
|
|
19
|
-
* matches documents where those characters appear adjacently.
|
|
17
|
+
* - queries segment identically, so "缓存方案" becomes the tokens
|
|
18
|
+
* `缓 存 方 案` and the searcher can match them.
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* The query side deliberately does NOT wrap the segmented run in a phrase.
|
|
21
|
+
* Phrase semantics were tried first and made retrieval worse: a document
|
|
22
|
+
* saying "缓存走本地文件" and a query saying "缓存方案用什么" share the
|
|
23
|
+
* prefix but diverge immediately, so an adjacency requirement rejects the
|
|
24
|
+
* result a caller actually wanted. Emitting single tokens instead lets
|
|
25
|
+
* upstream's `sanitizeQuery` build an AND expression, and BM25 ranks the
|
|
26
|
+
* document that shares more characters first, which is the ranking a
|
|
27
|
+
* character-based index can honestly provide. See {@link buildCjkQuery}.
|
|
24
28
|
*/
|
|
25
29
|
/** Han, Hiragana, Katakana, and Hangul ranges that need segmentation. */
|
|
26
30
|
const CJK_PATTERN = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
|
|
@@ -53,6 +53,12 @@ interface ArchivedLine {
|
|
|
53
53
|
/**
|
|
54
54
|
* Install the pre-compaction archiver.
|
|
55
55
|
*
|
|
56
|
+
* The listener buffers every session event as it arrives and flushes the
|
|
57
|
+
* buffer when compaction begins. Buffering rather than reading the transcript
|
|
58
|
+
* at compaction time matters: `compaction/prune` drops the events behind the
|
|
59
|
+
* summary, and it may run before an asynchronous archive reads them. A flush
|
|
60
|
+
* from our own buffer cannot race that prune.
|
|
61
|
+
*
|
|
56
62
|
* @param ctx - plugin context carrying the session event bus.
|
|
57
63
|
* @param getClient - resolves the live MCP client, or undefined when the bridge is down.
|
|
58
64
|
* @param options - enablement and size guard.
|
|
@@ -69,6 +75,29 @@ export declare function installPrecompactArchive(ctx: Context, getClient: () =>
|
|
|
69
75
|
* reached are searchable beside the evidence.
|
|
70
76
|
*/
|
|
71
77
|
export declare function classify(events: readonly SessionEventLike[]): ArchivedLine[];
|
|
78
|
+
/**
|
|
79
|
+
* Whether a message body is harness-injected context rather than transcript.
|
|
80
|
+
*
|
|
81
|
+
* DSH attaches `<current_runtime_context>`, `<active_memory>`,
|
|
82
|
+
* `<system-reminder>`, and `<resume_snapshot>` blocks to user messages, so
|
|
83
|
+
* they arrive with the same `user/message` type as a genuine user turn. They
|
|
84
|
+
* are per-turn runtime noise, not requirements or decisions: filing them under
|
|
85
|
+
* `constraint` both dilutes that layer and returns stale policy snapshots for
|
|
86
|
+
* policy-shaped queries. `<active_memory>` is also a second-hand summary of
|
|
87
|
+
* events that are archived directly, so keeping it would store the same facts
|
|
88
|
+
* twice.
|
|
89
|
+
*
|
|
90
|
+
* The check has to cover two shapes. The tag form is what the model sees when
|
|
91
|
+
* a block is inlined whole, but `textOf` reads only the text blocks of a
|
|
92
|
+
* message, so a block's opening tag can be stripped before this point and the
|
|
93
|
+
* body then begins with the injected block's own heading — the transcripts
|
|
94
|
+
* this was written against start with "Current runtime context." rather than
|
|
95
|
+
* with a tag. Matching the headings as well keeps those from being filed.
|
|
96
|
+
*
|
|
97
|
+
* A body qualifies only when an injected marker *starts* the message, so a
|
|
98
|
+
* user who quotes one of these tags mid-sentence is still archived.
|
|
99
|
+
*/
|
|
100
|
+
export declare function isInjectedContext(text: string): boolean;
|
|
72
101
|
/**
|
|
73
102
|
* Whether assistant prose states a value worth retrieving on its own.
|
|
74
103
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"precompact.d.ts","sourceRoot":"","sources":["../../src/precompact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAGrD,+DAA+D;AAC/D,eAAO,MAAM,MAAM;;;;CAIT,CAAA;AAEV,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,OAAO,MAAM,CAAC,CAAA;AAE5D,qDAAqD;AACrD,MAAM,WAAW,iBAAiB;IAChC,iCAAiC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,iFAAiF;IACjF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CACnC;AAED,UAAU,gBAAgB;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CACxB;AAYD,6EAA6E;AAC7E,UAAU,YAAY;IACpB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;
|
|
1
|
+
{"version":3,"file":"precompact.d.ts","sourceRoot":"","sources":["../../src/precompact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAGrD,+DAA+D;AAC/D,eAAO,MAAM,MAAM;;;;CAIT,CAAA;AAEV,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,OAAO,MAAM,CAAC,CAAA;AAE5D,qDAAqD;AACrD,MAAM,WAAW,iBAAiB;IAChC,iCAAiC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,iFAAiF;IACjF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CACnC;AAED,UAAU,gBAAgB;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CACxB;AAYD,6EAA6E;AAC7E,UAAU,YAAY;IACpB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAcD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,MAAM,cAAc,GAAG,SAAS,EAC3C,OAAO,GAAE,iBAAsB,GAC9B,MAAM,IAAI,CAiCZ;AA+CD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,GAAG,YAAY,EAAE,CAW5E;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD;AAoBD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAO1D;AAcD,mFAAmF;AACnF,wBAAgB,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAwBtD;AAWD,wEAAwE;AACxE,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3C;AAaD,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GAAG,YAAY,CAAA"}
|
package/lib/types/precompact.js
CHANGED
|
@@ -31,9 +31,23 @@ export const LAYERS = {
|
|
|
31
31
|
narrative: 'narrative',
|
|
32
32
|
};
|
|
33
33
|
const DEFAULT_MAX_CHARS_PER_LAYER = 120_000;
|
|
34
|
+
/** Events that carry transcript value and are therefore buffered. */
|
|
35
|
+
const CARRIES_TRANSCRIPT = new Set([
|
|
36
|
+
'user/message',
|
|
37
|
+
'assistant/message',
|
|
38
|
+
'tool/result',
|
|
39
|
+
]);
|
|
40
|
+
/** Buffered events per session before the oldest are dropped. */
|
|
41
|
+
const MAX_BUFFERED_EVENTS = 2_000;
|
|
34
42
|
/**
|
|
35
43
|
* Install the pre-compaction archiver.
|
|
36
44
|
*
|
|
45
|
+
* The listener buffers every session event as it arrives and flushes the
|
|
46
|
+
* buffer when compaction begins. Buffering rather than reading the transcript
|
|
47
|
+
* at compaction time matters: `compaction/prune` drops the events behind the
|
|
48
|
+
* summary, and it may run before an asynchronous archive reads them. A flush
|
|
49
|
+
* from our own buffer cannot race that prune.
|
|
50
|
+
*
|
|
37
51
|
* @param ctx - plugin context carrying the session event bus.
|
|
38
52
|
* @param getClient - resolves the live MCP client, or undefined when the bridge is down.
|
|
39
53
|
* @param options - enablement and size guard.
|
|
@@ -43,29 +57,49 @@ export function installPrecompactArchive(ctx, getClient, options = {}) {
|
|
|
43
57
|
if (options.enabled === false)
|
|
44
58
|
return () => { };
|
|
45
59
|
const maxCharsPerLayer = options.maxCharsPerLayer ?? DEFAULT_MAX_CHARS_PER_LAYER;
|
|
60
|
+
// Buffered transcript per session, plus the compaction points already filed.
|
|
61
|
+
const buffers = new WeakMap();
|
|
46
62
|
const archived = new Set();
|
|
47
63
|
return ctx.on('session/event', (session, event) => {
|
|
48
|
-
|
|
64
|
+
const key = session;
|
|
65
|
+
if (event.type === 'compaction/start') {
|
|
66
|
+
const buffered = buffers.get(key) ?? [];
|
|
67
|
+
// Clear before the async flush: a second start event for the same
|
|
68
|
+
// compaction must not file the same content twice.
|
|
69
|
+
buffers.set(key, []);
|
|
70
|
+
void archive(session, buffered, getClient, maxCharsPerLayer, archived).catch(() => {
|
|
71
|
+
// Archiving is a best-effort passenger on the compaction path; a
|
|
72
|
+
// failure here must never surface in the compaction that triggered it.
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!CARRIES_TRANSCRIPT.has(event.type))
|
|
77
|
+
return;
|
|
78
|
+
const buffer = buffers.get(key);
|
|
79
|
+
if (buffer === undefined) {
|
|
80
|
+
buffers.set(key, [event]);
|
|
49
81
|
return;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
82
|
+
}
|
|
83
|
+
buffer.push(event);
|
|
84
|
+
// Bound the buffer so a session that never compacts cannot grow forever.
|
|
85
|
+
if (buffer.length > MAX_BUFFERED_EVENTS)
|
|
86
|
+
buffer.splice(0, buffer.length - MAX_BUFFERED_EVENTS);
|
|
54
87
|
});
|
|
55
88
|
}
|
|
56
|
-
/**
|
|
57
|
-
async function archive(session, getClient, maxCharsPerLayer, archived) {
|
|
89
|
+
/** Classify the buffered transcript and file each layer into the knowledge base. */
|
|
90
|
+
async function archive(session, buffered, getClient, maxCharsPerLayer, archived) {
|
|
58
91
|
const client = getClient();
|
|
59
92
|
if (client === undefined)
|
|
60
93
|
return;
|
|
61
94
|
const key = sessionId(session);
|
|
62
|
-
|
|
63
|
-
// compaction must not duplicate the content.
|
|
64
|
-
const stamp = `${key}:${session.seq ?? session.snapshotEvents().length}`;
|
|
95
|
+
const stamp = `${key}:${session.seq ?? buffered.length}`;
|
|
65
96
|
if (archived.has(stamp))
|
|
66
97
|
return;
|
|
67
98
|
archived.add(stamp);
|
|
68
|
-
|
|
99
|
+
// Fall back to the live transcript when nothing was buffered — a plugin
|
|
100
|
+
// mounted mid-session has no history of its own but the log is still whole.
|
|
101
|
+
const events = buffered.length > 0 ? buffered : session.snapshotEvents();
|
|
102
|
+
const lines = classify(events);
|
|
69
103
|
if (lines.length === 0)
|
|
70
104
|
return;
|
|
71
105
|
const grouped = group(lines, maxCharsPerLayer);
|
|
@@ -102,6 +136,8 @@ export function classify(events) {
|
|
|
102
136
|
const text = textOf(event);
|
|
103
137
|
if (text.length === 0)
|
|
104
138
|
continue;
|
|
139
|
+
if (isInjectedContext(text))
|
|
140
|
+
continue;
|
|
105
141
|
const layer = layerOf(event.type, text);
|
|
106
142
|
if (layer === undefined)
|
|
107
143
|
continue;
|
|
@@ -109,6 +145,36 @@ export function classify(events) {
|
|
|
109
145
|
}
|
|
110
146
|
return lines;
|
|
111
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Whether a message body is harness-injected context rather than transcript.
|
|
150
|
+
*
|
|
151
|
+
* DSH attaches `<current_runtime_context>`, `<active_memory>`,
|
|
152
|
+
* `<system-reminder>`, and `<resume_snapshot>` blocks to user messages, so
|
|
153
|
+
* they arrive with the same `user/message` type as a genuine user turn. They
|
|
154
|
+
* are per-turn runtime noise, not requirements or decisions: filing them under
|
|
155
|
+
* `constraint` both dilutes that layer and returns stale policy snapshots for
|
|
156
|
+
* policy-shaped queries. `<active_memory>` is also a second-hand summary of
|
|
157
|
+
* events that are archived directly, so keeping it would store the same facts
|
|
158
|
+
* twice.
|
|
159
|
+
*
|
|
160
|
+
* The check has to cover two shapes. The tag form is what the model sees when
|
|
161
|
+
* a block is inlined whole, but `textOf` reads only the text blocks of a
|
|
162
|
+
* message, so a block's opening tag can be stripped before this point and the
|
|
163
|
+
* body then begins with the injected block's own heading — the transcripts
|
|
164
|
+
* this was written against start with "Current runtime context." rather than
|
|
165
|
+
* with a tag. Matching the headings as well keeps those from being filed.
|
|
166
|
+
*
|
|
167
|
+
* A body qualifies only when an injected marker *starts* the message, so a
|
|
168
|
+
* user who quotes one of these tags mid-sentence is still archived.
|
|
169
|
+
*/
|
|
170
|
+
export function isInjectedContext(text) {
|
|
171
|
+
return INJECTED_CONTEXT_PATTERN.test(text);
|
|
172
|
+
}
|
|
173
|
+
const INJECTED_CONTEXT_PATTERN = new RegExp('^\\s*(?:' +
|
|
174
|
+
'<(?:current_runtime_context|active_memory|system-reminder|resume_snapshot)\\b' +
|
|
175
|
+
'|Current runtime context\\b' +
|
|
176
|
+
'|The available skill catalog changed\\b' +
|
|
177
|
+
')');
|
|
112
178
|
/** Return the layer for one event, or undefined when it carries no transcript value. */
|
|
113
179
|
function layerOf(type, text) {
|
|
114
180
|
if (type === 'user/message')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-context-mode",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Expose context-mode MCP tools as native DeepSeek Harness tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"lib",
|
|
32
|
+
"scripts/cleanup-injected.mjs",
|
|
32
33
|
"skills",
|
|
33
34
|
"vendor/context-mode/server.bundle.mjs",
|
|
34
35
|
"vendor/context-mode/LICENSE",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Remove harness-injected context blocks that earlier builds filed as
|
|
4
|
+
* constraints.
|
|
5
|
+
*
|
|
6
|
+
* Before `isInjectedContext` existed, `precompact` classified purely on event
|
|
7
|
+
* type, so the `<current_runtime_context>` / `<active_memory>` blocks DSH
|
|
8
|
+
* attaches to user messages were archived as `session/<id>/constraint` — the
|
|
9
|
+
* layer meant to hold requirements and decisions. Those rows are runtime
|
|
10
|
+
* noise, and they are stale policy snapshots besides.
|
|
11
|
+
*
|
|
12
|
+
* Scope is deliberately narrow. A row is only a candidate when it lives in an
|
|
13
|
+
* archived session layer AND carries an injected marker immediately after the
|
|
14
|
+
* archive's own `## [layer] party (seq N)` heading. A document or source file
|
|
15
|
+
* that merely *mentions* `active_memory` (the plugin's own sources do) keeps
|
|
16
|
+
* its row, because such a match is never preceded by an archive heading.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* node scripts/cleanup-injected.mjs --db <path> [--apply]
|
|
20
|
+
*
|
|
21
|
+
* Without `--apply` nothing is written; the script only reports what it would
|
|
22
|
+
* delete. Both FTS5 tables are cleaned together, since `chunks` and
|
|
23
|
+
* `chunks_trigram` hold the same logical rows and would otherwise disagree.
|
|
24
|
+
*/
|
|
25
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
26
|
+
import { existsSync } from 'node:fs'
|
|
27
|
+
|
|
28
|
+
const args = process.argv.slice(2)
|
|
29
|
+
const apply = args.includes('--apply')
|
|
30
|
+
const dbIndex = args.indexOf('--db')
|
|
31
|
+
const dbPath = dbIndex >= 0 ? args[dbIndex + 1] : undefined
|
|
32
|
+
|
|
33
|
+
if (dbPath === undefined) {
|
|
34
|
+
console.error('usage: node scripts/cleanup-injected.mjs --db <path> [--apply]')
|
|
35
|
+
process.exit(2)
|
|
36
|
+
}
|
|
37
|
+
if (!existsSync(dbPath)) {
|
|
38
|
+
console.error(`database not found: ${dbPath}`)
|
|
39
|
+
process.exit(2)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Markers that identify a harness-injected block, matched after the heading. */
|
|
43
|
+
const MARKERS = [
|
|
44
|
+
'<current_runtime_context',
|
|
45
|
+
'<active_memory',
|
|
46
|
+
'<system-reminder',
|
|
47
|
+
'<resume_snapshot',
|
|
48
|
+
'Current runtime context',
|
|
49
|
+
'The available skill catalog changed',
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
/** Archive headings look like `## [约束] 用户 (seq 2766)`. */
|
|
53
|
+
const heading = '## [%'
|
|
54
|
+
const patterns = MARKERS.map(marker => `${heading}%${marker}%`)
|
|
55
|
+
|
|
56
|
+
/** Build the content predicate for one table alias. */
|
|
57
|
+
const clausesFor = alias => MARKERS.map(() => `(${alias}.content LIKE ?)`).join(' OR ')
|
|
58
|
+
|
|
59
|
+
const selectSql = `
|
|
60
|
+
SELECT c.rowid AS rowid, s.label AS label, c.content AS content
|
|
61
|
+
FROM chunks c
|
|
62
|
+
JOIN sources s ON s.id = c.source_id
|
|
63
|
+
WHERE s.label LIKE 'session/%'
|
|
64
|
+
AND (${clausesFor('c')})
|
|
65
|
+
`
|
|
66
|
+
|
|
67
|
+
const db = new DatabaseSync(dbPath)
|
|
68
|
+
const rows = db.prepare(selectSql).all(...patterns)
|
|
69
|
+
|
|
70
|
+
// `chunks_trigram` mirrors `chunks`; the same logical row has a different
|
|
71
|
+
// rowid per table, so the trigram side is matched by content within the same
|
|
72
|
+
// session-scoped sources.
|
|
73
|
+
const trigramSql = `
|
|
74
|
+
SELECT t.rowid AS rowid, s.label AS label, t.content AS content
|
|
75
|
+
FROM chunks_trigram t
|
|
76
|
+
JOIN sources s ON s.id = t.source_id
|
|
77
|
+
WHERE s.label LIKE 'session/%'
|
|
78
|
+
AND (${clausesFor('t')})
|
|
79
|
+
`
|
|
80
|
+
const trigramRows = db.prepare(trigramSql).all(...patterns)
|
|
81
|
+
|
|
82
|
+
console.log(`mode: ${apply ? 'APPLY' : 'DRY RUN'}`)
|
|
83
|
+
console.log(`database: ${dbPath}`)
|
|
84
|
+
console.log(`chunks rows matched: ${rows.length}`)
|
|
85
|
+
console.log(`chunks_trigram rows matched: ${trigramRows.length}`)
|
|
86
|
+
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
const preview = row.content.replace(/\s+/g, ' ').slice(0, 96)
|
|
89
|
+
console.log(` [${row.rowid}] ${row.label}\n ${preview}`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (rows.length === 0 && trigramRows.length === 0) {
|
|
93
|
+
console.log('\nnothing to clean')
|
|
94
|
+
db.close()
|
|
95
|
+
process.exit(0)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!apply) {
|
|
99
|
+
console.log('\ndry run only — re-run with --apply to delete these rows')
|
|
100
|
+
db.close()
|
|
101
|
+
process.exit(0)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
db.exec('BEGIN')
|
|
105
|
+
try {
|
|
106
|
+
const delChunks = db.prepare('DELETE FROM chunks WHERE rowid = ?')
|
|
107
|
+
for (const row of rows) delChunks.run(row.rowid)
|
|
108
|
+
const delTrigram = db.prepare('DELETE FROM chunks_trigram WHERE rowid = ?')
|
|
109
|
+
for (const row of trigramRows) delTrigram.run(row.rowid)
|
|
110
|
+
db.exec('COMMIT')
|
|
111
|
+
} catch (error) {
|
|
112
|
+
db.exec('ROLLBACK')
|
|
113
|
+
console.error('cleanup failed, rolled back:', error)
|
|
114
|
+
db.close()
|
|
115
|
+
process.exit(1)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// `sources.chunk_count` is a cached tally; recompute it for the labels touched
|
|
119
|
+
// so the bookkeeping matches the rows that remain.
|
|
120
|
+
const touched = new Set([...rows, ...trigramRows].map(row => row.label))
|
|
121
|
+
const recount = db.prepare(`
|
|
122
|
+
UPDATE sources
|
|
123
|
+
SET chunk_count = (
|
|
124
|
+
SELECT COUNT(*) FROM chunks c WHERE c.source_id = sources.id
|
|
125
|
+
)
|
|
126
|
+
WHERE label = ?
|
|
127
|
+
`)
|
|
128
|
+
for (const label of touched) recount.run(label)
|
|
129
|
+
|
|
130
|
+
// Reclaim the space freed by the deletes.
|
|
131
|
+
db.exec("INSERT INTO chunks(chunks) VALUES('optimize')")
|
|
132
|
+
db.exec("INSERT INTO chunks_trigram(chunks_trigram) VALUES('optimize')")
|
|
133
|
+
|
|
134
|
+
console.log(`\ndeleted ${rows.length} + ${trigramRows.length} rows, recounted ${touched.size} source(s)`)
|
|
135
|
+
db.close()
|
|
@@ -582,7 +582,7 @@ ${s}`}}import{execFileSync as WN}from"node:child_process";function GN(){if(proce
|
|
|
582
582
|
WHERE tool IN ('ctx_search', 'ctx_fetch_and_index')`).get();m?.bytes&&(a+=Number(m.bytes))}catch{}}}finally{h.close()}}catch{}}let u=0;t.sessionId&&t.contentDbPath&&(u=UD(t.sessionId,t.contentDbPath,{loadDatabase:t.loadDatabase}),i+=u);let l=Math.floor((o+i+c)/4);return{eventDataBytes:o,bytesAvoided:i,bytesReturned:a,snapshotBytes:c,contentBytes:u,totalSavedTokens:l}}function am(t){let e=pc({worktreeHash:t.worktreeHash,sessionsDir:t.sessionsDir}),r=pc({sessionId:t.sessionId,worktreeHash:t.worktreeHash,sessionsDir:t.sessionsDir,contentDbPath:t.contentDbPath}),n=r.bytesReturned,s=e.bytesAvoided+e.bytesReturned,o=Math.max(0,s-n);return{eventDataBytes:e.eventDataBytes,bytesAvoided:o,bytesReturned:n,snapshotBytes:e.snapshotBytes,contentBytes:r.contentBytes,totalSavedTokens:Math.floor((e.eventDataBytes+o+e.snapshotBytes)/4)}}var qD={minEvents:100,minProjects:5,recencyMs:30*864e5,minAvgBytes:50};function BD(t,e,r){let n={name:t.name,eventCount:0,sessionCount:0,dataBytes:0,rescueBytes:0,contentBytes:0,uuidConvs:0,projectDirs:[],firstMs:Number.POSITIVE_INFINITY,lastMs:0,isReal:!1};if(!_r(t.sessionsDir))return n;let s=[];try{s=ks(t.sessionsDir).filter(l=>l.endsWith(".db"))}catch{return n}if(s.length===0)return n;let o=null;try{o=e()}catch{return n}if(!o)return n;let i=new Set,a=new Set;for(let l of s){let d=vt(t.sessionsDir,l);try{let f=new o(d,{readonly:!0});try{let h=f.prepare("SELECT COUNT(*) AS cnt, COALESCE(SUM(LENGTH(data)), 0) AS bytes FROM session_events").get();h&&(n.eventCount+=Number(h.cnt??0),n.dataBytes+=Number(h.bytes??0));try{let p=f.prepare("SELECT COUNT(*) AS cnt FROM session_meta").get();n.sessionCount+=Number(p?.cnt??0)}catch{}try{let p=f.prepare("SELECT COALESCE(SUM(length(snapshot)), 0) AS bytes FROM session_resume WHERE consumed = 1").get();p?.bytes&&(n.rescueBytes+=Number(p.bytes))}catch{}try{let p=f.prepare("SELECT MIN(created_at) AS mn, MAX(created_at) AS mx FROM session_events").get();if(p?.mn){let m=Date.parse(p.mn+(p.mn.endsWith("Z")?"":"Z"));Number.isFinite(m)&&m<n.firstMs&&(n.firstMs=m)}if(p?.mx){let m=Date.parse(p.mx+(p.mx.endsWith("Z")?"":"Z"));Number.isFinite(m)&&m>n.lastMs&&(n.lastMs=m)}}catch{}try{let p=f.prepare("SELECT DISTINCT project_dir AS p FROM session_events WHERE project_dir != ''").all();for(let m of p)m.p&&i.add(m.p)}catch{}try{let p=f.prepare("SELECT DISTINCT session_id AS s FROM session_events").all();for(let m of p)m.s&&a.add(m.s)}catch{}}finally{f.close()}}catch{}}n.projectDirs=Array.from(i),n.uuidConvs=a.size;let c=n.eventCount>0?n.dataBytes/n.eventCount:0,u=n.lastMs>0&&r.nowMs-n.lastMs<=r.recencyMs;return n.isReal=n.eventCount>=r.minEvents&&i.size>=r.minProjects&&u&&c>=r.minAvgBytes,n}function gc(t){let e=FD({home:t?.home}),r=t?.loadDatabase??He,n={...qD,...t?.filter??{},nowMs:t?.filter?.nowMs??Date.now()},s=[],o=0,i=0,a=0;for(let c of e){if(!_r(c.sessionsDir))continue;let u=BD(c,r,n);s.push(u),o+=u.eventCount,i+=u.sessionCount,a+=u.dataBytes+u.rescueBytes}return{totalEvents:o,totalSessions:i,totalBytes:a,perAdapter:s}}var SS={project:"What you're building",feedback:"How you work",user:"Who you are",reference:"Where to look",memory:"Long-term context",other:"Other notes"},HD={"claude-code":"Claude Code","gemini-cli":"Gemini CLI",antigravity:"Antigravity","antigravity-cli":"Antigravity CLI",openclaw:"Openclaw",codex:"Codex CLI",cursor:"Cursor","vscode-copilot":"VS Code Copilot","copilot-cli":"GitHub Copilot CLI",kiro:"Kiro",pi:"Pi",omp:"OMP","qwen-code":"Qwen Code",kilo:"Kilo",opencode:"OpenCode",zed:"Zed","jetbrains-copilot":"JetBrains"};function fc(t){return HD[t]??t}function Ve(t){if(!Number.isFinite(t)||t<=0)return"0 B";if(t<1024)return`${Math.round(t)} B`;let e=t/1024;if(e<1024)return e<100?`${e.toFixed(1)} KB`:`${Math.round(e)} KB`;let r=e/1024;if(r<1024)return r<100?`${r.toFixed(1)} MB`:`${Math.round(r)} MB`;let n=r/1024;return n<100?`${n.toFixed(2)} GB`:`${n.toFixed(1)} GB`}function VD(t){let e=parseFloat(t);if(isNaN(e)||e<1)return"< 1 min";if(e<60)return`${Math.round(e)} min`;let r=Math.floor(e/60),n=Math.round(e%60);return n>0?`${r}h ${n}m`:`${r}h`}function lc(t){if(!t)return!1;try{return Intl.DateTimeFormat.supportedLocalesOf(t).length===0?!1:(new Intl.DateTimeFormat(t),!0)}catch{return!1}}function WD(){let t=process.env??{},e=t.CONTEXT_MODE_LOCALE??"";if(e&&!lc(e)&&(e=""),!e){if(process.platform==="darwin"){try{let n=zD("defaults",["read","-g","AppleLocale"],{encoding:"utf8",timeout:500}).trim();n&&(e=n.replace(/_/g,"-"))}catch{}e&&!lc(e)&&(e="")}if(!e&&(t.LC_TIME||t.LANG)){let n=(t.LC_TIME||t.LANG||"").split(".")[0];n&&(e=n.replace(/_/g,"-")),e&&!lc(e)&&(e="")}if(!e)try{e=new Intl.DateTimeFormat().resolvedOptions().locale}catch{e="en-US"}}let r=t.CONTEXT_MODE_TZ??"";if(!r)try{r=new Intl.DateTimeFormat().resolvedOptions().timeZone}catch{r="UTC"}return lc(e)||(e="en-US"),{locale:e,tz:r||"UTC"}}function pS(t){let e=hc();return e?t===e?"~":t.startsWith(e+LD)?"~"+t.slice(e.length):t:t}function GD(t,e,r){if(!Number.isFinite(e)||e<=0)return[];let n=e*ni(),s=(y,b=2)=>y.toFixed(b),o=Math.round(n/20),i=(n/200).toFixed(1),a=Math.round(n/73.67),c=Math.round(n*10),u=r>0?Math.round(n*10/r*365):0,l=(e*3/1e6).toFixed(2),d=(e*2.5/1e6).toFixed(2),f=(e*1.25/1e6).toFixed(2),h=(e*1/1e6).toFixed(2),p=process.env.PI_CONTEXT_MODE_PRICE_OUTPUT_PER_TOKEN!==void 0,m=process.env.PI_CONTEXT_MODE_MODEL_ID,g=[];return p&&m?g.push(` $${s(n)} of ${m} tokens your team didn't burn.`):p?g.push(` $${s(n)} of tokens your team didn't burn.`):g.push(` $${s(n)} of Opus 4.7 tokens your team didn't burn.`),g.push(` context-mode kept ${Ve(t)} out of context \u2014 that's ${o} months of Cursor Pro paid for itself.`),c>0&&u>0&&(g.push(""),g.push(` Scale across a 10-dev team and that's ~$${u.toLocaleString("en-US")}/year saved.`)),p||(g.push(""),g.push(" (Opus rates shown for context. On cheaper models the dollar number drops; the savings ratio holds.)")),g}function KD(t){let{conversation:e,lifetime:r,multiAdapter:n,realBytes:s,cwd:o,locale:i,tz:a,now:c,version:u,latestVersion:l}=t,d=[],f=e.events*yS,h=Math.round((e.snapshotBytes??0)/4),p=f+h,m=s?.conversation?.totalSavedTokens??0,g=Math.max(p,m),y=(r?.totalEvents??0)*yS,b=Math.round((r?.rescueBytes??0)/4),_=y+b,x=s?.lifetime?.totalSavedTokens??0,w=Math.max(_,x),I=s?.lifetime?.bytesReturned??0,R=s?.lifetime?.bytesAvoided??0,M=I+R>0?Math.max(1,Math.floor(I/4)):Math.max(1,Math.round(w*.02)),z=n?.totalBytes&&n.totalBytes>0?n.totalBytes:w*4,W=s?.conversation?s.conversation.eventDataBytes+s.conversation.bytesAvoided+s.conversation.snapshotBytes:g*4,$=e.daysAlive>=1?`${e.daysAlive.toFixed(1)} days alive \xB7 still going`:`${Math.max(1,Math.round(e.daysAlive*24))} hr alive \xB7 still going`,T=r?.firstEventMs??n?.perAdapter?.[0]?.firstMs??0,F=T>0?Math.max(1,Math.round((c-T)/864e5)):0,de=n?.totalSessions??r?.totalSessions??1,nt=n?.perAdapter.filter(Pe=>Pe.isReal).length??0,ir;if(n&&nt>=2)ir=`across ${nt} AI tools`;else if(n&&nt===1){let Pe=n.perAdapter.find(Ct=>Ct.isReal);ir=`in ${Pe?fc(Pe.name):"Claude Code"}`}else ir="in Claude Code";F>0?d.push(` Across ${F} days you ran ${qt(de)} conversations ${ir}.`):d.push(` You ran ${qt(de)} conversations ${ir}.`);let Ac=F>0?z/F:0;d.push(` context-mode kept ${Ve(z)} out of your context window \u2014 about ${Ve(Ac)} every single day.`),d.push(""),d.push(""),d.push(" \u2500\u2500\u2500 1. Where you are now \u2500\u2500\u2500"),d.push("");let Em=e.firstEventMs&&e.firstEventMs>0?fS(e.firstEventMs,i,a):"";if(Em?d.push(` This conversation started ${Em} in ${pS(o)}.`):d.push(` This conversation lives in ${pS(o)}.`),d.push(` ${$}.`),e.snapshotsConsumed>0&&e.snapshotBytes>0){let Pe=e.lastRescueMs&&e.lastRescueMs>0?fS(e.lastRescueMs,i,a):"",Ct=Math.round(e.snapshotBytes/1024);Pe?d.push(` On ${Pe}, /compact fired \u2014 ${Ct} KB rescued from snapshot.`):d.push(` /compact fired \u2014 ${Ct} KB rescued from snapshot.`),d.push(" Without that, you'd be re-explaining everything to a blank model right now.")}d.push("");let km=s?.conversation,Tm=km?.bytesAvoided??0,Nc=km?.bytesReturned??0;if(Tm+Nc===0)d.push(" No measurable redirect activity captured yet \u2014 bars will appear once context-mode diverts its first payload."),d.push("");else{let Pe=Tm+Nc,Ct=Math.max(1,Nc),It=Math.max(1,Math.floor(Pe/4)),ar=Math.max(1,Math.floor(Ct/4)),Dc=Ur(It,It,32),XS=Ur(ar,It,32),YS=(1-ar/It)*100,QS=Math.max(1,Math.round(It/ar));d.push(` Without context-mode ${Ve(Pe).padStart(8)} ${Dc} ${qt(It).padStart(7)} tokens`),d.push(` With context-mode ${Ve(Ct).padStart(8)} ${XS} ${qt(ar).padStart(7)} tokens`),d.push(` ${YS.toFixed(1)}% kept out of context \xB7 your AI ran ${QS}\xD7 longer before /compact fired`),d.push("")}if(e.byDay&&e.byDay.length>0){let Pe=e.lastEventMs&&e.firstEventMs?Math.max(1,Math.round((e.lastEventMs-e.firstEventMs)/864e5)+1):e.byDay.length;d.push(` How that ${Ve(W)} built up \u2014 ${Pe} days, ${e.byDay.length} active:`),d.push(""),d.push(...XD(e.byDay,i,a))}d.push(""),d.push(""),d.push(" \u2500\u2500\u2500 2. What this chat captured (used when you --continue or /resume here) \u2500\u2500\u2500"),d.push("");let WS=e.byCategory.reduce((Pe,Ct)=>Pe+Ct.count,0).toLocaleString(i);d.push(` ${WS} things \u2014 files, errors, decisions, agent runs:`),d.push("");let GS=e.byCategory[0]?.count??1;for(let Pe of e.byCategory)d.push(` ${Pe.label.padEnd(26)} ${String(Pe.count).padStart(5)} ${Ur(Pe.count,GS,28)}`);d.push(""),d.push(""),d.push(" \u2500\u2500\u2500 3. The scope, getting wider \u2500\u2500\u2500"),d.push("");let $m=e.firstEventMs&&e.firstEventMs>0?new Intl.DateTimeFormat(i,{timeZone:a,year:"numeric",month:"short",day:"numeric"}).format(new Date(e.firstEventMs)):"",Rm=T>0?new Intl.DateTimeFormat(i,{timeZone:a,year:"numeric",month:"short",day:"numeric"}).format(new Date(T)):"",Pm=r?.distinctProjects??0,KS=r?.totalEvents??n?.totalEvents??0;if(d.push(` This chat: ${Ve(W)} kept out \xB7 ${e.events.toLocaleString(i)} captures${$m?` \xB7 started ${$m}`:""}.`),d.push(` All your work: ${Ve(z)} kept out \xB7 ${KS.toLocaleString(i)} captures across ${Pm} project${Pm===1?"":"s"}${Rm?` \xB7 since ${Rm}`:""}.`),d.push(""),d.push(""),d.push(" \u2500\u2500\u2500 4. The bottom line \u2500\u2500\u2500"),d.push(""),d.push(...GD(z,w,F)),d.push(""),d.push(""),d.push(" \u2500\u2500\u2500 5. What context-mode learned about how you work \u2500\u2500\u2500"),d.push(""),r&&r.autoMemoryCount>0){d.push(` ${r.autoMemoryCount} preferences picked up across ${r.autoMemoryProjects} project${r.autoMemoryProjects===1?"":"s"}:`);let Pe=Object.entries(r.autoMemoryByPrefix).sort((It,ar)=>ar[1]-It[1]),Ct=Pe.length>0?Pe[0][1]:1;for(let[It,ar]of Pe){let Dc=SS[It]??It;d.push(` ${Dc.padEnd(26)} ${String(ar).padStart(2)} ${Ur(ar,Ct,20)}`)}}else d.push(" No preferences learned yet \u2014 context-mode picks them up automatically.");d.push(""),d.push(""),d.push(" Your AI talks less, remembers more, costs less."),d.push(` Locale ${i} \xB7 timezone ${a} \xB7 pricing examples for illustration only.`),d.push("");let JS=u?`v${u}`:"context-mode";return d.push(` ${JS}`),u&&l&&l!=="unknown"&&bS(l,u)&&d.push(` Update available: v${u} -> v${l} | ctx_upgrade`),JD(d)}function JD(t){let e=[],r=0;for(let n of t)n===""?(r++,r<=2&&e.push(n)):(r=0,e.push(n));for(;e.length>0&&e[e.length-1]==="";)e.pop();return e}function XD(t,e,r){if(t.length===0)return[];let n=[...t].sort((f,h)=>f.ms-h.ms),s=n[0],o=n[n.length-1],i=Math.max(1,o.ms-s.ms),a=n[0];for(let f of n)f.count>a.count&&(a=f);let c=56,u=Array.from({length:c},()=>"\u2500");for(let f of n){let h=Math.round((f.ms-s.ms)/i*(c-1)),p="\u25CF";f===a&&(p="\u2588"),(f.rescueBytes??0)>0&&(p="\u25C6"),u[h]=p}let l=f=>{let h=new Intl.DateTimeFormat(e,{timeZone:r,month:"short",day:"numeric"}).formatToParts(new Date(f)),p=(h.find(g=>g.type==="month")?.value??"").toLowerCase(),m=h.find(g=>g.type==="day")?.value??"";return`${p} ${m}`},d=[];d.push(` ${l(s.ms)} ${u.join("")} ${l(o.ms)}`),d.push("");for(let f of n){let h=l(f.ms).padEnd(7),p=`${f.count} captures`,m=f===a?" \u2190 peak":"",g=(f.rescueBytes??0)>0?` \u25C6 /compact rescued ${Math.round((f.rescueBytes??0)/1024)} KB`:"";d.push(` ${h} ${p}${m}${g}`)}return d.push(""),d.push(" \u25CF active day \u2588 peak day \u25C6 /compact rescue"),d}function fS(t,e,r){if(!Number.isFinite(t)||t<=0)return"";let n=new Date(t);if(Number.isNaN(n.getTime()))return"";let s=new Intl.DateTimeFormat(e,{timeZone:r,year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",hour12:!1}).formatToParts(n),o=d=>s.find(f=>f.type===d)?.value??"",i=o("day"),a=o("month"),c=o("year"),u=o("hour"),l=o("minute");return u==="24"&&(u="00"),`${i} ${a} ${c} at ${u}:${l} (${r})`}function qt(t){return t>=1e6?`${(t/1e6).toFixed(1)}M`:t>=1e3?`${(t/1e3).toFixed(1)}K`:String(t)}function ni(){let t=process.env.PI_CONTEXT_MODE_PRICE_OUTPUT_PER_TOKEN;if(t!==void 0&&t!==""){let e=Number(t);if(Number.isFinite(e)&&e>0)return e}return 5/1e6}var eB=5/1e6;function mc(t){return`$${((Number.isFinite(t)&&t>0?t:0)*ni()).toFixed(2)}`}function Ur(t,e,r=40){if(e<=0)return"\u2591".repeat(r);let n=Math.max(1,Math.round(t/e*r));return"\u2588".repeat(Math.min(n,r))+"\u2591".repeat(Math.max(0,r-n))}function mS(t,e){let r=e?.sessionTokensSaved??0;if(t.total_events===0&&(e?.lifetime?.totalEvents??0)===0&&r===0&&(e?.multiAdapter?.totalEvents??0)===0)return[];let n=e?.topN??Number.POSITIVE_INFINITY,s=[];s.push("");let o=e?.multiAdapter,i=o?.perAdapter.filter(m=>m.isReal).length??0,a=o?.totalEvents??e?.lifetime?.totalEvents??t.total_events,c=o?.totalSessions??e?.lifetime?.totalSessions??t.session_count,u=e?.lifetime?.distinctProjects;if(a>0&&u&&u>0){let m=i>=2?" everywhere":"";s.push(` All your work${m} \xB7 ${qt(a)} events captured across ${u} project${u===1?"":"s"} \xB7 ${qt(c)} conversations`)}else{s.push("Persistent memory \u2713 preserved across compact, restart & upgrade");let m=c===0&&r>0?1:c,g=m===1?"1 session":`${qt(m)} sessions`,y=a*256+r;s.push(` ${qt(a)} events \xB7 ${g} \xB7 ~${mc(y)} saved lifetime`)}s.push("");let l=e?.lifetime?.categoryCounts,d;l&&Object.keys(l).length>0?d=Object.entries(l).filter(([,m])=>m>0).map(([m,g])=>({category:m,count:g,label:dc[m]||m})).sort((m,g)=>g.count-m.count):d=(t.by_category??[]).filter(m=>m&&m.count>0);let f=d.slice(0,n),h=f.length>0?f[0].count:1;for(let m of f)s.push(` ${m.label.padEnd(26)} ${String(m.count).padStart(5)} ${Ur(m.count,h,30)}`);let p=Math.max(0,d.length-n);return p>0&&s.push(` ... ${p} more categor${p===1?"y":"ies"}`),s}function hS(t){if(!t||t.autoMemoryCount===0)return[];let e=[];e.push(""),e.push(` Preferences learned \xB7 ${t.autoMemoryCount} across ${t.autoMemoryProjects} project${t.autoMemoryProjects===1?"":"s"}`);let r=Object.entries(t.autoMemoryByPrefix).sort((s,o)=>o[1]-s[1]).slice(0,6),n=r.length>0?r[0][1]:1;for(let[s,o]of r){let i=SS[s]??s;e.push(` ${i.padEnd(26)} ${String(o).padStart(2)} ${Ur(o,n,20)}`)}return e}function gS(t,e){let r=[],n=mc(t),s=(e?.totalEvents??0)*256+t,o=mc(s);return r.push(""),r.push("\u2500".repeat(65)),r.push("Your AI talks less, remembers more, costs less."),r.push(`${n} this session \xB7 ${o} lifetime`),r.push("\u2500".repeat(65)),r}var yS=256;function _S(t){if(!t)return[];let e=[],r=[];for(let s of t.perAdapter)(s.isReal?e:r).push(s);if(e.length===0&&r.length===0)return[];let n=[];if(e.length>0){n.push(""),n.push("Where it came from (tools you actually used \u2014 fixtures + probes filtered):"),n.push("");let s=16,o=10,i=10,a=16;n.push(` ${"Tool".padEnd(s)}${"Captures".padStart(o)}${"Indexed".padStart(i)}${"Total kept out".padStart(a)}`);let c=[...e].sort((u,l)=>l.dataBytes+l.rescueBytes-(u.dataBytes+u.rescueBytes));for(let u of c){let l=u.dataBytes+u.rescueBytes,d=u.eventCount>0?qt(u.eventCount):"\u2014",f=Ve(u.dataBytes),h=Ve(l);n.push(` ${fc(u.name).padEnd(s)}${d.padStart(o)}${f.padStart(i)}${h.padStart(a)}`)}}if(r.length>0){e.length>0&&n.push("");let s=r.map(o=>fc(o.name)).join(", ");n.push(` Skipped (${r.length}): ${s}`),n.push(" These adapters have DBs on disk but only test fixtures, dev skeletons,"),n.push(" or detection probes \u2014 no real chat activity.")}return n}function yc(t,e,r,n){let s=[],o=VD(t.session.uptime_min),i=n?.lifetime,a=n?.mcpUsage,c=n?.conversation,u=n?.realBytes,l=n?.multiAdapter,d=l?.perAdapter.filter(I=>I.isReal).length??0;if(l&&d>0){let I=l.totalSessions||i?.totalSessions||0,R=i?.firstEventMs??0,M=R>0?Math.max(1,Math.round((Date.now()-R)/864e5)):0,z=M>0?`Across ${M} day${M===1?"":"s"} `:"",W=I>0?`you ran ${qt(I)} conversation${I===1?"":"s"} `:"you ran ",$;if(d>=2)$=`across ${d} AI tools`;else{let T=l.perAdapter.find(F=>F.isReal);$=`in ${T?fc(T.name):"Claude Code"}`}s.push(`${z}${W}${$}.`),s.push("")}if(c&&c.events>0){s.length>0&&(s.length=0);let I=WD(),R=n?.cwd??process.cwd(),M=n?.now??Date.now(),z=n?.locale??I.locale,W=n?.tz??I.tz;return s.push(...KD({conversation:c,lifetime:i,multiAdapter:l,realBytes:u,cwd:R,locale:z,tz:W,now:M,version:e,latestVersion:r})),s.join(`
|
|
583
583
|
`)}let f=t.savings.kept_out+(t.cache?t.cache.bytes_saved:0),h=t.savings.total_bytes_returned,p=t.savings.total_calls,m=f+h,g=m>0?f/m*100:0,y=Math.round(f/4),b=h>0?Math.max(1,Math.round(m/Math.max(h,1))):0;if(f===0){s.push(`context-mode ${o} ${p} calls`),s.push(""),p===0?s.push("No tool calls yet. Use batch_execute or execute to start saving tokens."):s.push(`${Ve(h)} entered context | 0 tokens saved`),s.push(...mS(t.projectMemory,{lifetime:i,multiAdapter:l,sessionTokensSaved:0})),s.push(..._S(l)),s.push(...hS(i)),s.push(...gS(0,i)),s.push("");let I=e?`v${e}`:"context-mode";return s.push(I),e&&r&&r!=="unknown"&&bS(r,e)&&s.push(`Update available: v${e} -> v${r} | ctx_upgrade`),s.join(`
|
|
584
584
|
`)}s.push(`${qt(y)} tokens saved \xB7 ${g.toFixed(1)}% reduction \xB7 ${o} \xB7 ~${mc(y)} saved (Opus)`),s.push(""),s.push(`Without context-mode |${Ur(m,m)}| ${Ve(m)}`),s.push(`With context-mode |${Ur(h,m)}| ${Ve(h)}`),s.push(""),b>=2?s.push(`${Ve(f)} kept out of your conversation \u2014 ${b}\xD7 longer sessions before compact.`):s.push(`${Ve(f)} kept out of your conversation. Never entered context.`),s.push("");let _=[`${p} calls`];t.cache&&t.cache.hits>0&&_.push(`${t.cache.hits} cache hits (+${Ve(t.cache.bytes_saved)})`),s.push(_.join(" \xB7 "));let x=t.savings.by_tool.filter(I=>I.calls>0);if(x.length>=2){s.push("");let I=x.map(R=>{let M=R.context_kb*1024,z=g<100?M/(1-g/100):M,W=Math.max(0,z-M);return{...R,returnedBytes:M,estimatedSaved:W}}).sort((R,M)=>M.estimatedSaved-R.estimatedSaved);for(let R of I){let M=R.tool.length>22?R.tool.slice(0,19)+"...":R.tool;s.push(` ${M.padEnd(22)} ${String(R.calls).padStart(4)} calls ${Ve(R.estimatedSaved).padStart(8)} saved`)}}if(a&&a.length>0){let I=a.filter(R=>R.median_concurrency!=null&&(R.max_concurrency??1)>1);if(I.length>0){s.push(""),s.push("Parallel I/O \u2713 one call did the work of many \u2014 faster runs, lower bill, same answer.");for(let R of I){let M=R.tool_name.replace(/^mcp__.*?__/,"");s.push(` ${M.padEnd(22)} ${R.calls} batches \xB7 ${R.median_concurrency} typical, ${R.max_concurrency} peak`)}}}s.push(...mS(t.projectMemory,{lifetime:i,multiAdapter:l,sessionTokensSaved:y})),s.push(..._S(l)),s.push(...hS(i)),s.push(...gS(y,i)),s.push("");let w=e?`v${e}`:"context-mode";return s.push(w),e&&r&&r!=="unknown"&&r!==e&&s.push(`Update available: v${e} -> v${r} | ctx_upgrade`),s.join(`
|
|
585
|
-
`)}var xc=Os(sz(import.meta.url)),sr=(()=>{let t="0.3.
|
|
585
|
+
`)}var xc=Os(sz(import.meta.url)),sr=(()=>{let t="0.3.1";if(t!==void 0&&t.trim().length>0)return t.trim();for(let e of["../../package.json","../package.json","./package.json"]){let r=rt(xc,e);if(Ie(r))try{return JSON.parse(oi(r,"utf8")).version}catch{}}return"unknown"})(),Ic=process.env.CONTEXT_MODE_UPSTREAM_CHECK==="1";function LS(){return Ie(rt(xc,"package.json"))?xc:Os(xc)}function cz(t){try{let e=process.platform==="win32"?kc("cmd.exe",["/d","/s","/c","codex plugin list"],{encoding:"utf-8",stdio:["ignore","pipe","ignore"],timeout:5e3}):kc("codex",["plugin","list"],{encoding:"utf-8",stdio:["ignore","pipe","ignore"],timeout:5e3});if(e.status!==0)return t;let r=Lf(String(e.stdout));if(r&&Ie(rt(r,".codex-plugin","hooks.json")))return r}catch{}return t}function jS(t){let e=LS();return t==="codex"?cz(e):e}process.env.CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS!=="1"&&(process.on("unhandledRejection",t=>{process.stderr.write(`[context-mode] unhandledRejection: ${t}
|
|
586
586
|
`)}),process.on("uncaughtException",t=>{try{YD(2,`[context-mode] uncaughtException: ${t?.message??t}
|
|
587
587
|
`)}finally{process.exit(1)}}));var An=Va(),Sc=Hv(An),Se=new Ua({name:"context-mode",version:sr}),uz=[];function lz(t={}){if((t.embedded??process.env.CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS)==="1")return!1;let r=t.platform??mt().platform;if(r!=="opencode"&&r!=="kilo")return!1;let n=t.settings??dz(r);return pz(n)&&fz(n)}function dz(t){let e=t==="kilo"?"kilo":"opencode",r=[rt(`${e}.json`),rt(`${e}.jsonc`),rt(`.${e}`,`${e}.json`),rt(`.${e}`,`${e}.jsonc`),gt(Is(),".config",e,`${e}.json`),gt(Is(),".config",e,`${e}.jsonc`)];for(let n of r)try{if(!Ie(n))continue;return JSON.parse(uS(oi(n,"utf8")))}catch{}return null}function pz(t){let e=t?.plugin;return Array.isArray(e)&&e.some(r=>typeof r=="string"&&r.includes("context-mode"))}function fz(t){let e=t?.mcp;return!!(e&&typeof e=="object"&&!Array.isArray(e)&&Object.prototype.hasOwnProperty.call(e,"context-mode"))}var FS=lz(),pm=!1;function mz(t={}){if(pm)return;pm=!0;let e=t.write??(n=>{process.stderr.write(n)}),r=t.platform??"opencode/kilo";e(`[context-mode] ctx_* tools/list intentionally empty on this MCP child: legacy mcp.context-mode block coexists with plugin: ["context-mode"] in ${r}.json \u2014 plugin-native tools are the supported path (#623). Run \`context-mode upgrade\` to remove the legacy block (preserves other MCP servers).
|
|
588
588
|
`)}function ZB(){pm=!1}function hz(t=Se){t.server.registerCapabilities({tools:{listChanged:!1}}),t.server.setRequestHandler(mn,async()=>({tools:[]}))}var gz=Se.registerTool.bind(Se);Se.registerTool=(...t)=>{let[e,r,n]=t;if(FS){mz();return}let s=yz(e,n);return uz.push({name:e,config:r,handler:s}),t[2]=s,gz(...t)};function yz(t,e){return async r=>{$x();try{return await e(r)}catch(n){let s=Ez(n);if(s)try{return V(t,s)}catch(o){if(o instanceof Zr)return s;throw o}throw n}finally{Rx()}}}FS&&process.env.CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS!=="1"&&hz(Se);var _m=new az;async function UB(t,e){let r=typeof t=="string"?{projectDir:t}:t;return _m.run(r,e)}Se.server.registerCapabilities({prompts:{listChanged:!1},resources:{listChanged:!1}});Se.server.setRequestHandler(Yn,async()=>({prompts:[]}));Se.server.setRequestHandler(Jn,async()=>({resources:[]}));Se.server.setRequestHandler(Xn,async()=>({resourceTemplates:[]}));function fm(t){if(Array.isArray(t))return t.map(fm);if(t===null||typeof t!="object")return t;let e={};for(let[r,n]of Object.entries(t))if(r!=="additionalProperties"){if(r==="const"){e.enum=[n];continue}e[r]=fm(n)}return e}function _z(t=Se){try{let r=t.server._requestHandlers?.get("tools/list");if(typeof r!="function")return;t.server.setRequestHandler(mn,async(n,s)=>{let o=await r(n,s);if(o&&Array.isArray(o.tools)){for(let i of o.tools)if(!(!i||i.inputSchema==null))try{i.inputSchema=fm(i.inputSchema)}catch{}}return o})}catch{}}var ii=new qo({runtimes:An,projectRoot:()=>ht()}),Oc=gt(ym(),`cm-fs-preload-${process.pid}.js`);Ec(Oc,`(function(){var __cm_fs=0;process.on('exit',function(){if(__cm_fs>0)try{process.stderr.write('__CM_FS__:'+__cm_fs+'\\n')}catch(e){}});try{var f=require('fs');var ors=f.readFileSync;f.readFileSync=function(){var r=ors.apply(this,arguments);if(Buffer.isBuffer(r))__cm_fs+=r.length;else if(typeof r==='string')__cm_fs+=Buffer.byteLength(r);return r;};}catch(e){}})();
|