@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.
@@ -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
+ }