@falling-ts/dsh-force-compact 0.2.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.cn.md +170 -0
- package/README.md +479 -0
- package/cordis.patch.yml +13 -0
- package/index.js +550 -0
- package/package.json +54 -0
- package/src/core/crashnet.js +215 -0
- package/src/core/log.js +346 -0
- package/src/core/pairing.js +188 -0
- package/src/core/policy.js +35 -0
- package/src/core/projected.js +139 -0
- package/src/core/settings.js +475 -0
- package/src/core/ui-signal.js +271 -0
- package/src/engine/backend.js +143 -0
- package/src/engine/builtin.js +1336 -0
- package/src/engine/checkpoint.js +206 -0
- package/src/engine/region.js +579 -0
- package/src/engine/summarizer.js +818 -0
- package/src/hooks/command.js +164 -0
- package/src/hooks/guard.js +706 -0
- package/src/hooks/idle.js +136 -0
- package/src/hooks/wire-rewrite.js +151 -0
- package/web/client.js +807 -0
- package/web/swish.css +188 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/force-compact` slash command.
|
|
3
|
+
*
|
|
4
|
+
* Selected from the `/` command list. Its handler runs **without sending the
|
|
5
|
+
* line to the model**, so it can act on an agent that is busy. When the agent is
|
|
6
|
+
* idle it compacts immediately through the compaction service's **idle manual
|
|
7
|
+
* entry** (`compactNow`); when the agent is busy `compactNow` is rejected
|
|
8
|
+
* (owner `null` requires an idle agent) and the handler queues a process-local
|
|
9
|
+
* force flag (`queueForceCompact`) that the `agent/pre-step` hook consumes at
|
|
10
|
+
* the next model step — which then force-compacts instead of requesting the
|
|
11
|
+
* model (the "insert a js memory record" behaviour).
|
|
12
|
+
*
|
|
13
|
+
* @module @falling-ts/dsh-force-compact/command
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { queueForceCompact } from './guard.js'
|
|
17
|
+
import { resolveCompaction } from '../engine/backend.js'
|
|
18
|
+
import { readRawSetting } from '../core/settings.js'
|
|
19
|
+
import { publishCompressing, publishDone } from '../core/ui-signal.js'
|
|
20
|
+
import { guardFn, renderCrash, captureThrowSite, appendCrashLine as appendDiag } from '../core/crashnet.js'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register the global `/force-compact` command. A no-op when the `commands`
|
|
24
|
+
* service is not mounted AT THIS MOMENT (typical during the window between
|
|
25
|
+
* plugin boot and the agent-presets plane activating — the caller retries
|
|
26
|
+
* on each guarded listener invocation, so a transient absence self-heals).
|
|
27
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
28
|
+
* @returns {boolean} whether the command was registered this call.
|
|
29
|
+
*/
|
|
30
|
+
// Internal body of `registerCommand` — routed through the crash-net wrapper.
|
|
31
|
+
function __registerCommandBody(ctx) {
|
|
32
|
+
const commands = ctx.get('commands')
|
|
33
|
+
if (commands === undefined || typeof commands.register !== 'function') {
|
|
34
|
+
// Silent on purpose: a transient miss during the boot→preset-plane window
|
|
35
|
+
// is expected; the deferred-registration loop in index.js retries until
|
|
36
|
+
// `commands` appears. If the service is PERMANENTLY absent (rare — e.g. a
|
|
37
|
+
// stripped-down composition) the operator sees the symptom (slash-command
|
|
38
|
+
// picker empty) and can diagnose directly rather than chasing hundreds of
|
|
39
|
+
// boot-miss log lines.
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
commands.register({
|
|
44
|
+
name: 'force-compact',
|
|
45
|
+
description: 'Force-compact the agent session context now (compacts immediately when idle).',
|
|
46
|
+
recordInput: false,
|
|
47
|
+
handler: async (invocation) => {
|
|
48
|
+
// SAFETY ENVELOPE: a slash-command handler that throws surfaces a raw
|
|
49
|
+
// error to the user. Contain the whole body so ANY anomaly (missing
|
|
50
|
+
// `invocation.agent`, a missing `session`, a rejecting settings read, a
|
|
51
|
+
// failing backend call) settles as a friendly `{kind:'error'}` result.
|
|
52
|
+
try {
|
|
53
|
+
return await __forceCompactCommandBody(ctx, invocation)
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
56
|
+
ctx.logger.warn(`[force-compact] /force-compact handler degraded — ${message}`)
|
|
57
|
+
// UNIVERSAL-CRASH-NET diagnostic alongside the ctx.logger line above —
|
|
58
|
+
// a durable, parseable trail independent of logger wiring.
|
|
59
|
+
try {
|
|
60
|
+
const lines = renderCrash('command.handler', error, captureThrowSite())
|
|
61
|
+
for (const line of lines) appendDiag(line)
|
|
62
|
+
} catch (_netFailure) { /* swallow */ }
|
|
63
|
+
return { kind: 'error', text: `compaction could not be started: ${message}` }
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
ctx.logger.debug('[force-compact] registered /force-compact command')
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Public entry — wrapped by the universal crash net. */
|
|
73
|
+
export const registerCommand = guardFn('command.registerCommand', __registerCommandBody)
|
|
74
|
+
|
|
75
|
+
/** Body of the `/force-compact` command handler; wrapped by its safe envelope. */
|
|
76
|
+
async function __forceCompactCommandBody(ctx, invocation) {
|
|
77
|
+
const agent = (invocation && typeof invocation === 'object') ? invocation.agent : undefined
|
|
78
|
+
const session = (agent && typeof agent === 'object') ? agent.session : undefined
|
|
79
|
+
if (agent === undefined || agent === null || session === undefined || session === null || typeof session.id !== 'string') {
|
|
80
|
+
return { kind: 'error', text: 'no usable agent session for this command' }
|
|
81
|
+
}
|
|
82
|
+
// Locate the compaction backend through `agent.ctx` (presets isolate it)
|
|
83
|
+
// with a host-global fallback (see `engine/backend.js`). The
|
|
84
|
+
// `compactionMode` setting is read once here (raw, cheap) and passed so
|
|
85
|
+
// the resolver need not re-read settings.
|
|
86
|
+
const mode = await readRawSetting(ctx, 'compactionMode')
|
|
87
|
+
const backend = await resolveCompaction(ctx, agent, mode)
|
|
88
|
+
ctx.logger.debug(`[force-compact] ${session.id}: /force-compact handler entered (backend ${backend ? backend.kind : 'UNAVAILABLE'})`)
|
|
89
|
+
|
|
90
|
+
// Guard the case it is not available so the command settles as an error
|
|
91
|
+
// rather than throwing out of the handler. When the OFFICIAL service is
|
|
92
|
+
// realm-isolated AND `builtinEnabled=false`, NOTHING backs this command.
|
|
93
|
+
if (backend === undefined || typeof backend.compactNow !== 'function') {
|
|
94
|
+
ctx.logger.warn(
|
|
95
|
+
`[force-compact] ${session.id}: NO compaction backend available (official service unreachable ` +
|
|
96
|
+
`AND builtin engine missing prerequisites). Enable \`builtinEnabled=true\` in the ` +
|
|
97
|
+
`\`falling-ts-force-compact\` namespace to restore the fallback.`
|
|
98
|
+
)
|
|
99
|
+
return { kind: 'error', text: 'no compaction backend available (enable builtinEnabled or make the official service reachable)' }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// compactNow is the idle manual-compaction entry (owner null). It requires
|
|
103
|
+
// an idle agent and uses the engine's own range selection. When the agent
|
|
104
|
+
// is busy it throws (ManualCompactionError) — in that case queue the force
|
|
105
|
+
// flag so the pre-step hook force-compacts at the next model step.
|
|
106
|
+
try {
|
|
107
|
+
// LIVE UI SIGNAL — PIN RED "compressing" BEFORE the model-request /
|
|
108
|
+
// commit happens. Both publishers swallow their own failures, so the
|
|
109
|
+
// messenger can never disturb the actual compaction outcome returned
|
|
110
|
+
// below.
|
|
111
|
+
await publishCompressing(ctx)
|
|
112
|
+
// P1 — thread `invocation.commandId` as the 3rd positional arg: the
|
|
113
|
+
// official `compactNow(agent, signal, commandId)` accepts it directly;
|
|
114
|
+
// the builtin `compactNow(agent, signal, sourceCommandId, opts)`
|
|
115
|
+
// absorbs it as `sourceCommandId` (positional widening verified).
|
|
116
|
+
const result = await backend.compactNow(agent, invocation.signal, invocation.commandId)
|
|
117
|
+
if (result === undefined || result === null) {
|
|
118
|
+
// Persist this diagnosis (WARN level so it survives the default INFO
|
|
119
|
+
// floor AND the `[force-compact]` marker routes it into the plugin's
|
|
120
|
+
// own durable log file — previously the reason lived ONLY in a
|
|
121
|
+
// `ctx.logger.info` line that the stock in-memory sink dropped,
|
|
122
|
+
// leaving the user with a bare "no compactable range"). Surface
|
|
123
|
+
// facets: node count (below the 6-node minimum?), head source
|
|
124
|
+
// (a previous checkpoint? re-running on an already-condensed head),
|
|
125
|
+
// and the char-estimated surface sum (vs the 8000 retention floor).
|
|
126
|
+
// The richer meter-priced detail (per-node prices, boundaryKind,
|
|
127
|
+
// crossing points) is additionally recorded by the builtin engine's
|
|
128
|
+
// own skip diagnostic (its `info`/`warn` helpers feed this same
|
|
129
|
+
// durable file).
|
|
130
|
+
const surfNodes = (session && session.surface && Array.isArray(session.surface.nodes)) ? session.surface.nodes : []
|
|
131
|
+
let headIsCheckpoint = false
|
|
132
|
+
if (surfNodes.length > 0 && Array.isArray(session.events)) {
|
|
133
|
+
const headEvent = session.events[surfNodes[0]]
|
|
134
|
+
const headSource = headEvent && headEvent.data && typeof headEvent.data === 'object' ? headEvent.data.source : undefined
|
|
135
|
+
headIsCheckpoint = !!(headSource && typeof headSource === 'object' && (headSource.plugin === 'force-compact-builtin' || headSource.plugin === 'compact'))
|
|
136
|
+
}
|
|
137
|
+
ctx.logger.warn(
|
|
138
|
+
`[force-compact] ${session.id}: no compactable range via ${backend?.kind} — `
|
|
139
|
+
+ `${surfNodes.length} surface nodes (min 6 required), head=${headIsCheckpoint ? 'previous checkpoint' : 'ordinary history'}`,
|
|
140
|
+
)
|
|
141
|
+
return { kind: 'success', text: `no compactable range (${surfNodes.length} surface nodes) — say something to build up more history, or raise the surface size` }
|
|
142
|
+
}
|
|
143
|
+
// COMMITTED (range shadowed + summary added) — pin GREEN "done"; the
|
|
144
|
+
// next model request's `llm/stream` watermark overwrites it with a
|
|
145
|
+
// fresh random working pair shortly after (natural cadence, no timer).
|
|
146
|
+
await publishDone(ctx)
|
|
147
|
+
ctx.logger.info(
|
|
148
|
+
`[force-compact] ${session.id}: /force-compact (${backend?.kind}) shadowed ${result.shadowedSeqs?.length ?? '?'} nodes `
|
|
149
|
+
+ `(~${result.shadowedTokenCount ?? '?'} tokens)`,
|
|
150
|
+
)
|
|
151
|
+
return { kind: 'success', text: `compacted ~${result.shadowedTokenCount ?? '?'} tokens via ${backend?.kind}` }
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// Busy (or otherwise unable) — queue the force flag for the next step.
|
|
154
|
+
// P1 — carry `invocation.commandId` so the pre-step consumer can echo
|
|
155
|
+
// it back into the `compaction/*` bracket's `sourceCommandId` field.
|
|
156
|
+
queueForceCompact(session.id, invocation.commandId)
|
|
157
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
158
|
+
ctx.logger.info(`[force-compact] ${session.id}: ${backend?.kind} said ${message}; queued for the next model step`)
|
|
159
|
+
return {
|
|
160
|
+
kind: 'success',
|
|
161
|
+
text: `agent is busy — will force-compact at the next model step (${message})`,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|