@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,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live UI status messenger — the plugin-private bridge between the HOST half
|
|
3
|
+
* ("which phase am I in right now?") and the CLIENT half ("paint THAT pair on
|
|
4
|
+
* the `TurnStatus` DOM node").
|
|
5
|
+
*
|
|
6
|
+
* Why settings at all
|
|
7
|
+
* ------------------
|
|
8
|
+
* The client half (`web/client.js`) already mirrors the `falling-ts-force-compact`
|
|
9
|
+
* namespace through `settingsScope.bind` → `createSnapshotStore`, so ANY field
|
|
10
|
+
* written by the host here is reflected in the browser live (the `SettingsScope`
|
|
11
|
+
* revision-fencing contract). That is the ONLY sanctioned host→browser live-data
|
|
12
|
+
* channel this independent plugin bundle can use — there is no reverse RPC seam
|
|
13
|
+
* for a client-loaded plugin to expose arbitrary callable methods (the unary
|
|
14
|
+
* route map is closed and host-defined). The `liveUi` field documented below is
|
|
15
|
+
* therefore a PLUGIN-PRIVATE transient messenger: the host is its only writer;
|
|
16
|
+
* the client never writes it; it deliberately persists to `settings.yaml` like
|
|
17
|
+
* every other field of the namespace (harmless cosmetic residue — the worst
|
|
18
|
+
* outcome after a restart is the badge briefly showing a stale phase before the
|
|
19
|
+
* next LLM call overwrites it).
|
|
20
|
+
*
|
|
21
|
+
* Phases
|
|
22
|
+
* ------
|
|
23
|
+
* Every model request begins with a WORKING phase (a random text+color pair
|
|
24
|
+
* drawn from the 20×20 tables below — the "工作中的状态" set). Around a
|
|
25
|
+
* force-compaction the display is OVERRIDDEN deterministically, not randomly:
|
|
26
|
+
*
|
|
27
|
+
* • COMPRESSING — pinned red `[强制压缩中>>>]`, fired BEFORE the `compactNow` /
|
|
28
|
+
* `compactRegion` call;
|
|
29
|
+
* • DONE — pinned green `[压缩完成!]`, fired right AFTER the call
|
|
30
|
+
* commits; then after `DONE_FALLBACK_MS` (3 s) a forced
|
|
31
|
+
* `isImportant=true` push redraws a fresh random Deep working pair
|
|
32
|
+
* (single-purpose timer — the only one in this module).
|
|
33
|
+
*
|
|
34
|
+
* All writers are GUARANTEED never to throw (they wrap the settings-service
|
|
35
|
+
* write in try/catch): a messenger failure must NEVER disrupt the model
|
|
36
|
+
* request or the compaction transaction itself.
|
|
37
|
+
*
|
|
38
|
+
* @module @falling-ts/dsh-force-compact/ui-signal
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** The settings field name carrying the live UI status (host-written, client-read). */
|
|
42
|
+
export const LIVE_UI_FIELD = 'liveUi'
|
|
43
|
+
|
|
44
|
+
/** Phase discriminants. Closed union — consumers switch on exactly these. */
|
|
45
|
+
export const PHASE_WORKING = 'working'
|
|
46
|
+
export const PHASE_COMPRESSING = 'compressing'
|
|
47
|
+
export const PHASE_DONE = 'done'
|
|
48
|
+
|
|
49
|
+
/** Pinned (never randomized) payloads for the deterministic phases. */
|
|
50
|
+
export const PINNED_TEXTS = Object.freeze({
|
|
51
|
+
[PHASE_COMPRESSING]: '[强制压缩中>>>]',
|
|
52
|
+
[PHASE_DONE]: '[压缩完成!]',
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
/** Pinned colors matching {@link PINNED_TEXTS} — red while compacting, green on completion. */
|
|
56
|
+
export const PINNED_COLORS = Object.freeze({
|
|
57
|
+
[PHASE_COMPRESSING]: '#ff4d4f',
|
|
58
|
+
[PHASE_DONE]: '#52c41a',
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The 20 WORKING-phase texts. Deliberately irreverent, meme-flavored one-liners
|
|
63
|
+
* aimed at the agent ITSELF ("我正在憋大招..." / "我在偷渡..." / ...) — the
|
|
64
|
+
* badge talks about what the agent is supposedly up to in a playful voice
|
|
65
|
+
* instead of dry status verbs. Lengths intentionally exceed the old four-char
|
|
66
|
+
* constraint; the client paints the raw string with no width assumption.
|
|
67
|
+
* Colors are unchanged and remain randomly paired with these labels.
|
|
68
|
+
* @readonly
|
|
69
|
+
*/
|
|
70
|
+
export const WORKING_TEXTS = Object.freeze([
|
|
71
|
+
'正在酝酿骚操作...',
|
|
72
|
+
'正在憋大招...',
|
|
73
|
+
'灵感正在路上...',
|
|
74
|
+
'脑细胞开会中...',
|
|
75
|
+
'灵魂拷问进行中...',
|
|
76
|
+
'偷偷翻你底牌...',
|
|
77
|
+
'量子纠缠计算中...',
|
|
78
|
+
'假装很忙...',
|
|
79
|
+
'摸鱼式工作中...',
|
|
80
|
+
'疯狂敲键盘(精神上)...',
|
|
81
|
+
'正在缝合上下文...',
|
|
82
|
+
'正在驯服混沌...',
|
|
83
|
+
'正在召唤赛博大脑...',
|
|
84
|
+
'正在翻阅《天机》...',
|
|
85
|
+
'CPU 正在冒烟...',
|
|
86
|
+
'正在跟熵值搏斗...',
|
|
87
|
+
'正在画饼给你吃...',
|
|
88
|
+
'正在偷渡灵感...',
|
|
89
|
+
'正在暗中观察...',
|
|
90
|
+
'马上就好(大概)...',
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The 20 WORKING-phase colors — a fixed palette covering the hue wheel (soft
|
|
95
|
+
* blues/greens for calm phases, warm amber/violet toward the end); each pairs
|
|
96
|
+
* with any text independently (random pairing, not a locked text-color index,
|
|
97
|
+
* so repeated draws visibly vary BOTH dimensions).
|
|
98
|
+
* @readonly
|
|
99
|
+
*/
|
|
100
|
+
export const WORKING_COLORS = Object.freeze([
|
|
101
|
+
'#4f9cf9',
|
|
102
|
+
'#5b8def',
|
|
103
|
+
'#6a5bff',
|
|
104
|
+
'#8b5cf6',
|
|
105
|
+
'#a855f7',
|
|
106
|
+
'#c45bf9',
|
|
107
|
+
'#db6bd4',
|
|
108
|
+
'#e86bb0',
|
|
109
|
+
'#f06b8b',
|
|
110
|
+
'#f76b5b',
|
|
111
|
+
'#fb8c5b',
|
|
112
|
+
'#fca95b',
|
|
113
|
+
'#fdc35b',
|
|
114
|
+
'#d8e05b',
|
|
115
|
+
'#aede5b',
|
|
116
|
+
'#7ee083',
|
|
117
|
+
'#5be0a0',
|
|
118
|
+
'#5becd8',
|
|
119
|
+
'#5bcdf9',
|
|
120
|
+
'#7ba8f9',
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Draw a random working-phase status: a random text paired with a random
|
|
125
|
+
* color (independently chosen, so the pair space is 20×20 = 400 distinct
|
|
126
|
+
* combinations). Pure — no I/O, trivially testable.
|
|
127
|
+
* @returns {{phase: string, text: string, color: string}}
|
|
128
|
+
*/
|
|
129
|
+
export function randomWorkingPair() {
|
|
130
|
+
return {
|
|
131
|
+
phase: PHASE_WORKING,
|
|
132
|
+
text: WORKING_TEXTS[Math.floor(Math.random() * WORKING_TEXTS.length)],
|
|
133
|
+
color: WORKING_COLORS[Math.floor(Math.random() * WORKING_COLORS.length)],
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Build the pinned payload for a deterministic phase.
|
|
139
|
+
* @param {'compressing'|'done'} phase
|
|
140
|
+
* @returns {{phase: string, text: string, color: string}}
|
|
141
|
+
*/
|
|
142
|
+
export function pinnedPayload(phase) {
|
|
143
|
+
return { phase, text: PINNED_TEXTS[phase], color: PINNED_COLORS[phase] }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Publish one UI status onto the `liveUi` field of the `falling-ts-force-compact`
|
|
148
|
+
* namespace. This is THE host→browser delivery point: the client half's
|
|
149
|
+
* `settingsScope.bind` mirror flips its snapshot on the next accepted
|
|
150
|
+
* revision, and the browser component repaints the `TurnStatus` DOM node.
|
|
151
|
+
*
|
|
152
|
+
* Guarantees:
|
|
153
|
+
* • NEVER throws — a settings-service absence or a rejected write is caught
|
|
154
|
+
* and logged at most once per lifetime (observability only; the model
|
|
155
|
+
* request and any surrounding compaction transaction proceed untouched).
|
|
156
|
+
* • Fire-and-forget from the caller's perspective: the returned promise
|
|
157
|
+
* always settles (resolve on success, resolve-with-warning on failure).
|
|
158
|
+
*
|
|
159
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
160
|
+
* @param {{phase: string, text: string, color: string}} status
|
|
161
|
+
* @param {boolean} [isImportant=false] — `true` bypasses the guard entirely
|
|
162
|
+
* and writes unconditionally. `false` (the default) refuses to overwrite a
|
|
163
|
+
* currently displayed text that starts with `[` (i.e. a pinned bracket-form
|
|
164
|
+
* message such as `[强制压缩中>>>]`), returning early without touching
|
|
165
|
+
* settings.
|
|
166
|
+
* @returns {Promise<void>}
|
|
167
|
+
*/
|
|
168
|
+
let warnedOnce = false
|
|
169
|
+
export async function publishUiStatus(ctx, status, isImportant = false) {
|
|
170
|
+
try {
|
|
171
|
+
const settings = ctx.get('settings')
|
|
172
|
+
if (settings === undefined || typeof settings.update !== 'function') return
|
|
173
|
+
const NS = 'falling-ts-force-compact'
|
|
174
|
+
// Non-important pushes refuse to overwrite a pinned bracket-form text.
|
|
175
|
+
if (!isImportant) {
|
|
176
|
+
let currentText
|
|
177
|
+
try {
|
|
178
|
+
// SYNC read — the settings service's `get` returns the cached value
|
|
179
|
+
// immediately (same call style as settings.js:226); the gate is purely
|
|
180
|
+
// advisory (worst case: the write proceeds, nothing breaks), so there
|
|
181
|
+
// is no reason to await an async variant even if one ever appeared.
|
|
182
|
+
const nsValue = (typeof settings.get === 'function') ? settings.get(NS) : undefined
|
|
183
|
+
currentText = (nsValue != null && typeof nsValue === 'object')
|
|
184
|
+
? nsValue[LIVE_UI_FIELD]?.text
|
|
185
|
+
: (typeof nsValue === 'string' ? nsValue : undefined)
|
|
186
|
+
} catch { /* read failure must not block the important path — fall through */ }
|
|
187
|
+
if (typeof currentText === 'string' && currentText.startsWith('[')) return
|
|
188
|
+
}
|
|
189
|
+
await settings.update(NS, { [LIVE_UI_FIELD]: status })
|
|
190
|
+
if (!warnedOnce) {
|
|
191
|
+
warnedOnce = true
|
|
192
|
+
try {
|
|
193
|
+
ctx.logger.debug(`[force-compact] ui-signal: publishing ${status?.phase} "${status?.text}" (${status?.color}) via ${NS}.${LIVE_UI_FIELD}`)
|
|
194
|
+
} catch { /* logging must never propagate */ }
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
198
|
+
try {
|
|
199
|
+
ctx.logger.warn(`[force-compact] ui-signal publish failed (ignored, cosmetic only) — ${message}`)
|
|
200
|
+
} catch { /* never */ }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The host-side driver for one MODEL REQUEST's start moment. Call from the
|
|
206
|
+
* `llm/stream` waterfall (once per outgoing call): draw a fresh random working
|
|
207
|
+
* pair and publish it NON-importantly (so a pinned bracket-form text such as
|
|
208
|
+
* `[压缩完成!]` survives the push until its own fallback timer clears it — see
|
|
209
|
+
* {@link publishDone}). A fresh pair is also emitted on the 3-second fallback
|
|
210
|
+
* after DONE ({@link DONE_FALLBACK_MS}), firing with `isImportant=true` so it
|
|
211
|
+
* overwrites the DONE banner even though a `[`-prefixed text is on screen.
|
|
212
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
213
|
+
* @returns {Promise<void>}
|
|
214
|
+
*/
|
|
215
|
+
export async function publishRandomWorking(ctx) {
|
|
216
|
+
await publishUiStatus(ctx, randomWorkingPair())
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Publish the pinned RED "[强制压缩中>>>]" status. Call BEFORE a `compactNow` /
|
|
221
|
+
* `compactRegion` invocation. Passes `isImportant=true` so the pinned
|
|
222
|
+
* bracket-form message can overwrite whatever is currently displayed (including
|
|
223
|
+
* another pinned text).
|
|
224
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
225
|
+
* @returns {Promise<void>}
|
|
226
|
+
*/
|
|
227
|
+
export async function publishCompressing(ctx) {
|
|
228
|
+
await publishUiStatus(ctx, pinnedPayload(PHASE_COMPRESSING), true)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* How long after the pinned "[压缩完成!]" DONE banner is published before the
|
|
233
|
+
* forced fallback kicks in and paints a fresh random Deep working pair back on
|
|
234
|
+
* the badge (with `isImportant=true`). 3000 ms.
|
|
235
|
+
*
|
|
236
|
+
* Note: this is the plugin's SINGLE-USE TIMER — a deliberate, documented
|
|
237
|
+
* exception to the collection rule "plugins are pure host listeners that do
|
|
238
|
+
* not introduce timers". See the `dsh-force-compact` AGENTS.md deviation note.
|
|
239
|
+
*/
|
|
240
|
+
const DONE_FALLBACK_MS = 3000
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Publish the pinned GREEN "[压缩完成!]" status. Call AFTER a `compactNow` /
|
|
244
|
+
* `compactRegion` invocation commits. After {@link DONE_FALLBACK_MS} (3 s) a
|
|
245
|
+
* forced fallback (`isImportant=true`) overwrites the DONE banner with a fresh
|
|
246
|
+
* random Deep working pair, restoring the usual working appearance regardless
|
|
247
|
+
* of whether a subsequent `llm/stream` fires in the interim. We paint a
|
|
248
|
+
* FRESH random working pair (not the literal pre-compression text, which is no
|
|
249
|
+
* longer recoverable from the settings store — it was already overwritten by
|
|
250
|
+
* the COMPRESSING/DONE banners): the intent is "back to a normal working
|
|
251
|
+
* look", which a freshly-drawn working pair satisfies.
|
|
252
|
+
*
|
|
253
|
+
* Passes `isImportant=true` for the initial DONE push: reaching `publishDone`
|
|
254
|
+
* PRECEDED BY `publishCompressing`, which has already written the pinned red
|
|
255
|
+
* `[强制压缩中>>>]` bracket-form text. Without `isImportant=true` the gate inside
|
|
256
|
+
* {@link publishUiStatus} (which refuses non-important pushes over a currently
|
|
257
|
+
* displayed `[`-bracket text) would see our OWN still-displayed `compressing`
|
|
258
|
+
* banner and bail out, so the green `[压缩完成!]` would silently never be written
|
|
259
|
+
* and the 3 s fallback would jump straight from COMPRESSING to a fresh working
|
|
260
|
+
* pair — the DONE banner never appearing at all. Since a DONE push can only
|
|
261
|
+
* follow a `compressing` push from THIS plugin, there is no manually-set custom
|
|
262
|
+
* banner to protect, and overriding our own prior banner is exactly intended.
|
|
263
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
264
|
+
* @returns {Promise<void>}
|
|
265
|
+
*/
|
|
266
|
+
export async function publishDone(ctx) {
|
|
267
|
+
await publishUiStatus(ctx, pinnedPayload(PHASE_DONE), true)
|
|
268
|
+
setTimeout(() => {
|
|
269
|
+
void publishUiStatus(ctx, randomWorkingPair(), true)
|
|
270
|
+
}, DONE_FALLBACK_MS)
|
|
271
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locate the `compaction` service for a given agent.
|
|
3
|
+
*
|
|
4
|
+
* In modern harness compositions the compaction BACKEND (`compaction-basic`)
|
|
5
|
+
* is mounted per **agent preset realm** (the `standard` preset isolates the
|
|
6
|
+
* `compaction` service into each session's realm), so the HOST-GLOBAL
|
|
7
|
+
* `ctx.get('compaction')` observes `undefined` while each live agent's own
|
|
8
|
+
* context (`agent.ctx`) resolves the instance. A host-level function plugin
|
|
9
|
+
* (this one) holds a global context, so reading `ctx.get('compaction')` alone
|
|
10
|
+
* finds nothing — but it DOES have the live agent handle on every event
|
|
11
|
+
* payload, and `agent.ctx` is the canonical way to reach that agent's
|
|
12
|
+
* realm-scoped world.
|
|
13
|
+
*
|
|
14
|
+
* The location MODE comes from the `compactionMode` setting (`'realm'`
|
|
15
|
+
* default, `'global'` opt-out), read directly from the raw settings namespace
|
|
16
|
+
* so the hot model-request path never pays a full-settings-parse cost.
|
|
17
|
+
*
|
|
18
|
+
* Resolution tries, in priority order:
|
|
19
|
+
* 1. `agent.ctx` (the agent's realm-scoped context, guaranteed by the
|
|
20
|
+
* `Agent` type definition) — the modern preset-plane location;
|
|
21
|
+
* 2. the host-global `ctx.get('compaction')` — the base-bundle location
|
|
22
|
+
* plus a safety net when the agent context did not hold the service;
|
|
23
|
+
* 3. `ctx.compaction` (injected-property convention, topology-sensitive).
|
|
24
|
+
*
|
|
25
|
+
* `'realm'` (default) tries all three so every layout works; `'global'`
|
|
26
|
+
* restricts to steps 2–3 (a deployment known to mount the backend globally).
|
|
27
|
+
*
|
|
28
|
+
* @module @falling-ts/dsh-force-compact/service-resolver
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { readRawSetting, COMPACT_MODE_GLOBAL } from '../core/settings.js'
|
|
32
|
+
|
|
33
|
+
import { compactNowBuiltin, compactRegionBuiltin } from './builtin.js'
|
|
34
|
+
import { guardFn } from '../core/crashnet.js'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Find a usable compaction backend for one agent.
|
|
38
|
+
*
|
|
39
|
+
* Resolution order (priority 1 first):
|
|
40
|
+
* 1. the OFFICIAL `compaction` service (`compactNow`/`compactRegion`) — the
|
|
41
|
+
* authoritative summarizer; preferred whenever it is reachable from this
|
|
42
|
+
* agent's context (see the historical note above about realm placement);
|
|
43
|
+
* 2. this plugin's BUILTIN engine (its own transaction, event names, and
|
|
44
|
+
* summarizer) — the fallback used whenever the official service is
|
|
45
|
+
* unreachable from this context (the common `standard` preset layout) and
|
|
46
|
+
* the `builtinEnabled` setting allows it (default `true`).
|
|
47
|
+
*
|
|
48
|
+
* Both backends expose the SAME shape — `compactNow` and `compactRegion` — so
|
|
49
|
+
* callers are agnostic to which produced the result.
|
|
50
|
+
*
|
|
51
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx a context to fall back to (the plugin-global context) when the agent-side candidates do not resolve.
|
|
52
|
+
* @param {import('@deepseek-ai/dsh-agent').Agent|undefined} agent the agent owning the target session.
|
|
53
|
+
* @param {string|undefined} mode the `compactionMode` setting value (`'realm'`|`'global'`).
|
|
54
|
+
* @returns {Promise<{ compactNow: Function, compactRegion: Function, kind: 'official'|'builtin' }|undefined>} a normalized backend, or `undefined` when neither the official service nor the builtin engine is usable.
|
|
55
|
+
*/
|
|
56
|
+
// Internal body of `resolveCompaction` — routed through the crash-net wrapper
|
|
57
|
+
// so an unusual throw (a throwing `ctx.get` proxy, a rejecting `readRawSetting`)
|
|
58
|
+
// becomes a visible diagnostic rather than a silent propagation.
|
|
59
|
+
async function __resolveCompactionBody(ctx, agent, mode) {
|
|
60
|
+
const official = await findOfficialService(ctx, agent, mode)
|
|
61
|
+
if (official !== undefined) {
|
|
62
|
+
return { compactNow: official.compactNow, compactRegion: official.compactRegion, kind: 'official' }
|
|
63
|
+
}
|
|
64
|
+
return await builtinBackend(ctx, agent)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Public entry — wrapped by the universal crash net. */
|
|
68
|
+
export const resolveCompaction = guardFn('backend.resolveCompaction', __resolveCompactionBody)
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Locate the OFFICIAL `compaction` service for this agent via the historical
|
|
72
|
+
* two-tier resolver. Reads the `compactionMode` setting (`'realm'` default,
|
|
73
|
+
* `'global'` opt-out) to choose candidates:
|
|
74
|
+
* - `'realm'` (default): the agent's OWN realm-scoped context (`agent.ctx`,
|
|
75
|
+
* the canonical modern location), then the host-global `ctx.get('compaction')`;
|
|
76
|
+
* - `'global'`: only the host-global `ctx.get('compaction')`.
|
|
77
|
+
*
|
|
78
|
+
* NEVER declares `inject:['compaction']` and NEVER reads `ctx.compaction` as a
|
|
79
|
+
* property (that would trip the strict-injection fatal). Only `ctx.get` /
|
|
80
|
+
* `agent.ctx.get` — both safe and tolerant.
|
|
81
|
+
*
|
|
82
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
83
|
+
* @param {import('@deepseek-ai/dsh-agent').Agent|undefined} agent
|
|
84
|
+
* @param {string|undefined} mode
|
|
85
|
+
* @returns {Promise<object|undefined>} the official compaction service, or `undefined`.
|
|
86
|
+
*/
|
|
87
|
+
async function findOfficialService(ctx, agent, mode) {
|
|
88
|
+
const effectiveMode = (typeof mode === 'string' && mode === COMPACT_MODE_GLOBAL)
|
|
89
|
+
? COMPACT_MODE_GLOBAL
|
|
90
|
+
: ((mode === undefined) ? (await readRawSetting(ctx, 'compactionMode')) : 'realm')
|
|
91
|
+
|
|
92
|
+
if (effectiveMode !== COMPACT_MODE_GLOBAL
|
|
93
|
+
&& agent && agent.ctx !== undefined && agent.ctx !== null) {
|
|
94
|
+
const byRealm = tryGet(agent.ctx)
|
|
95
|
+
if (byRealm !== undefined) return byRealm
|
|
96
|
+
}
|
|
97
|
+
return tryGet(ctx)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build the BUILTIN backend for this agent: the plugin's own engine, gated by
|
|
102
|
+
* the `builtinEnabled` setting (default `true`). Returns `undefined` when the
|
|
103
|
+
* engine is disabled or lacks what it needs (an `agent` handle with a session
|
|
104
|
+
* and an `llm` service for the summarizer).
|
|
105
|
+
*/
|
|
106
|
+
async function builtinBackend(ctx, agent) {
|
|
107
|
+
const enabled = (await readRawSetting(ctx, 'builtinEnabled')) ?? true
|
|
108
|
+
if (!enabled) return undefined
|
|
109
|
+
if (agent === undefined || agent === null || agent.session === undefined) return undefined
|
|
110
|
+
const llm = ctx.get('llm')
|
|
111
|
+
if (llm === undefined || typeof llm.stream !== 'function') return undefined
|
|
112
|
+
return {
|
|
113
|
+
kind: 'builtin',
|
|
114
|
+
// P1 — widen positionals so command-driven callers can thread
|
|
115
|
+
// `sourceCommandId` (3rd arg) and auto/idle callers can supply
|
|
116
|
+
// `opts` (4th arg) to the builtin engine. The official passthrough
|
|
117
|
+
// (priority-1 path) ignores extra positionals harmlessly.
|
|
118
|
+
compactNow: (...args) => compactNowBuiltin(ctx, ...args),
|
|
119
|
+
compactRegion: (...args) => compactRegionBuiltin(ctx, ...args),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Safely attempt `receiver.get('compaction')`, returning the service ONLY when
|
|
125
|
+
* it exposes the methods this plugin calls (`compactNow` / `compactRegion`).
|
|
126
|
+
* Never throws — an unusable receiver or a missing service yields `undefined`.
|
|
127
|
+
*
|
|
128
|
+
* @param {any} receiver any receiver (typically a Cordis context, possibly a
|
|
129
|
+
* Proxy; some partial receivers may lack `.get` altogether).
|
|
130
|
+
* @returns {object|undefined}
|
|
131
|
+
*/
|
|
132
|
+
function tryGet(receiver) {
|
|
133
|
+
if (receiver === undefined || receiver === null) return undefined
|
|
134
|
+
if (typeof receiver.get !== 'function') return undefined
|
|
135
|
+
try {
|
|
136
|
+
const svc = receiver.get('compaction')
|
|
137
|
+
if (svc === undefined || svc === null) return undefined
|
|
138
|
+
if (typeof svc.compactNow === 'function' || typeof svc.compactRegion === 'function') return svc
|
|
139
|
+
} catch {
|
|
140
|
+
// Any receiver shape (including non-proxy partials) is tolerated.
|
|
141
|
+
}
|
|
142
|
+
return undefined
|
|
143
|
+
}
|