dsh-context-mode 0.3.1 → 0.3.3
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 +74 -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/compaction.d.ts +100 -0
- package/lib/types/compaction.d.ts.map +1 -0
- package/lib/types/compaction.js +94 -0
- package/lib/types/precompact.d.ts +23 -0
- package/lib/types/precompact.d.ts.map +1 -1
- package/lib/types/precompact.js +32 -0
- package/package.json +15 -2
- package/scripts/cleanup-injected.mjs +135 -0
package/README.md
CHANGED
|
@@ -92,6 +92,80 @@ 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
|
+
|
|
127
|
+
## Checkpoint archive index
|
|
128
|
+
|
|
129
|
+
A checkpoint keeps only a summary of the span it replaces, and the shipped
|
|
130
|
+
checkpoint format has no section for tool output — a long tool result survives
|
|
131
|
+
only as whatever the summarizing model chose to keep. `precompact` files that
|
|
132
|
+
same span into the knowledge base beforehand, but nothing told the model so,
|
|
133
|
+
and a summary that omits a detail reads as if the detail never existed.
|
|
134
|
+
|
|
135
|
+
This package also exports a compaction engine that closes that gap. It extends
|
|
136
|
+
the shipped `BasicCompactionEngine` and overrides only `summarize()`, appending
|
|
137
|
+
an `## Archive Index` that names the archived sources:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import DshContextModeCompaction from 'dsh-context-mode/compaction'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Mount it in place of the shipped backend, inside the isolate group the shipped
|
|
144
|
+
one requires:
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
- id: compaction
|
|
148
|
+
name: cordis:group
|
|
149
|
+
group: true
|
|
150
|
+
isolate:
|
|
151
|
+
compaction: true
|
|
152
|
+
toolResultPruner: true
|
|
153
|
+
config:
|
|
154
|
+
- id: compaction-basic
|
|
155
|
+
name: 'dsh-context-mode/compaction' # was @deepseek-ai/dsh-compaction-basic
|
|
156
|
+
|
|
157
|
+
- id: command-compact
|
|
158
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Trigger policy, retention, the transaction bracket, token metering, and
|
|
162
|
+
prefix-cache-aligned replay all stay on the shipped engine — only the returned
|
|
163
|
+
summary text differs. The index is appended to the summary rather than injected
|
|
164
|
+
into the summarization instruction, so the shipped output contract is untouched
|
|
165
|
+
and no model has to follow an amended format. Failing to build the index
|
|
166
|
+
returns the superseded summary unchanged, and restoring the shipped `name` row
|
|
167
|
+
disables the index entirely.
|
|
168
|
+
|
|
95
169
|
## Development
|
|
96
170
|
|
|
97
171
|
```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]/;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction backend that appends an archive index to every checkpoint.
|
|
3
|
+
*
|
|
4
|
+
* DSH's shipped backend condenses an older span of the conversation into a
|
|
5
|
+
* summary and lets the raw events fall out of the derived history. The summary
|
|
6
|
+
* is lossy by design: its instruction fixes the sections a checkpoint may
|
|
7
|
+
* carry, and tool output has no section of its own, so a long tool result
|
|
8
|
+
* survives only as whatever the summarizing model chose to keep.
|
|
9
|
+
*
|
|
10
|
+
* This plugin's `precompact` listener files the same span into the
|
|
11
|
+
* context-mode knowledge base first, so the detail still exists and
|
|
12
|
+
* `ctx_search` can reach it. What it cannot do from outside is tell the model
|
|
13
|
+
* that — the checkpoint text is written here, inside the engine, and the
|
|
14
|
+
* shipped engine has no reason to mention a knowledge base it does not know
|
|
15
|
+
* about. That is the gap this subclass closes.
|
|
16
|
+
*
|
|
17
|
+
* Two deliberate choices keep the risk low:
|
|
18
|
+
*
|
|
19
|
+
* - Only `summarize()` is overridden. Trigger policy, retention, the
|
|
20
|
+
* bracket-first transaction, token metering, and KV-cache-aligned replay
|
|
21
|
+
* all stay on the shipped implementation, so this cannot diverge from the
|
|
22
|
+
* behavior the rest of DSH expects.
|
|
23
|
+
* - The index is appended to the *returned* summary, never injected into the
|
|
24
|
+
* summarization instruction. The summarizing model never sees this text,
|
|
25
|
+
* so the shipped output contract ("keep every section, in order") stays
|
|
26
|
+
* intact and no model has to be trusted to follow an amended format.
|
|
27
|
+
*
|
|
28
|
+
* Nothing here is allowed to fail a compaction: if the index cannot be built,
|
|
29
|
+
* the superseded summary is returned unchanged.
|
|
30
|
+
*
|
|
31
|
+
* @module dsh-context-mode/compaction
|
|
32
|
+
*/
|
|
33
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
34
|
+
import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic';
|
|
35
|
+
/**
|
|
36
|
+
* The shipped summarization input and result types, derived from the base
|
|
37
|
+
* class rather than restated.
|
|
38
|
+
*
|
|
39
|
+
* The package does not re-export them from its root, and deep-importing its
|
|
40
|
+
* private `lib/types/summarizer.js` path would break on any internal move.
|
|
41
|
+
* Deriving from the method signature keeps this module aligned with whatever
|
|
42
|
+
* the installed version declares, and needs no import of its own.
|
|
43
|
+
*/
|
|
44
|
+
type SummarizeArgs = Parameters<BasicCompactionEngine['summarize']>;
|
|
45
|
+
type SummarizedResult = Awaited<ReturnType<BasicCompactionEngine['summarize']>>;
|
|
46
|
+
/** Session-event shapes this module reads. */
|
|
47
|
+
interface SessionEventLike {
|
|
48
|
+
readonly type: string;
|
|
49
|
+
readonly seq?: number;
|
|
50
|
+
readonly data?: unknown;
|
|
51
|
+
}
|
|
52
|
+
interface SessionLike {
|
|
53
|
+
readonly id?: string;
|
|
54
|
+
snapshotEvents(): readonly SessionEventLike[];
|
|
55
|
+
}
|
|
56
|
+
interface AgentLike {
|
|
57
|
+
readonly session?: SessionLike;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A compaction engine that names the session archive in each checkpoint.
|
|
61
|
+
*
|
|
62
|
+
* Constructed by DSH exactly like the shipped engine it extends, so the row
|
|
63
|
+
* that mounts it needs no additional wiring.
|
|
64
|
+
*/
|
|
65
|
+
export declare class DshContextModeCompaction extends BasicCompactionEngine {
|
|
66
|
+
/**
|
|
67
|
+
* Summarize the replayed region, then append the archive index.
|
|
68
|
+
*
|
|
69
|
+
* The index is appended after `super.summarize()` resolves, so the shipped
|
|
70
|
+
* call — and therefore prefix-cache alignment, token accounting, and the
|
|
71
|
+
* returned `SummaryResult` envelope — are unchanged.
|
|
72
|
+
*/
|
|
73
|
+
protected summarize(input: SummarizeArgs[0], agent: SummarizeArgs[1], signal?: SummarizeArgs[2]): Promise<SummarizedResult>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build the archive-index block for one agent's session.
|
|
77
|
+
*
|
|
78
|
+
* The source labels are derivable without waiting on the archiver: the
|
|
79
|
+
* `precompact` listener files each layer under `session/<id>/<layer>`, and the
|
|
80
|
+
* session id is available here. The index therefore states *where* the detail
|
|
81
|
+
* lives rather than claiming anything about what it contains.
|
|
82
|
+
*
|
|
83
|
+
* @param agent - owner of the session being compacted.
|
|
84
|
+
* @returns the markdown block, or an empty string when no session is reachable.
|
|
85
|
+
*/
|
|
86
|
+
export declare function buildArchiveIndex(agent: AgentLike): string;
|
|
87
|
+
/**
|
|
88
|
+
* Append an index block to the text of a summary.
|
|
89
|
+
*
|
|
90
|
+
* Only text blocks are touched. A summary may also carry non-text blocks, and
|
|
91
|
+
* rewriting or dropping those is the shipped engine's business, not this
|
|
92
|
+
* module's; they are copied through untouched.
|
|
93
|
+
*
|
|
94
|
+
* @param summary - the superseded summary blocks.
|
|
95
|
+
* @param index - the block to append.
|
|
96
|
+
* @returns new blocks with the index appended to the trailing text.
|
|
97
|
+
*/
|
|
98
|
+
export declare function appendToSummary(summary: readonly ContentBlock[], index: string): ContentBlock[];
|
|
99
|
+
export default DshContextModeCompaction;
|
|
100
|
+
//# sourceMappingURL=compaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/compaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAA;AAEzE;;;;;;;;GAQG;AACH,KAAK,aAAa,GAAG,UAAU,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC,CAAA;AACnE,KAAK,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAA;AAQ/E,8CAA8C;AAC9C,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;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,IAAI,SAAS,gBAAgB,EAAE,CAAA;CAC9C;AAED,UAAU,SAAS;IACjB,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAA;CAC/B;AAED;;;;;GAKG;AACH,qBAAa,wBAAyB,SAAQ,qBAAqB;IACjE;;;;;;OAMG;cACsB,SAAS,CAChC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EACvB,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EACvB,MAAM,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,gBAAgB,CAAC;CAY7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAuB1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,KAAK,EAAE,MAAM,GACZ,YAAY,EAAE,CAUhB;AAED,eAAe,wBAAwB,CAAA"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic';
|
|
2
|
+
/** Heading of the appended section. Kept short; it costs context on every later request. */
|
|
3
|
+
const INDEX_HEADING = '## Archive Index';
|
|
4
|
+
/** Upper bound on the appended section, so a checkpoint can never be grown without limit. */
|
|
5
|
+
const MAX_INDEX_CHARS = 1_200;
|
|
6
|
+
/**
|
|
7
|
+
* A compaction engine that names the session archive in each checkpoint.
|
|
8
|
+
*
|
|
9
|
+
* Constructed by DSH exactly like the shipped engine it extends, so the row
|
|
10
|
+
* that mounts it needs no additional wiring.
|
|
11
|
+
*/
|
|
12
|
+
export class DshContextModeCompaction extends BasicCompactionEngine {
|
|
13
|
+
/**
|
|
14
|
+
* Summarize the replayed region, then append the archive index.
|
|
15
|
+
*
|
|
16
|
+
* The index is appended after `super.summarize()` resolves, so the shipped
|
|
17
|
+
* call — and therefore prefix-cache alignment, token accounting, and the
|
|
18
|
+
* returned `SummaryResult` envelope — are unchanged.
|
|
19
|
+
*/
|
|
20
|
+
async summarize(input, agent, signal) {
|
|
21
|
+
const result = await super.summarize(input, agent, signal);
|
|
22
|
+
try {
|
|
23
|
+
const index = buildArchiveIndex(agent);
|
|
24
|
+
if (index.length === 0)
|
|
25
|
+
return result;
|
|
26
|
+
return { ...result, summary: appendToSummary(result.summary, index) };
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// An index is an improvement, never a requirement: a failure here must
|
|
30
|
+
// not turn into a failed compaction.
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build the archive-index block for one agent's session.
|
|
37
|
+
*
|
|
38
|
+
* The source labels are derivable without waiting on the archiver: the
|
|
39
|
+
* `precompact` listener files each layer under `session/<id>/<layer>`, and the
|
|
40
|
+
* session id is available here. The index therefore states *where* the detail
|
|
41
|
+
* lives rather than claiming anything about what it contains.
|
|
42
|
+
*
|
|
43
|
+
* @param agent - owner of the session being compacted.
|
|
44
|
+
* @returns the markdown block, or an empty string when no session is reachable.
|
|
45
|
+
*/
|
|
46
|
+
export function buildArchiveIndex(agent) {
|
|
47
|
+
const session = agent.session;
|
|
48
|
+
if (session === undefined)
|
|
49
|
+
return '';
|
|
50
|
+
const id = session.id;
|
|
51
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
52
|
+
return '';
|
|
53
|
+
const base = `session/${id}`;
|
|
54
|
+
const lines = [
|
|
55
|
+
INDEX_HEADING,
|
|
56
|
+
'',
|
|
57
|
+
'The raw transcript of this span was archived to the context-mode knowledge',
|
|
58
|
+
'base before it was condensed, so detail the summary above omits is still',
|
|
59
|
+
'retrievable with `ctx_search`. Scope each query to one layer by `source`:',
|
|
60
|
+
'',
|
|
61
|
+
`- \`source: "${base}/constraint"\` — user messages: requirements, decisions, limits`,
|
|
62
|
+
`- \`source: "${base}/finding"\` — tool results and stated conclusions`,
|
|
63
|
+
`- \`source: "${base}/narrative"\` — assistant reasoning and plans`,
|
|
64
|
+
'',
|
|
65
|
+
'Search for a concrete token you expect in the original (a command, an error',
|
|
66
|
+
'string, a path, an identifier) rather than a paraphrase of the question.',
|
|
67
|
+
];
|
|
68
|
+
const block = lines.join('\n');
|
|
69
|
+
return block.length <= MAX_INDEX_CHARS ? block : `${block.slice(0, MAX_INDEX_CHARS - 1)}…`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Append an index block to the text of a summary.
|
|
73
|
+
*
|
|
74
|
+
* Only text blocks are touched. A summary may also carry non-text blocks, and
|
|
75
|
+
* rewriting or dropping those is the shipped engine's business, not this
|
|
76
|
+
* module's; they are copied through untouched.
|
|
77
|
+
*
|
|
78
|
+
* @param summary - the superseded summary blocks.
|
|
79
|
+
* @param index - the block to append.
|
|
80
|
+
* @returns new blocks with the index appended to the trailing text.
|
|
81
|
+
*/
|
|
82
|
+
export function appendToSummary(summary, index) {
|
|
83
|
+
const blocks = summary.map(block => ({ ...block }));
|
|
84
|
+
for (let position = blocks.length - 1; position >= 0; position -= 1) {
|
|
85
|
+
const block = blocks[position];
|
|
86
|
+
if (block.type !== 'text' || typeof block.text !== 'string')
|
|
87
|
+
continue;
|
|
88
|
+
blocks[position] = { ...block, text: `${block.text}\n\n${index}` };
|
|
89
|
+
return blocks;
|
|
90
|
+
}
|
|
91
|
+
// A summary with no text block at all: add one rather than dropping the index.
|
|
92
|
+
return [...blocks, { type: 'text', text: index }];
|
|
93
|
+
}
|
|
94
|
+
export default DshContextModeCompaction;
|
|
@@ -75,6 +75,29 @@ export declare function installPrecompactArchive(ctx: Context, getClient: () =>
|
|
|
75
75
|
* reached are searchable beside the evidence.
|
|
76
76
|
*/
|
|
77
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;
|
|
78
101
|
/**
|
|
79
102
|
* Whether assistant prose states a value worth retrieving on its own.
|
|
80
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;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,
|
|
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
|
@@ -136,6 +136,8 @@ export function classify(events) {
|
|
|
136
136
|
const text = textOf(event);
|
|
137
137
|
if (text.length === 0)
|
|
138
138
|
continue;
|
|
139
|
+
if (isInjectedContext(text))
|
|
140
|
+
continue;
|
|
139
141
|
const layer = layerOf(event.type, text);
|
|
140
142
|
if (layer === undefined)
|
|
141
143
|
continue;
|
|
@@ -143,6 +145,36 @@ export function classify(events) {
|
|
|
143
145
|
}
|
|
144
146
|
return lines;
|
|
145
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
|
+
')');
|
|
146
178
|
/** Return the layer for one event, or undefined when it carries no transcript value. */
|
|
147
179
|
function layerOf(type, text) {
|
|
148
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.3",
|
|
4
4
|
"description": "Expose context-mode MCP tools as native DeepSeek Harness tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -24,11 +24,16 @@
|
|
|
24
24
|
"types": "./lib/types/index.d.ts",
|
|
25
25
|
"import": "./lib/types/index.js"
|
|
26
26
|
},
|
|
27
|
+
"./compaction": {
|
|
28
|
+
"types": "./lib/types/compaction.d.ts",
|
|
29
|
+
"import": "./lib/types/compaction.js"
|
|
30
|
+
},
|
|
27
31
|
"./package.json": "./package.json",
|
|
28
32
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
29
33
|
},
|
|
30
34
|
"files": [
|
|
31
35
|
"lib",
|
|
36
|
+
"scripts/cleanup-injected.mjs",
|
|
32
37
|
"skills",
|
|
33
38
|
"vendor/context-mode/server.bundle.mjs",
|
|
34
39
|
"vendor/context-mode/LICENSE",
|
|
@@ -58,16 +63,24 @@
|
|
|
58
63
|
},
|
|
59
64
|
"peerDependencies": {
|
|
60
65
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
66
|
+
"@deepseek-ai/dsh-compaction-basic": "^0.1.5-rc.2",
|
|
61
67
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
62
68
|
"@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
|
|
63
69
|
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2"
|
|
64
70
|
},
|
|
71
|
+
"peerDependenciesMeta": {
|
|
72
|
+
"@deepseek-ai/dsh-compaction-basic": {
|
|
73
|
+
"optional": true
|
|
74
|
+
}
|
|
75
|
+
},
|
|
65
76
|
"devDependencies": {
|
|
66
77
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
78
|
+
"@deepseek-ai/dsh-agent": "0.1.5-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-compaction-basic": "0.1.5-rc.2",
|
|
67
80
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
|
|
68
82
|
"@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
|
|
69
83
|
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
70
|
-
"@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
|
|
71
84
|
"@types/node": "^22.0.0",
|
|
72
85
|
"typescript": "^6.0.3"
|
|
73
86
|
}
|
|
@@ -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()
|