Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
__pycache__/
*.pyc
tests/opencode-sdk/node_modules/
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [1.2.0] - 2026-10-14

### Added

- Add an optional Great Library pointer and persist it in OpenCode native history

## [1.1.0] - 2026-10-14

### Added
Expand Down
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -196,6 +204,26 @@ Chromeria's `<base>/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:
Expand Down Expand Up @@ -334,7 +362,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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.0
1.2.0
18 changes: 16 additions & 2 deletions contextmd/serve/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -97,14 +101,24 @@ 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)}
report["project"] = {"entries": [row(e) for e in project_entries.values()],
"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)
Expand Down
19 changes: 18 additions & 1 deletion contextmd/serve/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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:
Expand All @@ -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()
Expand All @@ -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
Loading