dsh-context-guardian 0.1.0-alpha.2 → 0.1.0-alpha.4
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/CHANGELOG.md +31 -0
- package/docs/dsh-integration.md +4 -2
- package/engine.js +102 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## dsh-context-guardian 0.1.0-alpha.4 - pinned goal survives compaction (2026-09-24)
|
|
4
|
+
|
|
5
|
+
Engine only (`cg-engine-3`, unchanged). Every checkpoint now carries the
|
|
6
|
+
session's original request byte-for-byte, so a local model cannot drift off
|
|
7
|
+
the task across one or several compactions.
|
|
8
|
+
|
|
9
|
+
**What.** `extractGoal(nodes)` recovers the pinned goal: a goal block already
|
|
10
|
+
embedded in an earlier checkpoint wins; otherwise the first real user-text
|
|
11
|
+
node (never a tool result, never harness-injected context) is the fallback.
|
|
12
|
+
A later `Goal: ...` user message is captured as a numbered UPDATE, deduped by
|
|
13
|
+
seq (first wins) and replayed in order -- it does not replace the original
|
|
14
|
+
request. `renderGoal(g)` writes the pinned block back (`<<<GOAL ... GOAL>>>`,
|
|
15
|
+
plus one `<<<GOAL update seq N ... GOAL>>>` block per update) at the TOP of
|
|
16
|
+
every deterministic checkpoint, ahead of `RECALL_GUIDE`, and is never counted
|
|
17
|
+
against or cut by any length cap. The same block is also prepended as a new
|
|
18
|
+
first summary entry when the stock LLM summary succeeds, so the pin survives
|
|
19
|
+
regardless of which summarization path a given compaction takes.
|
|
20
|
+
|
|
21
|
+
**Tests.** `tests/goal_pin_smoke.mjs` (new, 11 checks): byte-for-byte survival
|
|
22
|
+
of a 6000-character non-ASCII request across a second compaction, no
|
|
23
|
+
duplication, `Goal:` updates captured/deduped/ordered, tool results and
|
|
24
|
+
harness-injected messages never mistaken for the goal, no goal block at all
|
|
25
|
+
when there is no user text, and the checkpoint-goal-wins-over-later-message
|
|
26
|
+
rule. `tests/engine_smoke.mjs`'s `llm_success_passes_through_untouched` check
|
|
27
|
+
was loosened to assert the stock summary block is present rather than at a
|
|
28
|
+
fixed index, since a pinned-goal block may now be prepended as `summary[0]`.
|
|
29
|
+
|
|
30
|
+
## dsh-context-guardian 0.1.0-alpha.3 - install from npm (2026-09-20)
|
|
31
|
+
|
|
32
|
+
Docs only. `dsh plugin --profile <name> add dsh-context-guardian` now works by name; the DSH guide says where `engine.js` lands after an npm install and how to point the agent-preset row at it. No code change (engine `cg-engine-3`).
|
|
33
|
+
|
|
3
34
|
## dsh-context-guardian 0.1.0-alpha.2 - the DSH compaction engine (2026-09-20)
|
|
4
35
|
|
|
5
36
|
The proxy (`context_guardian.py`) is unchanged. This adds the DSH side.
|
package/docs/dsh-integration.md
CHANGED
|
@@ -18,7 +18,9 @@ DSH mounts compaction inside the AGENT PRESET (the `compaction` group), not in t
|
|
|
18
18
|
|
|
19
19
|
## Install the engine (5 minutes)
|
|
20
20
|
|
|
21
|
-
1. Get the code
|
|
21
|
+
1. Get the code — either way works, and you need the absolute path of `engine.js` for step 3:
|
|
22
|
+
- **From npm (recommended):** `dsh plugin --profile <your-profile> add dsh-context-guardian`. The engine then sits at `<DSH_HOME>/profiles/<your-profile>/node_modules/dsh-context-guardian/engine.js`.
|
|
23
|
+
- **From git:** `git clone https://github.com/LuminariSoftwares/context-guardian` anywhere on disk. No `pnpm install` is needed for the engine; it has no dependencies.
|
|
22
24
|
|
|
23
25
|
2. Make your own agent preset if you do not have one: copy the shipped `standard` preset folder to `<DSH_HOME>/.agent-presets/<your-id>/` (it contains `agent.cordis.yml` and `preset.yml`). `<DSH_HOME>` is `~/.dsh` unless you set the `DSH_HOME` environment variable.
|
|
24
26
|
|
|
@@ -33,7 +35,7 @@ DSH mounts compaction inside the AGENT PRESET (the `compaction` group), not in t
|
|
|
33
35
|
tools: [recall, search]
|
|
34
36
|
```
|
|
35
37
|
In the shipped preset the sibling rows (`- id: tool-result-pruner`) start at 4 spaces; match whatever yours use.
|
|
36
|
-
Note: `name` is a `file:///` URL to YOUR
|
|
38
|
+
Note: `name` is a `file:///` URL to the `engine.js` from step 1 — for an npm install that is `file:///C:/Users/<you>/.dsh/profiles/<your-profile>/node_modules/dsh-context-guardian/engine.js`. It is a URL to YOUR copy (forward slashes, also on Windows), or a path starting with `./` relative to the preset folder if you copy `engine.js`, `cg_recall.js` and `vendor/compiler.js` next to it. `numCtx` must be your model's real context window.
|
|
37
39
|
|
|
38
40
|
4. Start a NEW session with that preset selected. No restart is needed: DSH re-mounts a preset whose file changed when the next session starts.
|
|
39
41
|
|
package/engine.js
CHANGED
|
@@ -47,6 +47,13 @@ export const name = 'context-guardian-engine'
|
|
|
47
47
|
export const inject = ['compaction']
|
|
48
48
|
export const ENGINE_REV = 'cg-engine-3'
|
|
49
49
|
|
|
50
|
+
// Pinned goal markers. The session's original request is pinned into every
|
|
51
|
+
// checkpoint byte-for-byte so a local model never drifts from the task after
|
|
52
|
+
// one (or two) compactions.
|
|
53
|
+
export const GOAL_OPEN = '<<<GOAL'
|
|
54
|
+
export const GOAL_CLOSE = 'GOAL>>>'
|
|
55
|
+
export const GOAL_UPDATE_RE = /^\s*goal\s*:/i
|
|
56
|
+
|
|
50
57
|
const PACKAGE_ROOT = dirname(fileURLToPath(import.meta.url))
|
|
51
58
|
const MODES = ['llm-then-deterministic', 'deterministic', 'off']
|
|
52
59
|
const ALL_TOOLS = ['recall', 'search', 'context_rewrite_cost', 'context_compact']
|
|
@@ -250,6 +257,83 @@ export function splitInjected(nodes, dropSources) {
|
|
|
250
257
|
/** Identifier-like: a path, a dotted/underscored/hyphenated name, or anything with a digit -- not an English word. */
|
|
251
258
|
const IDENTIFIER_RE = /[_/\\.\-\d]/
|
|
252
259
|
|
|
260
|
+
// ── pinned goal ─────────────────────────────────────────────────────────────
|
|
261
|
+
// A `<<<GOAL ... >>>`-delimited block survives compaction inside the checkpoint
|
|
262
|
+
// text; extractGoal reads it back out (from earlier checkpoints and from the
|
|
263
|
+
// live user messages) and renderGoal writes it verbatim onto a fresh one.
|
|
264
|
+
|
|
265
|
+
const GOAL_BLOCK_RE = new RegExp(`${GOAL_OPEN}\\n([\\s\\S]*?)\\n${GOAL_CLOSE}`)
|
|
266
|
+
const GOAL_UPDATE_BLOCK_RE = new RegExp(`${GOAL_OPEN} update seq (\\d+)\\n([\\s\\S]*?)\\n${GOAL_CLOSE}`, 'g')
|
|
267
|
+
|
|
268
|
+
/** Every text block of a message joined with "\n". */
|
|
269
|
+
function messageText(message) {
|
|
270
|
+
const blocks = Array.isArray(message?.content) ? message.content : []
|
|
271
|
+
return blocks.filter(block => block?.type === 'text').map(block => String(block.text ?? '')).join('\n')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** A real user turn: role user, not a checkpoint, no injected source, first block text. */
|
|
275
|
+
function isUserTextNode(message) {
|
|
276
|
+
if (message?.role !== 'user') return false
|
|
277
|
+
if (isCheckpointSource(message.source)) return false
|
|
278
|
+
const source = message.source
|
|
279
|
+
if (source !== undefined && source !== null) return false
|
|
280
|
+
return message.content?.[0]?.type === 'text'
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The session's pinned goal and its `goal:` updates, recovered from `nodes`
|
|
285
|
+
* (walked in the given order). A goal carried by any checkpoint node wins; the
|
|
286
|
+
* first user-text node is the fallback goal. Updates are de-duplicated by seq
|
|
287
|
+
* (first wins) and returned sorted by seq ascending.
|
|
288
|
+
*/
|
|
289
|
+
export function extractGoal(nodes) {
|
|
290
|
+
let goal = null
|
|
291
|
+
let goalNode = null
|
|
292
|
+
let firstUserText = null
|
|
293
|
+
const collected = []
|
|
294
|
+
for (const node of nodes ?? []) {
|
|
295
|
+
const message = node?.message
|
|
296
|
+
if (message === undefined || message === null) continue
|
|
297
|
+
if (isCheckpointSource(message.source)) {
|
|
298
|
+
const text = messageText(message)
|
|
299
|
+
if (goal === null) {
|
|
300
|
+
const match = text.match(GOAL_BLOCK_RE)
|
|
301
|
+
if (match !== null) goal = match[1]
|
|
302
|
+
}
|
|
303
|
+
for (const match of text.matchAll(GOAL_UPDATE_BLOCK_RE)) {
|
|
304
|
+
collected.push({ seq: Number(match[1]), text: match[2], node: null })
|
|
305
|
+
}
|
|
306
|
+
} else if (isUserTextNode(message)) {
|
|
307
|
+
const text = messageText(message)
|
|
308
|
+
if (firstUserText === null) firstUserText = { node, text }
|
|
309
|
+
if (GOAL_UPDATE_RE.test(text)) collected.push({ seq: node.seq, text, node })
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (goal === null && firstUserText !== null) {
|
|
313
|
+
goal = firstUserText.text
|
|
314
|
+
goalNode = firstUserText.node
|
|
315
|
+
}
|
|
316
|
+
const bySeq = new Map()
|
|
317
|
+
for (const update of collected) {
|
|
318
|
+
// Skip the goal node's own text; checkpoint updates carry node=null, so
|
|
319
|
+
// only a real goalNode may suppress an update.
|
|
320
|
+
if ((goalNode !== null && update.node === goalNode) || bySeq.has(update.seq)) continue
|
|
321
|
+
bySeq.set(update.seq, { seq: update.seq, text: update.text })
|
|
322
|
+
}
|
|
323
|
+
return { goal, updates: [...bySeq.values()].sort((a, b) => a.seq - b.seq) }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Render a pinned-goal block for a checkpoint head. '' when there is no goal. */
|
|
327
|
+
export function renderGoal(g) {
|
|
328
|
+
if (g?.goal === null || g?.goal === undefined) return ''
|
|
329
|
+
let out = "[pinned goal -- the session's first request, verbatim; later 'goal:' updates follow. Keep working toward it.]"
|
|
330
|
+
out += `\n${GOAL_OPEN}\n${g.goal}\n${GOAL_CLOSE}`
|
|
331
|
+
for (const update of g.updates ?? []) {
|
|
332
|
+
out += `\n${GOAL_OPEN} update seq ${update.seq}\n${update.text}\n${GOAL_CLOSE}`
|
|
333
|
+
}
|
|
334
|
+
return out
|
|
335
|
+
}
|
|
336
|
+
|
|
253
337
|
/** The deterministic checkpoint body for one region. Pure. */
|
|
254
338
|
export function buildCheckpoint(allRegionNodes, options, pressure, allNodes) {
|
|
255
339
|
const { kept: nodes, dropped } = splitInjected(allRegionNodes, options.dropSources)
|
|
@@ -274,10 +358,16 @@ export function buildCheckpoint(allRegionNodes, options, pressure, allNodes) {
|
|
|
274
358
|
})
|
|
275
359
|
const real = allRegionNodes.filter(node => node.seq >= 0).map(node => node.seq)
|
|
276
360
|
const range = real.length === 0 ? 'unmapped' : `${Math.min(...real)}-${Math.max(...real)}`
|
|
361
|
+
// The goal is computed from the region PLUS the whole session (region first),
|
|
362
|
+
// so a goal carried by a checkpoint inside the region wins over a later user
|
|
363
|
+
// message. It is never counted against or cut by any cap.
|
|
364
|
+
const goalNodes = allNodes === undefined || allNodes === null ? allRegionNodes : [...allRegionNodes, ...allNodes]
|
|
365
|
+
const goalBlock = renderGoal(extractGoal(goalNodes))
|
|
277
366
|
const parts = [
|
|
278
367
|
`[context-guardian checkpoint · deterministic · ${allRegionNodes.length} nodes · seqs ${range} · ~${regionTokens} -> ~${compiled.stats.tokens} tokens · ${COMPILER_REV}]`,
|
|
279
|
-
RECALL_GUIDE,
|
|
280
368
|
]
|
|
369
|
+
if (goalBlock.length > 0) parts.push(goalBlock)
|
|
370
|
+
parts.push(RECALL_GUIDE)
|
|
281
371
|
if (dropped.length > 0) parts.push(`[${dropped.length} harness-injected context messages omitted (instructions, skill list, runtime notes -- the harness re-injects them): seqs ${dropped.map(node => node.seq).join(', ')}]`)
|
|
282
372
|
parts.push(...compiled.entries)
|
|
283
373
|
const files = filesWritten(nodes, options.filesListed)
|
|
@@ -396,6 +486,17 @@ export function apply(ctx, config) {
|
|
|
396
486
|
const result = await original.call(this, input, agent, signal)
|
|
397
487
|
if (hasText(result)) {
|
|
398
488
|
record({ event: 'llm', session: String(agent?.session?.id ?? ''), provider: result.provider, model: result.model })
|
|
489
|
+
// Pin the goal on top of the stock summary too, as a NEW first text block.
|
|
490
|
+
let goalBlock = ''
|
|
491
|
+
try {
|
|
492
|
+
const region = mapRegionSeqs(agent?.session, input.messages).nodes
|
|
493
|
+
const session = agent?.session
|
|
494
|
+
const goalNodes = session === undefined || session === null ? region : [...region, ...sessionNodes(session)]
|
|
495
|
+
goalBlock = renderGoal(extractGoal(goalNodes))
|
|
496
|
+
} catch { goalBlock = '' }
|
|
497
|
+
if (goalBlock.length > 0) {
|
|
498
|
+
return { ...result, summary: [{ type: 'text', text: goalBlock }, ...(Array.isArray(result.summary) ? result.summary : [])] }
|
|
499
|
+
}
|
|
399
500
|
return result
|
|
400
501
|
}
|
|
401
502
|
return deterministic(input, agent, 'llm summary was empty')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-context-guardian",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
4
4
|
"description": "Context Guardian as a native DeepSeek Harness (DSH) bundle: deterministic, seq-pointer-preserving compaction with model-facing recall and search. Companion to dsh-tool-guardian.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|