smart-compaction 0.1.0
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 +21 -0
- package/README.md +121 -0
- package/cordis.patch.yml +5 -0
- package/index.js +90 -0
- package/package.json +67 -0
- package/select-range.js +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JoblessJoe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# smart-compaction
|
|
2
|
+
|
|
3
|
+
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) plugin that gives the
|
|
4
|
+
model a `compact_now` tool, so it can trigger compaction itself at a point *it* knows is safe —
|
|
5
|
+
right after finishing a step, never mid-edit — instead of only ever being interrupted by dsh's
|
|
6
|
+
automatic token-threshold trigger.
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
dsh's built-in compaction is purely reactive: an automatic trigger fires between agent steps
|
|
11
|
+
whenever token pressure crosses a fixed threshold, with no idea whether that moment is a good one
|
|
12
|
+
to interrupt. It can land between two dependent steps — e.g. right after reading a file and right
|
|
13
|
+
before editing it based on what was just read — and the resulting summary loses exactly the
|
|
14
|
+
context the very next step needed.
|
|
15
|
+
|
|
16
|
+
This plugin doesn't change *how* compaction works, only *who decides when*. It hands the model a
|
|
17
|
+
tool it can call proactively, on its own judgment, instead of leaving that decision entirely to a
|
|
18
|
+
blind token counter.
|
|
19
|
+
|
|
20
|
+
## What it does
|
|
21
|
+
|
|
22
|
+
One tool: **`compact_now`**. No arguments.
|
|
23
|
+
|
|
24
|
+
- Selects the largest currently-compactable span of conversation history — the same
|
|
25
|
+
tool-call-pairing-safe boundary logic dsh's own compaction already guarantees, reimplemented
|
|
26
|
+
here against dsh's *public* APIs only (no dsh core changes, no dependency on
|
|
27
|
+
`compaction-basic`'s internals).
|
|
28
|
+
- Calls dsh's own `ctx.compaction.compactRegion()` to actually do the compaction — the same
|
|
29
|
+
summarizer, the same durable `compaction/start`/`compaction/end` log events, the same guarantees
|
|
30
|
+
as any other compaction in the session.
|
|
31
|
+
- No-ops harmlessly (`"Not enough compactable history yet."`) if there isn't enough history yet —
|
|
32
|
+
safe for the model to call speculatively.
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
Two entry points exist on dsh's compaction service: `compactNow()` (what the human `/compact`
|
|
37
|
+
command uses — requires an **idle** agent, throws `busy` otherwise) and `compactRegion()` (what
|
|
38
|
+
dsh's own *automatic* between-step trigger uses — works fine **mid-turn**). A tool's `execute()`
|
|
39
|
+
always runs mid-turn — that's what calling a tool means — so `compact_now` is built on
|
|
40
|
+
`compactRegion()`, not `compactNow()`.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
From your dsh **web profile** (`web` below is the profile name; use whichever profile backs your
|
|
45
|
+
session):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
dsh plugin --profile web add smart-compaction
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This installs the package and adds it to `dsh.profile.bundles` for you. (No local `dsh` binary?
|
|
52
|
+
Run the equivalent by hand from the profile directory, e.g. `~/.dsh/profiles/web/`: `pnpm add
|
|
53
|
+
smart-compaction`, then add `"smart-compaction"` to that `package.json`'s `dsh.profile.bundles`
|
|
54
|
+
array yourself.)
|
|
55
|
+
|
|
56
|
+
No build step, no config. Restart your dsh service after adding it — new bundles are only picked
|
|
57
|
+
up on boot.
|
|
58
|
+
|
|
59
|
+
**Requires a `compaction` service on your profile.** Most profile templates ship one, but not all
|
|
60
|
+
do (e.g. `@deepseek-ai/dsh-web-app`-based profiles don't by default). If yours doesn't, `compact_now`
|
|
61
|
+
still installs cleanly (it won't break your profile's boot) but returns an error every time it's
|
|
62
|
+
called: `"no compaction service is configured on this profile"`. Add a `compaction-basic` bundle to
|
|
63
|
+
get one.
|
|
64
|
+
|
|
65
|
+
### Tell the model when to use it
|
|
66
|
+
|
|
67
|
+
`compact_now` only *offers* the capability — nothing calls it unless instructed to. Add something
|
|
68
|
+
like this to your `AGENTS.md` (or whatever your profile injects as standing instructions):
|
|
69
|
+
|
|
70
|
+
> After marking a todo item `completed` (never while one is `in_progress`), if the conversation
|
|
71
|
+
> has gotten long, call `compact_now`. It's safe to call speculatively — it no-ops if there isn't
|
|
72
|
+
> enough history to compact yet.
|
|
73
|
+
|
|
74
|
+
Tying it to todo-completion matters: it's a real, already-tracked signal for "I just finished a
|
|
75
|
+
self-contained unit of work," instead of asking the model to estimate its own remaining work,
|
|
76
|
+
which it's generally bad at.
|
|
77
|
+
|
|
78
|
+
## Building from source
|
|
79
|
+
|
|
80
|
+
Requires Node 22+. Plain JS, no build step — `git clone`, `pnpm install`, done.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/JoblessJoe/smart-compaction.git
|
|
84
|
+
cd smart-compaction
|
|
85
|
+
pnpm install
|
|
86
|
+
node test.js
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`test.js` is a pure unit-test check of the range-selection logic (`select-range.js`) — no live
|
|
90
|
+
dsh/Ollama session required.
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
None. The tool takes no arguments and needs no setup beyond installing it.
|
|
95
|
+
|
|
96
|
+
## Status
|
|
97
|
+
|
|
98
|
+
Verified end-to-end against a real dsh session, including:
|
|
99
|
+
|
|
100
|
+
- Basic call: the tool loads, the model calls it, it selects a valid boundary-safe range, and it
|
|
101
|
+
drives `compactRegion()` mid-turn without ever hitting the `busy` failure this design exists to
|
|
102
|
+
avoid.
|
|
103
|
+
- A real tool-call/tool-result pair (`todo_write`, marked `in_progress` then `completed`) sitting
|
|
104
|
+
in the compacted range — stays correctly paired, nothing split.
|
|
105
|
+
- Three `compact_now` calls in a row in one session (one accidental, caught by dsh's own
|
|
106
|
+
duplicate-call guard mid-task) — each completed cleanly, no crash, no `busy`, no corrupted state
|
|
107
|
+
from operating on a surface that already contains an earlier compaction's checkpoint message.
|
|
108
|
+
|
|
109
|
+
**Troubleshooting:** if the tool returns an error like `summarization produced no text summary
|
|
110
|
+
content`, that's not this plugin — `compact_now` selected a valid range and handed it to dsh's own
|
|
111
|
+
summarizer, which returned nothing. Observed specifically when the compacted range contains only
|
|
112
|
+
injected boilerplate (e.g. standing instructions) with no real assistant-generated text yet —
|
|
113
|
+
succeeded reliably once genuine conversation content was in the range. If it happens on ranges with
|
|
114
|
+
real content too, check whether your model's "thinking" level is an actually-enforced token budget
|
|
115
|
+
or just an instruction (e.g. Ollama's `think: low` is unenforced — a model can still spend its
|
|
116
|
+
entire output budget on hidden reasoning and return no visible summary text). Either way, this is a
|
|
117
|
+
model/summarizer-side issue, not something `compact_now` itself controls.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh plugin: registers a `compact_now` tool on `ctx.tools` so the model can
|
|
3
|
+
* voluntarily trigger compaction at a point it judges safe, instead of only
|
|
4
|
+
* ever being interrupted by dsh's automatic token-threshold trigger.
|
|
5
|
+
*
|
|
6
|
+
* Calls `ctx.compaction.compactRegion()` directly — NOT `compactNow()` (what
|
|
7
|
+
* the human `/compact` command uses). `compactRegion` doesn't require an
|
|
8
|
+
* idle agent, which matters because a tool's `execute()` always runs during
|
|
9
|
+
* an open turn; `compactNow()` would throw `busy` on every real call here.
|
|
10
|
+
* See HANDOFF.md for the full research trail.
|
|
11
|
+
*
|
|
12
|
+
* @module dsh-compact-now
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { toolPairingBalancedBefore } from '@deepseek-ai/dsh-compaction'
|
|
16
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
17
|
+
import { selectCompactableRange } from './select-range.js'
|
|
18
|
+
|
|
19
|
+
export const name = 'tool-compact-now'
|
|
20
|
+
// `compaction` deliberately NOT in `inject`: that would make this plugin's
|
|
21
|
+
// boot depend on the service existing, and not every profile bundle ships a
|
|
22
|
+
// compaction backend (e.g. dsh-web-app doesn't) — a hard inject broke boot
|
|
23
|
+
// on any profile that lacks one. Looked up via `ctx.get()` at call time
|
|
24
|
+
// instead, same as core's own optional-sibling-service pattern
|
|
25
|
+
// (compaction-basic's own `toolResultPruner` lookup).
|
|
26
|
+
export const inject = ['tools']
|
|
27
|
+
|
|
28
|
+
const DESCRIPTION =
|
|
29
|
+
'Voluntarily compact older conversation history now, at a point you know is safe: '
|
|
30
|
+
+ 'right after finishing a concrete step (e.g. just marked a todo item completed), '
|
|
31
|
+
+ 'never mid-edit or with unfinished work pending. Use this instead of waiting to be '
|
|
32
|
+
+ 'interrupted by automatic compaction, if the conversation feels like it has gotten '
|
|
33
|
+
+ 'long. Harmless to call speculatively — it no-ops if there is not enough history to '
|
|
34
|
+
+ 'compact yet.'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
38
|
+
*/
|
|
39
|
+
export function apply(ctx) {
|
|
40
|
+
ctx.tools.register(defineTool({
|
|
41
|
+
name: 'compact_now',
|
|
42
|
+
description: DESCRIPTION,
|
|
43
|
+
parameters: {},
|
|
44
|
+
output: {
|
|
45
|
+
schema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
properties: {
|
|
49
|
+
compacted: { type: 'boolean', required: true },
|
|
50
|
+
shadowedCount: { type: 'integer' },
|
|
51
|
+
shadowedTokenCount: { type: 'integer' },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
render: (_args, value) => [{
|
|
55
|
+
type: 'text',
|
|
56
|
+
text: value.compacted
|
|
57
|
+
? `Compacted ${value.shadowedCount} history items (~${value.shadowedTokenCount} tokens).`
|
|
58
|
+
: 'Not enough compactable history yet.',
|
|
59
|
+
}],
|
|
60
|
+
},
|
|
61
|
+
async execute(_args, exec) {
|
|
62
|
+
const compaction = ctx.get('compaction')
|
|
63
|
+
if (!compaction) {
|
|
64
|
+
throw new Error('compact_now: no compaction service is configured on this profile')
|
|
65
|
+
}
|
|
66
|
+
if (!exec.agent) {
|
|
67
|
+
throw new Error('compact_now requires an owning agent session')
|
|
68
|
+
}
|
|
69
|
+
const session = exec.agent.session
|
|
70
|
+
const range = selectCompactableRange(session, toolPairingBalancedBefore)
|
|
71
|
+
if (range === null) {
|
|
72
|
+
return { compacted: false }
|
|
73
|
+
}
|
|
74
|
+
let result
|
|
75
|
+
try {
|
|
76
|
+
result = await compaction.compactRegion(range.start, range.end, exec.agent, exec.signal)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// The four errors compactRegion's own range validation can throw all
|
|
79
|
+
// indicate this file's range-selection logic disagrees with core's —
|
|
80
|
+
// a bug here, not an expected runtime outcome. Surface it plainly.
|
|
81
|
+
throw new Error(`compact_now: compaction failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
compacted: true,
|
|
85
|
+
shadowedCount: result.shadowedSeqs.length,
|
|
86
|
+
shadowedTokenCount: result.shadowedTokenCount,
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
}))
|
|
90
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smart-compaction",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "dsh plugin: a compact_now tool letting the model trigger compaction itself at a safe point, instead of only the automatic token-threshold trigger.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": "joblessjoe",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/JoblessJoe/smart-compaction.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dsh",
|
|
18
|
+
"deepseek-harness",
|
|
19
|
+
"compaction",
|
|
20
|
+
"context-management"
|
|
21
|
+
],
|
|
22
|
+
"dsh": {
|
|
23
|
+
"bundle": {
|
|
24
|
+
"patch": "./cordis.patch.yml"
|
|
25
|
+
},
|
|
26
|
+
"compatibility": {
|
|
27
|
+
"dsh": ">=0.1.5-rc.3"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"dshhub": {
|
|
31
|
+
"schemaVersion": 1,
|
|
32
|
+
"displayName": "Smart Compaction",
|
|
33
|
+
"summary": "Gives the model a compact_now tool so it can trigger compaction itself at a safe point, instead of only being interrupted by the automatic token-threshold trigger.",
|
|
34
|
+
"categories": [
|
|
35
|
+
"Sessions & Messages",
|
|
36
|
+
"Tools & Capabilities"
|
|
37
|
+
],
|
|
38
|
+
"surfaces": [
|
|
39
|
+
"cli",
|
|
40
|
+
"web"
|
|
41
|
+
],
|
|
42
|
+
"capabilities": {
|
|
43
|
+
"provides": [
|
|
44
|
+
"tool:compact_now"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"compatibility": {
|
|
48
|
+
"dsh": ">=0.1.5-rc.3",
|
|
49
|
+
"node": ">=22"
|
|
50
|
+
},
|
|
51
|
+
"permissions": {}
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"index.js",
|
|
55
|
+
"select-range.js",
|
|
56
|
+
"cordis.patch.yml"
|
|
57
|
+
],
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@deepseek-ai/cordis": "*",
|
|
60
|
+
"@deepseek-ai/dsh-compaction": "*",
|
|
61
|
+
"@deepseek-ai/dsh-tools": "*"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@deepseek-ai/dsh-compaction": "0.0.1-rc.5",
|
|
65
|
+
"@deepseek-ai/dsh-tools": "0.0.1-rc.1"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/select-range.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reimplementation of dsh core's `selectCompactableRange`
|
|
3
|
+
* (`@deepseek-ai/dsh-compaction-basic`'s internal `region.ts` — not part of
|
|
4
|
+
* that package's public exports) using only APIs public to any dsh plugin:
|
|
5
|
+
* `toolPairingBalancedBefore` from `@deepseek-ai/dsh-compaction`, and
|
|
6
|
+
* `session.surface`/`session.eventAt` from `@deepseek-ai/dsh-session`.
|
|
7
|
+
*
|
|
8
|
+
* Fixed at the equivalent of `retainTokens = 0` — the largest currently
|
|
9
|
+
* compactable span, same as what dsh core's own `compactNow()` (the manual
|
|
10
|
+
* `/compact` command's backend) passes internally. Note this still always
|
|
11
|
+
* keeps the final surface node out of the compactable range: core's own
|
|
12
|
+
* retain-budget loop runs at least once even at `retainTokens = 0`, so it
|
|
13
|
+
* never offers the single most recent node for compaction either. This
|
|
14
|
+
* mirrors that behavior on purpose, not an oversight.
|
|
15
|
+
*
|
|
16
|
+
* @see HANDOFF.md for the full research trail behind this file.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {import('@deepseek-ai/dsh-session').Session} session
|
|
21
|
+
* @returns {{ start: import('@deepseek-ai/dsh-session').SessionSeq, end: import('@deepseek-ai/dsh-session').SessionSeq } | null}
|
|
22
|
+
*/
|
|
23
|
+
export function selectCompactableRange(session, toolPairingBalancedBefore) {
|
|
24
|
+
const nodes = session.surface.nodes
|
|
25
|
+
if (nodes.length === 0) return null
|
|
26
|
+
|
|
27
|
+
const head = session.eventAt(nodes[0])
|
|
28
|
+
const firstIdx = head !== undefined && head.type === 'system/message' ? 1 : 0
|
|
29
|
+
|
|
30
|
+
// Always retain at least the final surface node verbatim.
|
|
31
|
+
let keepFromIdx = nodes.length - 1
|
|
32
|
+
if (keepFromIdx <= firstIdx) return null
|
|
33
|
+
|
|
34
|
+
while (keepFromIdx > firstIdx) {
|
|
35
|
+
if (toolPairingBalancedBefore(session, nodes[keepFromIdx])) break
|
|
36
|
+
keepFromIdx -= 1
|
|
37
|
+
}
|
|
38
|
+
if (keepFromIdx <= firstIdx) return null
|
|
39
|
+
|
|
40
|
+
return { start: nodes[firstIdx], end: nodes[keepFromIdx - 1] }
|
|
41
|
+
}
|