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 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
@@ -0,0 +1,5 @@
1
+ # This bundle's own default layer: mounts the plugin under id
2
+ # `tool-compact-now`. Nothing to configure — the tool takes no config.
3
+ - insert:
4
+ - id: tool-compact-now
5
+ name: 'smart-compaction'
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
+ }
@@ -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
+ }