From 20b02b8686c4f706ce7203d769f04810505581bc Mon Sep 17 00:00:00 2001 From: Luke Maj Date: Wed, 14 Oct 2026 19:06:08 +0200 Subject: [PATCH 1/2] wip: persist the configured Great Library pointer in OpenCode --- .gitignore | 1 + README.md | 35 +++- contextmd/serve/__main__.py | 18 +- contextmd/serve/config.py | 19 +- contextmd/serve/opencode/contextmd.js | 208 +++++++++++++++++--- docs/map.md | 2 +- tests/great-library-path-corpus.json | 119 ++++++++++++ tests/opencode-sdk/package-lock.json | 93 +++++++++ tests/opencode-sdk/package.json | 7 + tests/opencode-sdk/pointer.test.mjs | 265 ++++++++++++++++++++++++++ tests/test_great_library.py | 101 ++++++++++ tests/test_opencode_pointer.py | 19 ++ 12 files changed, 853 insertions(+), 34 deletions(-) create mode 100644 tests/great-library-path-corpus.json create mode 100644 tests/opencode-sdk/package-lock.json create mode 100644 tests/opencode-sdk/package.json create mode 100644 tests/opencode-sdk/pointer.test.mjs create mode 100644 tests/test_great_library.py create mode 100644 tests/test_opencode_pointer.py diff --git a/.gitignore b/.gitignore index 7a60b85..703312a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__/ *.pyc +tests/opencode-sdk/node_modules/ diff --git a/README.md b/README.md index d24253b..6518fde 100644 --- a/README.md +++ b/README.md @@ -134,12 +134,20 @@ AgentsMD: follow the research procedure. | Moment | Served | | --- | --- | -| Session start | The session-start files, outside the turn budgets (Claude Code). After compaction or `/clear` a new context epoch starts and they are served again. | +| Session start | The session-start files, outside the turn budgets (Claude Code). After compaction or `/clear` a new context epoch starts; the configured Great Library pointer is served first within the run budget (Claude Code, Codex). | | User message | A new Chromeria run is a new turn: an 8,000-byte serve budget and a 2,000-byte deny budget. Cue scores the conversation and its fires are served at once (on Grok, at the next `PreToolUse`). | | Before a tool call | A matching must-know-before entry denies the call. A record-pointer rule keeps denying until a full read of its target in this session and context epoch; an entry self-pointer needs a full read after that version's first deny. An inline rule lifts once the model was shown it whole. After 3 denies without that, the next call passes, logged `not_read_capped`. One deny carries every whole rule that fits; the rest deny their next matching call. | | Tool result or failure | Other matching entries and the Cue fires, first fit within the serve budget and 2,000 bytes per output. On Grok, which discards this output, they are printed at the next `PreToolUse`. | | Before done | Logged only. | +When configured, the Great Library pointer reaches the first user message on +Claude Code, Codex and OpenCode, and the immediate continuation after compaction. +OpenCode saves it in native message history and keeps one owned copy in each +request, including native replay. Grok receives it in each new session's system +context and retains it through compaction. The pointer uses the run's 8,000-byte +serve budget; the log identifies it as `great_library`. Cursor and Antigravity +do not support this pointer yet. + Cue runs on the GPU serving host (see [Cue service](#cue-service)). A user message or a failed call is scored at once and waits at most 650 ms. Before a tool call Cue only starts scoring, and the tool result collects the score, so a @@ -196,6 +204,26 @@ Chromeria's `/userdata/server-runtime.json`. With the server down, the hook gives up at once; a slow or unreachable server is cut off after 2 seconds (4 for Grok), and the call runs unserved. +To make shared and personal knowledge discoverable, add a `great_library` +index-file path to that configuration, for example: + +```json +"great_library": "~/dev/promachos/library/README.md" +``` + +Before adding the key, verify that the **running** Chromeria build supports it, +update ContextMD to a supporting version, and reinstall its OpenCode plugin +with `bin/contextmd-serve install opencode --base ~/.t3`. Older Chromeria builds +reject unknown configuration keys and stop all serving. Then run +`bin/contextmd-serve check`: it must exit 0 and report an existing regular index +file. The path must be absolute or start with `~/`, contain no controls or line +separators, and fit 1,024 UTF-8 bytes after home expansion. + +Removing the key stops new pointer deliveries and Grok installations. Copies +already delivered retain their native lifetime: until compaction on Claude +Code and Codex, in OpenCode's stored history, and for the life of a Grok session. +An existing Grok session cannot acquire a changed index path; start a new session. + ### Off switch and log `bin/contextmd-serve off --base ~/.t3` turns serving off for that Chromeria: @@ -330,7 +358,10 @@ The card reads Observer's ledger read-only, like the report, and gets tool input and results from `agent-observer event show`, which is a read-only command. Cards hold transcript text, so keep them on the machine. -Run the tests with `python3 -m unittest discover -s tests -t .`. The contract +Install the pinned OpenCode test SDK with +`npm ci --ignore-scripts --prefix tests/opencode-sdk`, then run the tests with +`python3 -m unittest discover -s tests -t .`. The pointer tests exercise that +SDK's configured transport; the SDK is not a runtime dependency. The contract suite in `tests/test_contract.py` runs the real hook against a Chromeria dev server when `CONTEXTMD_CHROMERIA` names a Chromeria checkout. The standalone Library test compares the shared corpus copies without diff --git a/contextmd/serve/__main__.py b/contextmd/serve/__main__.py index 747acc6..5794aae 100644 --- a/contextmd/serve/__main__.py +++ b/contextmd/serve/__main__.py @@ -80,7 +80,11 @@ def main(argv=None) -> int: errors.append(f"{file}: {exc}") print(json.dumps({"entries": entries, "errors": errors}, indent=2)) return 1 if errors else 0 - cfg = config.load() + try: + cfg = config.load() + except (OSError, ValueError) as exc: + print(json.dumps({"errors": [str(exc)]}, indent=2)) + return 1 directory = store.library_dir(str(ns.project)) if ns.project else None sources = ([store.Source(directory, False)] if directory else []) + cfg.exact_keys + cfg.cue library = store.load(sources) @@ -97,6 +101,16 @@ def row(e): "entries": [{**row(e), "name": e.name, "applies_chars": len(e.applies)} for e in library.cue], "owners": library.owners, "errors": library.errors, "legacy": library.legacy}, } + index = Path(cfg.great_library) if cfg.great_library is not None else None + report["great_library"] = { + "configured": index is not None, + "path": str(index) if index is not None else None, + "exists": index.is_file() if index is not None else False, + "owner": store.head_commit(index.parent) if index is not None else None, + } + missing_index = index is not None and not report["great_library"]["exists"] + if missing_index: + report["great_library"]["error"] = "index is missing or not a regular file" if ns.project: project_entries = {e.path: e for e in [*library.entries, *library.cue] if directory and Path(e.path).parent == Path(directory)} @@ -104,7 +118,7 @@ def row(e): "owners": {directory: library.owners[directory]} if directory in library.owners else {}, "errors": library.errors, "legacy": library.legacy} print(json.dumps(report, indent=2)) - return 1 if library.errors else 0 + return 1 if library.errors or missing_index else 0 base = ns.base.expanduser().resolve() if ns.cmd == "health": return _health(ns, base) diff --git a/contextmd/serve/config.py b/contextmd/serve/config.py index 84bcb5c..13fceba 100644 --- a/contextmd/serve/config.py +++ b/contextmd/serve/config.py @@ -8,6 +8,7 @@ ``{"path": ..., "max_chars": ...}``; they sit outside the turn budgets - ``cue``: registered Library directories (default none); header fields choose each entry's matchers - ``cue_url``: the Cue service, such as ``http://100.116.0.2:8710``; without it Cue is off +- ``great_library``: the index file named by the first-input and compaction pointer Serving state (budgets, deny state, the log) lives in the Chromeria serving module. """ @@ -35,6 +36,20 @@ class Config: session_start: list[StartFile] = field(default_factory=list) cue: list[str] = field(default_factory=list) cue_url: str | None = None + great_library: str | None = None + + +def great_library_path(value: object) -> str: + """Validate the pointer's single-line path after expanding the server user's home.""" + if not isinstance(value, str) or not value or not value.startswith(("/", "~/")): + raise ValueError("great_library must be a nonempty absolute path or start with ~/") + expanded = os.path.expanduser(value) + if any(ord(c) < 32 or 127 <= ord(c) <= 159 or ord(c) in (0x2028, 0x2029) + or 0xD800 <= ord(c) <= 0xDFFF for c in expanded): + raise ValueError("great_library must not contain controls, line separators or lone surrogates") + if len(expanded.encode("utf-8")) > 1024: + raise ValueError("great_library expanded path exceeds 1024 UTF-8 bytes") + return expanded def path() -> Path: @@ -46,7 +61,7 @@ def load(file: Path | None = None) -> Config: if not file.exists(): return Config() raw = json.loads(file.read_text()) - unknown = set(raw) - {"exact_keys", "session_start", "cue", "cue_url"} + unknown = set(raw) - {"exact_keys", "session_start", "cue", "cue_url", "great_library"} if unknown: raise ValueError(f"{file}: unknown keys {sorted(unknown)}") cfg = Config() @@ -58,4 +73,6 @@ def load(file: Path | None = None) -> Config: cfg.cue = list(raw["cue"]) if "cue_url" in raw: cfg.cue_url = raw["cue_url"] + if "great_library" in raw: + cfg.great_library = great_library_path(raw["great_library"]) return cfg diff --git a/contextmd/serve/opencode/contextmd.js b/contextmd/serve/opencode/contextmd.js index b5ddc87..e1d12d4 100644 --- a/contextmd/serve/opencode/contextmd.js +++ b/contextmd/serve/opencode/contextmd.js @@ -15,12 +15,16 @@ * a reply when it first adds its part, just before the model reads it, and * releases a reply it drops before that, so the reservation does not wait out * OpenCode's settle window (chromeria#221). + * The Great Library pointer has a separate receipt and a saved, marked text + * part. Native replay preserves its marker; each request keeps one owned copy. + * Compaction processing is registered synchronously and awaited before the + * continuation can save or reuse that part. * OpenCode has no before-done hook. Every failure is silent: the call proceeds * unserved. */ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { randomBytes } from "node:crypto"; +import { createHash, randomBytes } from "node:crypto"; const BASE = "__BASE__"; const MAX_OUTPUT = 4096; @@ -39,10 +43,11 @@ function endpoint() { } } -async function moment(raw) { +async function moment(raw, { pointer: wantsPointer = false } = {}) { const ep = endpoint(); if (!ep) return null; const delivery = randomBytes(12).toString("hex"); + const pointerDelivery = wantsPointer ? randomBytes(12).toString("hex") : null; try { const res = await fetch(`${ep.url}/contextmd/v1/moment`, { method: "POST", @@ -51,6 +56,7 @@ async function moment(raw) { "content-type": "application/json", "x-contextmd-harness": "opencode", "x-contextmd-delivery": delivery, + ...(pointerDelivery ? { "x-contextmd-pointer-delivery": pointerDelivery } : {}), }, body: JSON.stringify(raw), signal: AbortSignal.timeout(2000), // the serving cut-off while Cue is new (chromeria#248) @@ -71,7 +77,11 @@ async function moment(raw) { .catch(() => "retry"); // A drop the server never hears about holds the reservation until the 120 s window. const release = () => post("release").then(() => undefined, () => undefined); - return { reply, settle, release }; + const held = pointerDelivery && typeof reply.pointer === "string" && reply.pointer + ? { ep, text: reply.pointer, delivery: pointerDelivery, state: "waiting", + applyingSent: false, attempts: 0, part: null, next: null } + : null; + return { reply, settle, release, pointer: held }; } catch { return null; } @@ -82,6 +92,78 @@ async function moment(raw) { // and is dropped. const served = new Map(); const turns = new Map(); +// Pointer receipts never enter `served`: a generic drop cannot refund a saved part. +const pointer = new Map(); +const pending = new Map(); + +function ownedParts(messages) { + return messages.flatMap((message) => message.info.role === "user" + ? message.parts.filter((part) => { + const mark = part.metadata && part.metadata.contextmd; + return part.type === "text" && part.synthetic === true && part.ignored !== true + && typeof part.text === "string" && part.text.length > 0 + && mark && mark.pointer === "great_library" + && typeof mark.delivery === "string" && /^[0-9a-f]{24}$/.test(mark.delivery) + && typeof mark.version === "string" && /^[0-9a-f]{12}$/.test(mark.version); + }) : []); +} + +function keepOneCopyPerText(messages) { + const owned = new Set(ownedParts(messages)); + const seen = new Set(); + // Native replay follows the retained tail. Keep its copy in this request only. + for (let i = messages.length - 1; i >= 0; i--) { + const kept = []; + for (let j = messages[i].parts.length - 1; j >= 0; j--) { + const part = messages[i].parts[j]; + if (owned.has(part)) { + if (seen.has(part.text)) continue; + seen.add(part.text); + } + kept.push(part); + } + messages[i].parts = kept.reverse(); + } +} + +const pointerCall = (held, action, reason) => + fetch(`${held.ep.url}/contextmd/v1/pointer/${action}/${held.delivery}` + + (reason ? `?reason=${encodeURIComponent(reason)}` : ""), { + method: "POST", + headers: { authorization: `Bearer ${held.ep.token}` }, + signal: AbortSignal.timeout(300), + }).then((res) => res.status, () => 0); + +async function finishHeld(sessionID, held, action, reason) { + const status = await pointerCall(held, action, reason); + if (status === 204 || status === 409) { + if (pointer.get(sessionID) === held) pointer.delete(sessionID); + } else held.next = { action, reason }; +} + +const settleHeld = (id, held) => finishHeld(id, held, "settle"); +const releaseHeld = (id, held, reason) => finishHeld(id, held, "release", reason); + +async function retireHeld(id, held) { + if (held.next) return finishHeld(id, held, held.next.action, held.next.reason); + if (held.attempts === 0) return releaseHeld(id, held, "epoch_closed"); + if (pointer.get(id) === held) pointer.delete(id); + // A save may still commit. Its old-epoch charge remains owned by that delivery. +} + +function markedPart(held, message) { + const headEnd = held.text.indexOf("\n\n"); + const line = headEnd < 0 ? held.text : held.text.slice(headEnd + 2); + const version = createHash("sha256").update(line, "utf8").digest("hex").slice(0, 12); + return { id: `prt_contextmdgl${held.delivery}`, sessionID: message.info.sessionID, + messageID: message.info.id, type: "text", synthetic: true, text: held.text, + metadata: { contextmd: { pointer: "great_library", delivery: held.delivery, version } } }; +} + +function pushIntoOwnMessage(messages, part) { + const own = messages.find((message) => message.info.id === part.messageID); + if (own && !own.parts.some((existing) => existing.id === part.id)) own.parts.push(part); +} function queue(sessionID, turn, text, settle, release) { if ((turns.get(sessionID) || 0) !== turn) { @@ -110,40 +192,101 @@ async function drop(sessionID) { export default async function contextmd(ctx) { const directory = (ctx && (ctx.directory || ctx.worktree)) || process.cwd(); + const client = ctx && ctx.client; + + async function save(part) { + try { + // OpenCode supplies its root SDK client, whose configured transport owns auth, + // directory and in-process fetch. That SDK has no public part-update method. + const result = await client._client.patch({ + url: "/session/{sessionID}/message/{messageID}/part/{partID}", + path: { sessionID: part.sessionID, messageID: part.messageID, partID: part.id }, + body: part, throwOnError: true, signal: AbortSignal.timeout(2000), + }); + return result && result.data && result.data.id === part.id ? "stored" : "unknown"; + } catch (error) { + const status = error && error.cause && error.cause.status; + return status >= 400 && status < 500 ? "rejected" : "unknown"; + } + } + + async function ensurePointer(sessionID, output) { + keepOneCopyPerText(output.messages); + await pending.get(sessionID); + const held = pointer.get(sessionID); + if (!held) return; + if (held.next) return finishHeld(sessionID, held, held.next.action, held.next.reason); + if (endpoint() === null) + return held.attempts === 0 ? releaseHeld(sessionID, held, "serving_off") : undefined; + const owned = ownedParts(output.messages); + if (owned.some((part) => part.metadata.contextmd.delivery === held.delivery) + || owned.some((part) => part.text === held.text)) return settleHeld(sessionID, held); + if (held.state === "waiting") { + if (typeof client?._client?.patch !== "function") + return releaseHeld(sessionID, held, "transport_unavailable"); + held.applyingSent = true; + const status = await pointerCall(held, "applying"); + if (status === 409) return void pointer.delete(sessionID); + if (status !== 204) return; // The acknowledgement may be lost after commit. + held.state = "applying"; + const last = [...output.messages].reverse().find((message) => message.info.role === "user"); + if (!last) return releaseHeld(sessionID, held, "no_user_message"); + held.part = markedPart(held, last); + } + held.attempts += 1; + const outcome = await save(held.part); + if (outcome === "rejected" && held.attempts === 1) + return releaseHeld(sessionID, held, "save_rejected"); + pushIntoOwnMessage(output.messages, held.part); + if (outcome === "stored") return settleHeld(sessionID, held); + // Abort, 5xx, a wrong response id or a retry's 4xx can follow a committed save. + // Keep the charge and retry the identical part until the request shows it. + } + + async function applyGenericItems(output) { + const last = [...output.messages].reverse().find((m) => m.info.role === "user"); + const list = last && served.get(last.info.sessionID); + if (!list || list.length === 0) return; + if (endpoint() === null) { + await drop(last.info.sessionID); + return; + } + for (const item of list) { + if (item.state === "refused") continue; + item.shown = true; + last.parts.push({ + id: `prt_contextmd${randomBytes(6).toString("hex")}`, + sessionID: last.info.sessionID, + messageID: last.info.id, + type: "text", + text: item.text, + synthetic: true, + }); + if (item.state === "pending") item.state = await item.settle(); + if (item.state === "retry") item.state = "pending"; + } + } + return { "chat.message": async (input) => { + await pending.get(input.sessionID); // A new user message: earlier served text belonged to the previous turn. const turn = (turns.get(input.sessionID) || 0) + 1; turns.set(input.sessionID, turn); await drop(input.sessionID); - const got = await moment({ hook: "chat.message", directory, input: { sessionID: input.sessionID } }); + const old = pointer.get(input.sessionID); + if (old && old.next) await finishHeld(input.sessionID, old, old.next.action, old.next.reason); + else if (old && old.attempts === 0) await releaseHeld(input.sessionID, old, "turn_ended"); + const got = await moment({ hook: "chat.message", directory, input: { sessionID: input.sessionID } }, + { pointer: true }); const text = got && got.reply && got.reply.context; if (text) queue(input.sessionID, turn, text, got.settle, got.release); + if (got && got.pointer) pointer.set(input.sessionID, got.pointer); }, "experimental.chat.messages.transform": async (_input, output) => { + await applyGenericItems(output); const last = [...output.messages].reverse().find((m) => m.info.role === "user"); - const list = last && served.get(last.info.sessionID); - if (!list || list.length === 0) return; - // Serving switched off: show and settle nothing more, and forget what was queued. - if (endpoint() === null) { - await drop(last.info.sessionID); - return; - } - for (const item of list) { - if (item.state === "refused") continue; - item.shown = true; - last.parts.push({ - id: `prt_contextmd${randomBytes(6).toString("hex")}`, - sessionID: last.info.sessionID, - messageID: last.info.id, - type: "text", - text: item.text, - synthetic: true, - }); - // Settle until the module confirms; a refused delivery is shown no more after this call. - if (item.state === "pending") item.state = await item.settle(); - if (item.state === "retry") item.state = "pending"; - } + if (last) await ensurePointer(last.info.sessionID, output); }, "tool.execute.before": async (input, output) => { const turn = turns.get(input.sessionID) || 0; @@ -179,9 +322,18 @@ export default async function contextmd(ctx) { if (text) queue(input.sessionID, turn, text, got.settle, got.release); else if (got && got.reply) await got.release(); }, - event: async ({ event }) => { + event: ({ event }) => { if (event && event.type === "session.compacted" && event.properties && event.properties.sessionID) { - await moment({ hook: "session.compacted", directory, input: { sessionID: event.properties.sessionID } }); + const id = event.properties.sessionID; + // Registration is synchronous; the continuation waits for the whole reply processing. + pending.set(id, (async () => { + const got = await moment({ hook: "session.compacted", directory, input: { sessionID: id } }, + { pointer: true }); + const old = pointer.get(id); + if (old) await retireHeld(id, old); + if (got && got.pointer) pointer.set(id, got.pointer); + })()); + return pending.get(id); } }, }; diff --git a/docs/map.md b/docs/map.md index 88b0e78..d461ab4 100644 --- a/docs/map.md +++ b/docs/map.md @@ -57,7 +57,7 @@ flowchart TD CU["5 Cue lane: scoring service and Chromeria lane"] %% CU: PR #36 #13 toolboxmd/chromeria PR #227 GL["Great Library pointer and registration on every machine"] - %% GL: #34 + %% GL: #34 toolboxmd/chromeria#311 SR["6 Serve evidence report"] %% SR: PR #38 #15 SC["6 Serve card and label prompt"] diff --git a/tests/great-library-path-corpus.json b/tests/great-library-path-corpus.json new file mode 100644 index 0000000..da21318 --- /dev/null +++ b/tests/great-library-path-corpus.json @@ -0,0 +1,119 @@ +[ + { + "name": "absolute", + "value": "/tmp/library/README.md", + "valid": true + }, + { + "name": "spaces-and-unicode", + "value": "/tmp/knowledge \ud83e\udde0 index.md", + "valid": true + }, + { + "name": "home-expansion", + "value": "~/library/README.md", + "expanded": "/Users/test/library/README.md", + "valid": true + }, + { + "name": "1024-utf8-bytes", + "value": "/\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9a", + "valid": true + }, + { + "name": "1025-utf8-bytes", + "value": "/\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9", + "valid": false + }, + { + "name": "expanded-home-over-limit", + "value": "~/README.md", + "expanded": "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/README.md", + "valid": false + }, + { + "name": "empty", + "value": "", + "valid": false + }, + { + "name": "relative", + "value": "library/README.md", + "valid": false + }, + { + "name": "other-user", + "value": "~someone/library/README.md", + "valid": false + }, + { + "name": "tilde-only", + "value": "~", + "valid": false + }, + { + "name": "null", + "value": null, + "valid": false + }, + { + "name": "number", + "value": 7, + "valid": false + }, + { + "name": "boolean", + "value": true, + "valid": false + }, + { + "name": "array", + "value": [], + "valid": false + }, + { + "name": "object", + "value": {}, + "valid": false + }, + { + "name": "line-feed", + "value": "/tmp/a\nb", + "valid": false + }, + { + "name": "nul", + "value": "/tmp/a\u0000b", + "valid": false + }, + { + "name": "delete", + "value": "/tmp/a\u007fb", + "valid": false + }, + { + "name": "c1-control", + "value": "/tmp/a\u0085b", + "valid": false + }, + { + "name": "line-separator", + "value": "/tmp/a\u2028b", + "valid": false + }, + { + "name": "paragraph-separator", + "value": "/tmp/a\u2029b", + "valid": false + }, + { + "name": "lone-high-surrogate", + "value": "/tmp/a\ud800b", + "valid": false + }, + { + "name": "lone-low-surrogate", + "value": "/tmp/a\udc00b", + "valid": false + } +] diff --git a/tests/opencode-sdk/package-lock.json b/tests/opencode-sdk/package-lock.json new file mode 100644 index 0000000..a0faa70 --- /dev/null +++ b/tests/opencode-sdk/package-lock.json @@ -0,0 +1,93 @@ +{ + "name": "opencode-sdk", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@opencode-ai/sdk": "1.19.0" + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.19.0.tgz", + "integrity": "sha512-lDFYTD6qxzq1mfwxF6Kq1DAs6rEwF4xoZ2I3G1sB7hAZ8Yb28JurtSmmcK7ZzZ8Z4qBUYbZimHJ6DM4JFq5amw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + } + } +} diff --git a/tests/opencode-sdk/package.json b/tests/opencode-sdk/package.json new file mode 100644 index 0000000..747d51a --- /dev/null +++ b/tests/opencode-sdk/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "type": "module", + "devDependencies": { + "@opencode-ai/sdk": "1.19.0" + } +} diff --git a/tests/opencode-sdk/pointer.test.mjs b/tests/opencode-sdk/pointer.test.mjs new file mode 100644 index 0000000..380e19a --- /dev/null +++ b/tests/opencode-sdk/pointer.test.mjs @@ -0,0 +1,265 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { test } from "node:test"; +import { createOpencodeClient } from "@opencode-ai/sdk/client"; + +const HEAD = "ContextMD, the user's own rule store, applies this rule from the user here:"; +const LINE = "The Great Library holds knowledge shared across projects and personal knowledge outside any project. Read /library/README.md in full."; +const POINTER = `${HEAD}\n\n${LINE}`; +const CUE = `${HEAD}\n\nKeep the project's command output concise.`; +const VERSION = createHash("sha256").update(LINE).digest("hex").slice(0, 12); +const source = readFileSync(process.env.CONTEXTMD_PLUGIN_SOURCE || + new URL("../../contextmd/serve/opencode/contextmd.js", import.meta.url), "utf8"); +const json = (data, status = 200) => new Response(JSON.stringify(data), { + status, headers: { "content-type": "application/json" }, +}); +const message = (id = "msg_first", parts = []) => ({ + info: { id, sessionID: "ses_native", role: "user" }, parts, +}); +const owned = (output) => output.messages.flatMap((m) => m.parts) + .filter((p) => p.metadata?.contextmd?.pointer === "great_library"); + +async function fixture(t, options = {}) { + const base = mkdtempSync(join(tmpdir(), "contextmd-pointer-test-")); + t.after(() => rmSync(base, { recursive: true, force: true })); + const state = join(base, "userdata", "contextmd"); + mkdirSync(state, { recursive: true }); + writeFileSync(join(base, "userdata", "server-runtime.json"), JSON.stringify({ origin: "http://serving.invalid" })); + writeFileSync(join(state, "hook-credential"), JSON.stringify({ token: "test-hook-token" })); + const pluginFile = join(base, "plugin.mjs"); + writeFileSync(pluginFile, source.replace('"__BASE__"', JSON.stringify(base))); + const calls = []; + const patches = []; + const stored = new Map(); + let provide = true; + const oldFetch = globalThis.fetch; + t.after(() => { globalThis.fetch = oldFetch; }); + globalThis.fetch = async (url, init) => { + assert.equal(new URL(url).origin, "http://serving.invalid"); + assert.equal(init.headers.authorization, "Bearer test-hook-token"); + const path = new URL(url).pathname; + const raw = init.body && JSON.parse(init.body); + calls.push({ path, headers: init.headers, raw }); + if (path.endsWith("/moment")) { + if (raw.hook === "session.compacted") provide = true; + const reply = provide ? { context: CUE, pointer: POINTER } : {}; + provide = false; + return options.moment ? options.moment(raw, reply) : json(reply); + } + if (path.includes("/pointer/applying/") && options.applying) return options.applying(); + return new Response(null, { status: 204 }); + }; + const client = createOpencodeClient({ + baseUrl: "http://native.invalid", directory: "/workspace/space and 知識", + headers: { authorization: "Basic test-opencode-auth" }, + fetch: async (request) => { + const raw = await request.text(); + const part = raw ? JSON.parse(raw) : null; + patches.push({ request, raw, part }); + if (options.save) return options.save({ request, part, stored, attempt: patches.length }); + if (!part) return json({ error: "missing JSON text part" }, 400); + stored.set(part.id, structuredClone(part)); + return json(part); + }, + }); + const factory = (await import(pathToFileURL(pluginFile).href)).default; + const plugin = await factory({ directory: "/workspace/space and 知識", client: options.noTransport ? {} : client }); + const transform = (output) => plugin["experimental.chat.messages.transform"]({}, output); + const start = () => plugin["chat.message"]({ sessionID: "ses_native" }); + const recreate = async () => (await import(`${pathToFileURL(pluginFile).href}?recreated`)).default({ + directory: "/workspace/space and 知識", client, + }); + return { plugin, transform, start, calls, patches, stored, state, client, recreate }; +} + +test("the real 1.19.0 root client saves the JSON part with its configured transport before pointer settlement", async (t) => { + const sdk = JSON.parse(readFileSync(new URL("node_modules/@opencode-ai/sdk/package.json", import.meta.url))); + assert.equal(sdk.version, "1.19.0"); + const f = await fixture(t); + assert.equal(f.client.part?.update, undefined); + await f.start(); + const output = { messages: [message("msg_first", [{ type: "text", text: "Hello" }])] }; + await f.transform(output); + assert.equal(f.patches.length, 1); + const patch = f.patches[0]; + assert.notEqual(patch.part, null, "raw root SDK PATCH must carry a JSON text-part body"); + assert.equal(patch.request.method, "PATCH"); + assert.equal(patch.request.url, `http://native.invalid/session/ses_native/message/msg_first/part/${patch.part.id}`); + assert.equal(patch.request.headers.get("authorization"), "Basic test-opencode-auth"); + assert.equal(patch.request.headers.get("x-opencode-directory"), encodeURIComponent("/workspace/space and 知識")); + assert.match(patch.request.headers.get("content-type"), /application\/json/); + assert.equal(patch.part.text, POINTER); + assert.equal(patch.part.synthetic, true); + assert.equal(patch.part.metadata.contextmd.version, VERSION); + assert.equal(f.stored.size, 1); + assert.equal(owned(output).length, 1); + const moment = f.calls.find((c) => c.path.endsWith("/moment")); + const p = moment.headers["x-contextmd-pointer-delivery"]; + const d = moment.headers["x-contextmd-delivery"]; + assert.notEqual(p, d); + assert.equal(patch.part.metadata.contextmd.delivery, p); + assert.deepEqual(f.calls.slice(1).map((c) => c.path), [ + `/contextmd/v1/settle/${d}`, `/contextmd/v1/pointer/applying/${p}`, `/contextmd/v1/pointer/settle/${p}`, + ]); + const later = { messages: [message("msg_first", [...f.stored.values()]), message("msg_later")] }; + await f.start(); + await f.transform(later); + assert.equal(f.patches.length, 1, "same epoch uses the stored pointer without another save"); +}); + +test("a first native 4xx releases only the pointer while ordinary context remains", async (t) => { + const f = await fixture(t, { save: () => json({ name: "InvalidPart", data: { message: "rejected" } }, 400) }); + await f.start(); + const output = { messages: [message()] }; + await f.transform(output); + assert.equal(owned(output).length, 0); + assert.equal(output.messages[0].parts[0].text, CUE); + const moment = f.calls[0]; + const p = moment.headers["x-contextmd-pointer-delivery"]; + const d = moment.headers["x-contextmd-delivery"]; + assert.ok(f.calls.some((c) => c.path === `/contextmd/v1/settle/${d}`)); + assert.ok(f.calls.some((c) => c.path === `/contextmd/v1/pointer/release/${p}`)); + assert.ok(!f.calls.some((c) => c.path === `/contextmd/v1/release/${d}`)); +}); + +test("missing native transport releases only the pointer and preserves the ordinary transform", async (t) => { + const f = await fixture(t, { noTransport: true }); + await f.start(); + const output = { messages: [message()] }; + await f.transform(output); + assert.equal(f.patches.length, 0); + assert.equal(output.messages[0].parts[0].text, CUE); + assert.ok(f.calls.some((c) => c.path.includes("/pointer/release/"))); + assert.ok(f.calls.some((c) => /\/v1\/settle\//.test(c.path))); +}); + +test("an ambiguous save stays charged across drop and retries the identical native id", async (t) => { + const f = await fixture(t, { save: ({ part, stored, attempt }) => { + assert.ok(part, "configured root transport sends the body"); + if (attempt === 1) { + stored.set(part.id, structuredClone(part)); // native commit, response lost + throw new TypeError("connection reset after commit"); + } + if (attempt === 2) return json({ name: "NotFound" }, 404); + stored.set(part.id, structuredClone(part)); + return json(part); + } }); + await f.start(); + for (let i = 0; i < 3; i++) { + const output = { messages: [message()] }; + if (i) await f.start(); // generic drop must not release a possibly saved pointer + await f.transform(output); + assert.equal(owned(output).length, 1); + if (i < 2) assert.ok(!f.calls.some((c) => c.path.includes("/pointer/settle/"))); + } + assert.equal(f.patches.length, 3); + assert.equal(new Set(f.patches.map((p) => p.part.id)).size, 1); + assert.equal(f.stored.size, 1); + assert.ok(!f.calls.some((c) => c.path.includes("/pointer/release/"))); + assert.ok(f.calls.some((c) => c.path.includes("/pointer/settle/"))); +}); + +test("a lost applying acknowledgement causes no native save or refund", async (t) => { + const f = await fixture(t, { applying: () => { throw new TypeError("acknowledgement lost"); } }); + await f.start(); + const output = { messages: [message()] }; + await f.transform(output); + assert.equal(f.patches.length, 0); + assert.equal(owned(output).length, 0); + assert.equal(output.messages[0].parts[0].text, CUE); + assert.ok(!f.calls.some((c) => c.path.includes("/pointer/release/"))); +}); + +test("native replay with new ids keeps one owned copy and preserves ordinary part order", async (t) => { + const f = await fixture(t); + await f.start(); + const output = { messages: [message()] }; + await f.transform(output); + assert.equal(f.stored.size, 1); + const part = [...f.stored.values()][0]; + const tail = structuredClone(part); + const replay = { ...structuredClone(part), id: "prt_replay", messageID: "msg_replay" }; + const quoted = { type: "text", text: POINTER, synthetic: true }; // no ownership metadata + const native = { messages: [ + message("msg_tail", [{ type: "text", text: "before" }, tail, { type: "text", text: "after" }]), + message("msg_replay", [quoted, replay, { type: "text", text: "user continuation" }]), + ] }; + await f.transform(native); + assert.deepEqual(native.messages[0].parts.map((p) => p.text), ["before", "after"]); + assert.equal(owned(native).length, 1); + assert.equal(owned(native)[0].id, "prt_replay"); + assert.equal(native.messages[1].parts[0], quoted); + assert.ok(f.stored.has(part.id), "request dedupe never edits stored history"); + assert.equal(f.patches.length, 1); +}); + +test("plugin recreation preserves native parts and does not save another copy", async (t) => { + const f = await fixture(t); + await f.start(); + await f.transform({ messages: [message()] }); + const plugin = await f.recreate(); + await plugin["chat.message"]({ sessionID: "ses_native" }); + const output = { messages: [message("msg_first", [...f.stored.values()]), message("msg_next")] }; + await plugin["experimental.chat.messages.transform"]({}, output); + assert.equal(owned(output).length, 1); + assert.equal(f.stored.size, 1); + assert.equal(f.patches.length, 1); +}); + +test("summary quotes and unmarked user text do not satisfy an owed pointer", async (t) => { + const f = await fixture(t); + await f.start(); + const output = { messages: [ + { info: { id: "msg_summary", sessionID: "ses_native", role: "assistant" }, + parts: [{ type: "text", synthetic: true, text: POINTER }] }, + message("msg_first", [{ type: "text", synthetic: true, text: POINTER }]), + ] }; + await f.transform(output); + assert.equal(f.stored.size, 1); + assert.equal(owned(output).length, 1); + assert.equal(output.messages[0].parts.length, 1); + assert.equal(output.messages[1].parts[0].text, POINTER); +}); + +test("the synchronous compaction barrier completes before either continuation path", async (t) => { + let resolve; + let arrived; + const observed = new Promise((r) => { arrived = r; }); + const f = await fixture(t, { moment: (raw, reply) => { + if (raw.hook !== "session.compacted") return json(reply); + arrived(); + return new Promise((r) => { resolve = () => r(json({ pointer: POINTER })); }); + } }); + await f.start(); + await f.transform({ messages: [message()] }); + f.plugin.event({ event: { type: "session.compacted", properties: { sessionID: "ses_native" } } }); + await observed; + const output = { messages: [message("msg_after_compaction")] }; + let continued = false; + const request = f.transform(output).then(() => { continued = true; }); + await Promise.resolve(); + assert.equal(continued, false); + assert.equal(f.patches.length, 1); + resolve(); + await request; + assert.equal(owned(output).length, 1); + assert.equal(f.patches.length, 2); + assert.equal(f.patches[1].part.messageID, "msg_after_compaction"); + await f.start(); + assert.equal(f.patches.length, 2); +}); + +test("turning serving off before native save releases only the unsaved pointer", async (t) => { + const f = await fixture(t); + await f.start(); + writeFileSync(join(f.state, "off"), ""); + const output = { messages: [message()] }; + await f.transform(output); + assert.equal(output.messages[0].parts.length, 0); + assert.equal(f.patches.length, 0); + assert.ok(f.calls.some((c) => c.path.includes("/pointer/release/"))); +}); diff --git a/tests/test_great_library.py b/tests/test_great_library.py new file mode 100644 index 0000000..5ed8adc --- /dev/null +++ b/tests/test_great_library.py @@ -0,0 +1,101 @@ +"""The configured index path is a single, bounded native-context instruction.""" + +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +from contextmd.serve import config + + +ROOT = Path(__file__).resolve().parents[1] +BIN = ROOT / "bin" / "contextmd-serve" + + +class GreatLibraryConfigTest(unittest.TestCase): + def test_path_corpus(self): + cases = json.loads((ROOT / "tests" / "great-library-path-corpus.json").read_text()) + for case in cases: + with self.subTest(case=case["name"]): + expand = lambda value: case.get("expanded", value) + with patch("contextmd.serve.config.os.path.expanduser", side_effect=expand): + if case["valid"]: + self.assertEqual(config.great_library_path(case["value"]), + case.get("expanded", case["value"])) + else: + with self.assertRaisesRegex(ValueError, "great_library"): + config.great_library_path(case["value"]) + + def test_absent_key_is_disabled_and_present_null_is_invalid(self): + with tempfile.TemporaryDirectory() as directory: + file = Path(directory) / "serving.json" + self.assertIsNone(config.load(file).great_library) + file.write_text('{}') + self.assertIsNone(config.load(file).great_library) + file.write_text('{"great_library": null}') + with self.assertRaisesRegex(ValueError, "great_library"): + config.load(file) + + def test_config_preserves_other_registered_libraries(self): + with tempfile.TemporaryDirectory() as directory: + file = Path(directory) / "serving.json" + file.write_text(json.dumps({"great_library": "/tmp/knowledge index.md", + "exact_keys": ["/tmp/exact"], "cue": ["/tmp/cue"]})) + loaded = config.load(file) + self.assertEqual(loaded.great_library, "/tmp/knowledge index.md") + self.assertEqual(loaded.exact_keys, ["/tmp/exact"]) + self.assertEqual(loaded.cue, ["/tmp/cue"]) + + +class GreatLibraryCheckTest(unittest.TestCase): + def run_check(self, file): + env = {**os.environ, "CONTEXTMD_SERVING_CONFIG": str(file), + "PYTHONDONTWRITEBYTECODE": "1"} + result = subprocess.run([sys.executable, str(BIN), "check"], env=env, + capture_output=True, text=True, timeout=15) + return result.returncode, json.loads(result.stdout) + + def test_regular_index_is_reported_and_missing_or_directory_fails(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + index = root / "README.md" + file = root / "serving.json" + file.write_text(json.dumps({"great_library": str(index), "exact_keys": [], "cue": []})) + code, report = self.run_check(file) + self.assertEqual(code, 1) + self.assertFalse(report["great_library"]["exists"]) + index.mkdir() + code, report = self.run_check(file) + self.assertEqual(code, 1) + self.assertFalse(report["great_library"]["exists"]) + index.rmdir() + index.write_text("# Great Library\n") + code, report = self.run_check(file) + self.assertEqual(code, 0) + self.assertEqual(report["great_library"], {"configured": True, "path": str(index), + "exists": True, "owner": None}) + + def test_invalid_key_exits_one_with_its_reason(self): + with tempfile.TemporaryDirectory() as directory: + file = Path(directory) / "serving.json" + file.write_text('{"great_library": "relative/README.md"}') + code, report = self.run_check(file) + self.assertEqual(code, 1) + self.assertIn("great_library", report["errors"][0]) + + def test_absent_pointer_does_not_require_an_index(self): + with tempfile.TemporaryDirectory() as directory: + file = Path(directory) / "serving.json" + file.write_text('{"exact_keys": [], "cue": []}') + code, report = self.run_check(file) + self.assertEqual(code, 0) + self.assertEqual(report["great_library"], {"configured": False, "path": None, + "exists": False, "owner": None}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_opencode_pointer.py b/tests/test_opencode_pointer.py new file mode 100644 index 0000000..94b513d --- /dev/null +++ b/tests/test_opencode_pointer.py @@ -0,0 +1,19 @@ +"""Run the plugin against OpenCode's pinned, configured root SDK transport.""" + +import subprocess +import unittest +from pathlib import Path + + +class OpenCodePointerTest(unittest.TestCase): + def test_persistence_receipts_and_native_replay(self): + root = Path(__file__).resolve().parents[1] + done = subprocess.run( + ["node", "--test", "tests/opencode-sdk/pointer.test.mjs"], + cwd=root, capture_output=True, text=True, timeout=30, + ) + self.assertEqual(done.returncode, 0, done.stdout + done.stderr) + + +if __name__ == "__main__": + unittest.main() From 1c47c08e6d6a27b899d68583ff8f8304a18da0f8 Mon Sep 17 00:00:00 2001 From: Luke Maj Date: Wed, 14 Oct 2026 19:12:31 +0200 Subject: [PATCH 2/2] feat(serve): add the configured Great Library pointer --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- docs/map.md | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cab4a95..1feede4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.1.0] - 2026-10-14 + +### Added + +- Add an optional Great Library pointer and persist it in OpenCode native history + ## [1.0.0] - 2026-10-14 ### Changed diff --git a/VERSION b/VERSION index 3eefcb9..9084fa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.1.0 diff --git a/docs/map.md b/docs/map.md index d461ab4..991830e 100644 --- a/docs/map.md +++ b/docs/map.md @@ -57,7 +57,7 @@ flowchart TD CU["5 Cue lane: scoring service and Chromeria lane"] %% CU: PR #36 #13 toolboxmd/chromeria PR #227 GL["Great Library pointer and registration on every machine"] - %% GL: #34 toolboxmd/chromeria#311 + %% GL: #34 #74 toolboxmd/chromeria#311 SR["6 Serve evidence report"] %% SR: PR #38 #15 SC["6 Serve card and label prompt"]