dsh-context-guardian 0.1.0-alpha.2

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/index.js ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * dsh-context-guardian -- Context Guardian as a native DSH bundle plugin.
3
+ *
4
+ * PHASE 1 SCAFFOLD. This file wires the plugin into DSH (config schema,
5
+ * settings section, the vendored compiler, the optional Python bridge). It
6
+ * does NOT touch compaction yet: no listener, no provider, no tools. Those
7
+ * are Phase 3 / 3b and are marked below where they land. Until then
8
+ * @deepseek-ai/dsh-compaction-basic behaves exactly as the profile has it.
9
+ *
10
+ * Written against DSH 0.1.2-alpha.2 (source read 2026-09-19):
11
+ * - settings: packages/settings/settings -> ctx.settings.installSection(owner, ns, schema, entry, hooks)
12
+ * - compaction: packages/compaction/compaction-basic -- `summarize()` is the
13
+ * engine's sole subclass customization hook
14
+ * - log seam: there is no "PreCompact" registration in this DSH; the
15
+ * durable signal is a `session/event` whose type is
16
+ * `compaction/start` (what dsh-openwolf listens to today)
17
+ *
18
+ * vendor/compiler.js and vendor/region.js are unmodified from
19
+ * dsh-compaction-instant@0.1.4 (MIT, TsFreddie; VCC principle by lllyasviel) --
20
+ * see vendor/LICENSE.dsh-compaction-instant. region.js imports
21
+ * @deepseek-ai/dsh-compaction and @deepseek-ai/dsh-llm, so it is loaded only
22
+ * where Phase 3b needs it, never at plugin load.
23
+ *
24
+ * MIT licensed.
25
+ */
26
+ import { spawn } from 'node:child_process'
27
+ import { existsSync } from 'node:fs'
28
+ import { dirname, join } from 'node:path'
29
+ import { createInterface } from 'node:readline'
30
+ import { fileURLToPath } from 'node:url'
31
+ import Schema from '@deepseek-ai/schemastery'
32
+ import { COMPILER_REV, DEFAULT_ARG_TOOLS } from './vendor/compiler.js'
33
+
34
+ export const name = 'dsh-context-guardian'
35
+
36
+ /** Nothing is required at load in Phase 1; settings attaches when present. */
37
+ export const inject = []
38
+
39
+ /** Settings namespace (must match /^[a-z][a-z0-9-]*$/). */
40
+ export const SETTINGS_NAMESPACE = 'context-guardian'
41
+
42
+ /** Resolved from the module URL -- never from process.cwd(). */
43
+ const PACKAGE_ROOT = dirname(fileURLToPath(import.meta.url))
44
+ const BRIDGE_SCRIPT = join(PACKAGE_ROOT, 'modules', 'cg_bridge.py')
45
+
46
+ const ratio = () => Schema.number().min(0.05).max(0.99)
47
+
48
+ export const Config = Schema.object({
49
+ python: Schema.string().default('')
50
+ .description('Python for the optional bridge. Empty = $GUARDIAN_PYTHON, then a .venv beside this package, then python/python3 on PATH.'),
51
+ numCtx: Schema.natural().default(32768)
52
+ .description('Model context window the ratios are measured against (GUARDIAN_NUM_CTX overrides).'),
53
+ compactThreshold: ratio().default(0.85)
54
+ .description('Hard compaction trigger (GUARDIAN_COMPACT_THRESHOLD overrides).'),
55
+ idleCompactRatio: ratio().default(0.45)
56
+ .description('Proactive trigger when the agent goes idle (GUARDIAN_IDLE_COMPACT_RATIO overrides).'),
57
+ keepRecentMessages: Schema.natural().default(8)
58
+ .description('Most recent messages never compacted (GUARDIAN_KEEP_RECENT_MESSAGES overrides).'),
59
+ reserveOutput: Schema.natural().default(8192)
60
+ .description('Tokens held back for the reply (GUARDIAN_RESERVE_OUTPUT overrides).'),
61
+ spanDir: Schema.string().default('')
62
+ .description('Span archive. Empty = $GUARDIAN_SPAN_DIR, then the proxy\'s own default.'),
63
+ maxRecallTokens: Schema.natural().default(16000)
64
+ .description('Cap on one recall restore (GUARDIAN_MAX_RECALL_TOKENS overrides).'),
65
+ maxSearchHits: Schema.natural().default(50)
66
+ .description('Cap on one search (GUARDIAN_MAX_SEARCH_HITS overrides).'),
67
+ toolArgTools: Schema.array(Schema.string()).default([])
68
+ .description('Tools whose compiled line keeps its key argument. Empty = the compiler\'s DEFAULT_ARG_TOOLS.'),
69
+ hideTools: Schema.array(Schema.string()).default([])
70
+ .description('Tools that produce no compiled line at all.'),
71
+ })
72
+
73
+ /**
74
+ * Fold the environment over a resolved section. GUARDIAN_* are the names
75
+ * context_guardian.py and its .env already use.
76
+ * Precedence: env > settings user layer > patch-row config > schema default.
77
+ * A malformed or out-of-range env value is ignored, never half-applied.
78
+ */
79
+ export function resolveOptions(section, env = process.env) {
80
+ const int = (key, fallback) => {
81
+ const raw = Number(env[key])
82
+ return Number.isInteger(raw) && raw > 0 ? raw : fallback
83
+ }
84
+ const frac = (key, fallback) => {
85
+ const raw = Number(env[key])
86
+ return Number.isFinite(raw) && raw >= 0.05 && raw <= 0.99 ? raw : fallback
87
+ }
88
+ return {
89
+ python: env.GUARDIAN_PYTHON?.trim() || section.python || defaultPython(),
90
+ numCtx: int('GUARDIAN_NUM_CTX', section.numCtx),
91
+ compactThreshold: frac('GUARDIAN_COMPACT_THRESHOLD', section.compactThreshold),
92
+ idleCompactRatio: frac('GUARDIAN_IDLE_COMPACT_RATIO', section.idleCompactRatio),
93
+ keepRecentMessages: int('GUARDIAN_KEEP_RECENT_MESSAGES', section.keepRecentMessages),
94
+ reserveOutput: int('GUARDIAN_RESERVE_OUTPUT', section.reserveOutput),
95
+ spanDir: env.GUARDIAN_SPAN_DIR?.trim() || section.spanDir || '',
96
+ maxRecallTokens: int('GUARDIAN_MAX_RECALL_TOKENS', section.maxRecallTokens),
97
+ maxSearchHits: int('GUARDIAN_MAX_SEARCH_HITS', section.maxSearchHits),
98
+ toolArgTools: section.toolArgTools?.length > 0 ? [...section.toolArgTools] : [...DEFAULT_ARG_TOOLS],
99
+ hideTools: [...(section.hideTools ?? [])],
100
+ }
101
+ }
102
+
103
+ /** Cross-field rule the schema cannot express; refuses the WRITE that breaks it. */
104
+ export function validateSection(section) {
105
+ if (section.idleCompactRatio >= section.compactThreshold) {
106
+ throw new Error(`context-guardian: idleCompactRatio (${section.idleCompactRatio}) must be below compactThreshold (${section.compactThreshold})`)
107
+ }
108
+ }
109
+
110
+ function defaultPython() {
111
+ const venv = process.platform === 'win32'
112
+ ? join(PACKAGE_ROOT, '.venv', 'Scripts', 'python.exe')
113
+ : join(PACKAGE_ROOT, '.venv', 'bin', 'python')
114
+ if (existsSync(venv)) return venv
115
+ return process.platform === 'win32' ? 'python' : 'python3'
116
+ }
117
+
118
+ /** The optional Python child; see modules/cg_bridge.py for the op list. */
119
+ export class PythonBridge {
120
+ constructor(options, logger) {
121
+ this.options = options
122
+ this.logger = logger
123
+ this.child = null
124
+ this.nextId = 1
125
+ this.pending = new Map()
126
+ this.stopped = false
127
+ }
128
+
129
+ ensureStarted() {
130
+ if (this.child !== null) return
131
+ if (this.stopped) throw new Error('context-guardian bridge is stopped')
132
+ const env = { ...process.env, PYTHONUNBUFFERED: '1', PYTHONIOENCODING: 'utf-8' }
133
+ if (this.options.spanDir) env.GUARDIAN_SPAN_DIR ??= this.options.spanDir
134
+ const child = spawn(this.options.python, [BRIDGE_SCRIPT], {
135
+ cwd: PACKAGE_ROOT,
136
+ env,
137
+ stdio: ['pipe', 'pipe', 'pipe'],
138
+ windowsHide: true, // no console window, no focus steal
139
+ })
140
+ this.child = child
141
+ createInterface({ input: child.stdout }).on('line', line => this.onLine(line))
142
+ createInterface({ input: child.stderr }).on('line', line => this.logger.debug(`[bridge] ${line}`))
143
+ child.on('error', error => this.onExit(`spawn failed (${this.options.python}): ${error.message}`))
144
+ child.on('exit', (code, signal) => this.onExit(`exited code=${code} signal=${signal}`))
145
+ }
146
+
147
+ onLine(line) {
148
+ let frame
149
+ try {
150
+ frame = JSON.parse(line)
151
+ } catch {
152
+ this.logger.warn(`context-guardian bridge: non-JSON line on stdout dropped (${line.slice(0, 120)})`)
153
+ return
154
+ }
155
+ const waiter = this.pending.get(frame.id)
156
+ if (waiter === undefined) return
157
+ this.pending.delete(frame.id)
158
+ clearTimeout(waiter.timer)
159
+ if (frame.ok === true) waiter.resolve(frame.result)
160
+ else waiter.reject(new Error(String(frame.error ?? 'bridge error')))
161
+ }
162
+
163
+ onExit(reason) {
164
+ if (this.child === null) return
165
+ this.child = null
166
+ for (const waiter of this.pending.values()) {
167
+ clearTimeout(waiter.timer)
168
+ waiter.reject(new Error(`context-guardian bridge ${reason}`))
169
+ }
170
+ this.pending.clear()
171
+ if (!this.stopped) this.logger.error(`context-guardian bridge ${reason}`)
172
+ }
173
+
174
+ request(op, params = {}, timeoutMs = 30000) {
175
+ this.ensureStarted()
176
+ const id = this.nextId++
177
+ return new Promise((resolve, reject) => {
178
+ const timer = setTimeout(() => {
179
+ this.pending.delete(id)
180
+ reject(new Error(`context-guardian bridge: "${op}" timed out after ${timeoutMs}ms`))
181
+ }, timeoutMs)
182
+ this.pending.set(id, { resolve, reject, timer })
183
+ this.child.stdin.write(`${JSON.stringify({ id, op, ...params })}\n`)
184
+ })
185
+ }
186
+
187
+ stop() {
188
+ this.stopped = true
189
+ const child = this.child
190
+ if (child === null) return
191
+ try {
192
+ child.stdin.write(`${JSON.stringify({ id: 0, op: 'shutdown' })}\n`)
193
+ child.stdin.end()
194
+ } catch { /* already gone */ }
195
+ const killer = setTimeout(() => child.kill(), 3000)
196
+ killer.unref()
197
+ child.once('exit', () => clearTimeout(killer))
198
+ }
199
+ }
200
+
201
+ export function apply(ctx, config) {
202
+ validateSection(config) // a bad patch row fails the load, loudly
203
+
204
+ let source = () => config
205
+ let bridge = null
206
+
207
+ const stopBridge = () => {
208
+ if (bridge === null) return
209
+ bridge.stop()
210
+ bridge = null
211
+ }
212
+
213
+ /** Current options; every Phase 3 consumer reads through this, never a copy. */
214
+ const options = () => resolveOptions(source())
215
+
216
+ /** Spawned on first use only -- Phase 1 never calls it on its own. */
217
+ // eslint-disable-next-line no-unused-vars
218
+ const getBridge = () => {
219
+ bridge ??= new PythonBridge(options(), ctx.logger)
220
+ return bridge
221
+ }
222
+
223
+ ctx.inject(['settings'], (settingsCtx) => {
224
+ settingsCtx.settings.installSection(ctx, SETTINGS_NAMESPACE, Config, config, {
225
+ validate: validateSection,
226
+ setSource: (current) => {
227
+ source = current
228
+ },
229
+ // Thresholds are read through options() at decision time, so a change
230
+ // needs no work here; only the interpreter / span dir are spawn-time.
231
+ onChange: () => {
232
+ if (bridge === null) return
233
+ const next = options()
234
+ if (next.python !== bridge.options.python || next.spanDir !== bridge.options.spanDir) stopBridge()
235
+ },
236
+ })
237
+ })
238
+
239
+ ctx.effect(() => stopBridge)
240
+
241
+ const now = options()
242
+ ctx.logger.info(`context-guardian scaffold loaded (compiler ${COMPILER_REV}; window ${now.numCtx}, hard ${now.compactThreshold}, idle ${now.idleCompactRatio}) -- compaction untouched in Phase 1`)
243
+
244
+ // ── PHASE 3 lands here ────────────────────────────────────────────────────
245
+ // 3.1 snapshot on ctx.on('session/event', ...) where event type is
246
+ // 'compaction/start' (absorbing dsh-openwolf's compactionSurvival, 3d)
247
+ // 3.2 compaction: subclass @deepseek-ai/dsh-compaction-basic and override
248
+ // summarize() with vendor/compiler.js compileRegion() -- 3b / 3c
249
+ // 3b recall + search tools via ctx.tools.register(defineTool(...)), the
250
+ // /recall command, toolArgTools / hideTools, recall + search budgets
251
+ // 3c context_rewrite_cost, idle trigger, tier ladder, keyword index,
252
+ // context_compact
253
+ }
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ cg_bridge.py -- private bridge between the dsh-context-guardian DSH plugin
4
+ (Node) and the Python Context Guardian in this repo.
5
+
6
+ Inside DSH, compaction itself is done in-process by the plugin (the vendored
7
+ deterministic compiler) -- no Python on the hot path. This bridge exists so the
8
+ plugin and the HTTP proxy stay ONE product: it reports the proxy's resolved
9
+ GUARDIAN_* configuration and reads the span archive the proxy has already
10
+ written, so recall/search can reach history compacted before the plugin
11
+ existed. The proxy (`context-guardian` / `context_guardian.py`) is untouched
12
+ and keeps working for anyone who prefers that path.
13
+
14
+ PROTOCOL -- one JSON object per line, both directions, UTF-8 (same framing as
15
+ dsh-tool-guardian's tg_bridge.py):
16
+ request {"id": <int>, "op": "<name>", ...params}
17
+ response {"id": <int>, "ok": true, "result": {...}}
18
+ {"id": <int>, "ok": false, "error": "<Type>: <message>"}
19
+
20
+ hello -> versions + interpreter. Imports NOTHING heavy; liveness probe.
21
+ config -> the proxy's resolved settings (imports context_guardian,
22
+ which needs its requirements.txt installed; fails loud if not)
23
+ spans {"limit": <int>?} -> newest span files in the archive
24
+ shutdown -> exits 0
25
+
26
+ python cg_bridge.py serve on stdio
27
+ python cg_bridge.py --selftest offline checks, stdlib only
28
+
29
+ Python >= 3.10 -- same floor as context_guardian.py. MIT licensed.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import io
34
+ import json
35
+ import os
36
+ import platform
37
+ import sys
38
+ import tempfile
39
+ from pathlib import Path
40
+
41
+ BRIDGE_VERSION = "0.1.0"
42
+
43
+ # context_guardian.py lives one level above modules/, in the repo AND in the
44
+ # published npm package. Resolved from __file__, never from the working dir.
45
+ PACKAGE_ROOT = Path(__file__).resolve().parent.parent
46
+
47
+ CONFIG_FIELDS = ("GUARDIAN_PORT", "GUARDIAN_HOST", "UPSTREAM_URL", "NUM_CTX",
48
+ "COMPACT_THRESHOLD", "RESERVE_OUTPUT", "KEEP_RECENT_MESSAGES",
49
+ "CHARS_PER_TOKEN_ESTIMATE", "KEEP_SPANS", "KEEP_SUMMARIES")
50
+
51
+
52
+ def default_span_dir() -> Path:
53
+ """The proxy's own rule, restated without importing it: env, else
54
+ <package>/logs/guardian_spans."""
55
+ return Path(os.environ.get("GUARDIAN_SPAN_DIR",
56
+ str(PACKAGE_ROOT / "logs" / "guardian_spans")))
57
+
58
+
59
+ def _load_proxy():
60
+ if str(PACKAGE_ROOT) not in sys.path:
61
+ sys.path.insert(0, str(PACKAGE_ROOT))
62
+ import context_guardian # noqa: PLC0415 (heavy: fastapi/httpx; on demand only)
63
+ return context_guardian
64
+
65
+
66
+ class Bridge:
67
+ def __init__(self, load_proxy=_load_proxy, span_dir=None):
68
+ self._load_proxy = load_proxy # injectable: selftest passes a fake
69
+ self._span_dir = span_dir
70
+
71
+ def op_hello(self, _req: dict) -> dict:
72
+ return {"bridge": BRIDGE_VERSION,
73
+ "python": platform.python_version(),
74
+ "executable": sys.executable,
75
+ "package_root": str(PACKAGE_ROOT),
76
+ "pid": os.getpid()}
77
+
78
+ def op_config(self, _req: dict) -> dict:
79
+ cg = self._load_proxy()
80
+ out = {name: getattr(cg, name) for name in CONFIG_FIELDS if hasattr(cg, name)}
81
+ out["SPAN_DIR"] = str(getattr(cg, "SPAN_DIR", default_span_dir()))
82
+ out["version"] = getattr(cg, "__version__", "unknown")
83
+ return out
84
+
85
+ def op_spans(self, req: dict) -> dict:
86
+ root = Path(self._span_dir) if self._span_dir else default_span_dir()
87
+ limit = max(1, min(int(req.get("limit") or 50), 500))
88
+ if not root.is_dir():
89
+ # Absent is reported as absent, never as an empty archive.
90
+ return {"dir": str(root), "exists": False, "spans": []}
91
+ files = sorted((p for p in root.iterdir() if p.is_file()),
92
+ key=lambda p: p.stat().st_mtime, reverse=True)
93
+ return {"dir": str(root), "exists": True, "total": len(files),
94
+ "spans": [{"name": p.name, "bytes": p.stat().st_size,
95
+ "mtime": p.stat().st_mtime} for p in files[:limit]]}
96
+
97
+ def op_shutdown(self, _req: dict) -> dict:
98
+ return {"exit": True}
99
+
100
+ def dispatch(self, req: dict) -> dict:
101
+ rid = req.get("id")
102
+ op = str(req.get("op") or "")
103
+ fn = getattr(self, "op_" + op, None)
104
+ if fn is None:
105
+ return {"id": rid, "ok": False, "error": "UnknownOp: %r" % op}
106
+ try:
107
+ return {"id": rid, "ok": True, "result": fn(req)}
108
+ except Exception as exc: # noqa: BLE001
109
+ return {"id": rid, "ok": False,
110
+ "error": "%s: %s" % (type(exc).__name__, exc)}
111
+
112
+
113
+ def serve(stdin, proto, bridge=None) -> int:
114
+ bridge = bridge or Bridge()
115
+ for line in stdin:
116
+ line = line.strip()
117
+ if not line:
118
+ continue
119
+ try:
120
+ req = json.loads(line)
121
+ if not isinstance(req, dict):
122
+ raise ValueError("frame is not an object")
123
+ except ValueError as exc:
124
+ reply = {"id": None, "ok": False, "error": "BadFrame: %s" % exc}
125
+ else:
126
+ reply = bridge.dispatch(req)
127
+ proto.write(json.dumps(reply, ensure_ascii=False) + "\n")
128
+ proto.flush()
129
+ if reply.get("ok") and (reply.get("result") or {}).get("exit"):
130
+ return 0
131
+ return 0
132
+
133
+
134
+ # ------------------------------------------------------------- selftest -----
135
+
136
+ class _FakeProxy:
137
+ __version__ = "9.9.9"
138
+ NUM_CTX = 32768
139
+ COMPACT_THRESHOLD = 0.85
140
+ SPAN_DIR = Path("fake-spans")
141
+
142
+
143
+ def _check_hello_needs_no_proxy_import() -> bool:
144
+ def boom():
145
+ raise AssertionError("hello must not import the proxy")
146
+ r = Bridge(load_proxy=boom).dispatch({"id": 1, "op": "hello"})
147
+ return r.get("ok") is True and r["result"]["bridge"] == BRIDGE_VERSION
148
+
149
+
150
+ def _check_config_reports_proxy_constants() -> bool:
151
+ r = Bridge(load_proxy=lambda: _FakeProxy).dispatch({"id": 2, "op": "config"})
152
+ res = r.get("result") or {}
153
+ return (r.get("ok") is True and res.get("NUM_CTX") == 32768
154
+ and res.get("COMPACT_THRESHOLD") == 0.85
155
+ and res.get("version") == "9.9.9" and res.get("SPAN_DIR") == "fake-spans")
156
+
157
+
158
+ def _check_config_import_failure_is_loud() -> bool:
159
+ def missing():
160
+ raise ImportError("No module named 'fastapi'")
161
+ r = Bridge(load_proxy=missing).dispatch({"id": 3, "op": "config"})
162
+ return r.get("ok") is False and "fastapi" in str(r.get("error"))
163
+
164
+
165
+ def _check_missing_span_dir_is_absent_not_empty() -> bool:
166
+ with tempfile.TemporaryDirectory() as tmp:
167
+ r = Bridge(span_dir=str(Path(tmp) / "nope")).dispatch({"id": 4, "op": "spans"})
168
+ res = r.get("result") or {}
169
+ return r.get("ok") is True and res.get("exists") is False and res.get("spans") == []
170
+
171
+
172
+ def _check_spans_lists_newest_first_with_limit() -> bool:
173
+ with tempfile.TemporaryDirectory() as tmp:
174
+ for i, name in enumerate(("a.json", "b.json", "c.json")):
175
+ p = Path(tmp) / name
176
+ p.write_text("{}", encoding="utf-8")
177
+ os.utime(p, (1000 + i, 1000 + i))
178
+ r = Bridge(span_dir=tmp).dispatch({"id": 5, "op": "spans", "limit": 2})
179
+ res = r.get("result") or {}
180
+ return (res.get("total") == 3
181
+ and [s["name"] for s in res.get("spans", [])] == ["c.json", "b.json"])
182
+
183
+
184
+ def _check_default_span_dir_is_under_package_root() -> bool:
185
+ saved = os.environ.pop("GUARDIAN_SPAN_DIR", None)
186
+ try:
187
+ d = default_span_dir()
188
+ finally:
189
+ if saved is not None:
190
+ os.environ["GUARDIAN_SPAN_DIR"] = saved
191
+ return (d == PACKAGE_ROOT / "logs" / "guardian_spans"
192
+ and (PACKAGE_ROOT / "context_guardian.py").is_file()
193
+ and PACKAGE_ROOT.name != "modules")
194
+
195
+
196
+ def _check_serve_roundtrip_and_shutdown_exits() -> bool:
197
+ stdin = io.StringIO('{"id": 7, "op": "hello"}\nnot json\n{"id": 8, "op": "shutdown"}\n'
198
+ '{"id": 9, "op": "hello"}\n')
199
+ proto = io.StringIO()
200
+ code = serve(stdin, proto)
201
+ frames = [json.loads(x) for x in proto.getvalue().splitlines()]
202
+ return (code == 0 and len(frames) == 3
203
+ and frames[0]["id"] == 7 and frames[0]["ok"] is True
204
+ and "BadFrame" in frames[1]["error"]
205
+ and frames[2]["result"]["exit"] is True)
206
+
207
+
208
+ CHECKS = [
209
+ ("hello_needs_no_proxy_import", _check_hello_needs_no_proxy_import),
210
+ ("config_reports_proxy_constants", _check_config_reports_proxy_constants),
211
+ ("config_import_failure_is_loud", _check_config_import_failure_is_loud),
212
+ ("missing_span_dir_is_absent_not_empty", _check_missing_span_dir_is_absent_not_empty),
213
+ ("spans_lists_newest_first_with_limit", _check_spans_lists_newest_first_with_limit),
214
+ ("default_span_dir_is_under_package_root", _check_default_span_dir_is_under_package_root),
215
+ ("serve_roundtrip_and_shutdown_exits", _check_serve_roundtrip_and_shutdown_exits),
216
+ ]
217
+
218
+
219
+ def selftest() -> int:
220
+ passed = 0
221
+ for name, fn in CHECKS:
222
+ try:
223
+ ok = fn() is True
224
+ except Exception as exc: # noqa: BLE001
225
+ ok = False
226
+ print(" FAIL %s (%s: %s)" % (name, type(exc).__name__, exc))
227
+ else:
228
+ print(" %s %s" % ("ok " if ok else "FAIL", name))
229
+ if ok:
230
+ passed += 1
231
+ total = len(CHECKS)
232
+ print("cg_bridge selftest: %d checks, %d passed, %d failed"
233
+ % (total, passed, total - passed))
234
+ return 0 if total > 0 and passed == total else 1
235
+
236
+
237
+ def main(argv=None) -> int:
238
+ argv = list(sys.argv[1:] if argv is None else argv)
239
+ if "--selftest" in argv:
240
+ return selftest()
241
+ if "--version" in argv:
242
+ print(BRIDGE_VERSION)
243
+ return 0
244
+ proto = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", newline="\n")
245
+ stdin = io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace")
246
+ sys.stdout = sys.stderr # nothing but `proto` may reach the real stdout
247
+ return serve(stdin, proto)
248
+
249
+
250
+ if __name__ == "__main__":
251
+ raise SystemExit(main())
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "dsh-context-guardian",
3
+ "version": "0.1.0-alpha.2",
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
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./engine": "./engine.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "index.js",
14
+ "engine.js",
15
+ "cg_recall.js",
16
+ "cordis.patch.yml",
17
+ "modules/cg_bridge.py",
18
+ "vendor/compiler.js",
19
+ "vendor/region.js",
20
+ "vendor/LICENSE.dsh-compaction-instant",
21
+ "context_guardian.py",
22
+ "configure.py",
23
+ "requirements.txt",
24
+ "docs/dsh-integration.md",
25
+ "THIRD_PARTY_NOTICES.md",
26
+ "README.md",
27
+ "CHANGELOG.md",
28
+ "LICENSE"
29
+ ],
30
+ "dsh": {
31
+ "bundle": {
32
+ "patch": "./cordis.patch.yml"
33
+ }
34
+ },
35
+ "scripts": {
36
+ "check": "node --check index.js && node --check engine.js && node --check cg_recall.js && node --check vendor/compiler.js",
37
+ "selftest": "python modules/cg_bridge.py --selftest",
38
+ "test:node": "node tests/probes/probe_cg_recall.mjs && node cg_recall.js --selftest && node tests/engine_smoke.mjs"
39
+ },
40
+ "engines": {
41
+ "node": "^22.19.0 || >=24.0.0"
42
+ },
43
+ "dependencies": {
44
+ "@deepseek-ai/schemastery": "^3.18.2"
45
+ },
46
+ "peerDependencies": {
47
+ "@deepseek-ai/cordis": "^4.0.2",
48
+ "@deepseek-ai/dsh-compaction": ">=0.1.2-alpha.2",
49
+ "@deepseek-ai/dsh-llm": ">=0.1.2-alpha.2",
50
+ "@deepseek-ai/dsh-tools": ">=0.1.2-alpha.2"
51
+ },
52
+ "keywords": [
53
+ "dsh",
54
+ "dsh-plugin",
55
+ "dsh-bundle",
56
+ "deepseek-harness",
57
+ "compaction",
58
+ "context-window",
59
+ "context-management",
60
+ "recall",
61
+ "local-llm",
62
+ "ollama"
63
+ ],
64
+ "license": "MIT",
65
+ "author": "LuminariSoftwares",
66
+ "homepage": "https://github.com/LuminariSoftwares/context-guardian",
67
+ "repository": {
68
+ "type": "git",
69
+ "url": "git+https://github.com/LuminariSoftwares/context-guardian.git"
70
+ },
71
+ "bugs": {
72
+ "url": "https://github.com/LuminariSoftwares/context-guardian/issues"
73
+ }
74
+ }
@@ -0,0 +1,4 @@
1
+ fastapi>=0.110
2
+ uvicorn[standard]>=0.29
3
+ httpx>=0.27
4
+ python-dotenv>=1.0
@@ -0,0 +1,28 @@
1
+ compiler.js and region.js in this directory are vendored, unmodified, from
2
+ dsh-compaction-instant@0.1.4 (npm; author TsFreddie), md5:
3
+ e74bb51763795abd1a839e61de4875fa compiler.js
4
+ dc719f0745a532dd80ef1a3125f3bf6d region.js
5
+ The compiler ports the principle of https://github.com/lllyasviel/VCC
6
+ (skills/conversation-compiler/scripts/VCC.py). Their original headers are kept.
7
+
8
+ MIT License
9
+
10
+ Copyright (c) 2026 TsFreddie
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.