Repository navigation
Releases: CarlDog/openchronicle-mcp
Release list
v3.8.0 — 2026-10-03
The timestamp release: stored project and memory timestamps are normalized to UTC, so chronological listings compare real instants consistently. This release advances the database to schema 5.
Important upgrade and rollback warning
Take and verify a fresh pre-migration snapshot, then run the candidate image's migration on a disposable copy before upgrading. Follow the migration 005 deploy and rollback runbook.
After migration 005 succeeds, rollback requires restoring the pre-migration snapshot. Do not simply run v3.7.0 against the migrated database. Older images can open schema 5 and write non-UTC or offset-free timestamps without warning; migration 005 will not rerun on that database. A volume served by an older image after migration must not be rolled forward.
For rollback, stop the container, disable its restart policy, freeze Portainer changes, stage and activate the pre-migration snapshot with --expected-schema 4, and only then select v3.7.0. Writes accepted after the snapshot are lost unless exported beforehand; reimporting them is a separate operator-reviewed step.
If migration 005 refuses before making changes, the database remains at schema 4 and reverting to v3.7.0 is safe. Use the reported row IDs to resolve invalid timestamps explicitly before retrying.
Timestamp correctness
- Migration 005 validates and normalizes
projects.created_at,memory_items.created_at, and non-nullmemory_items.updated_atin one savepoint. Aware timestamps retain the same instant and microsecond precision in UTC. Naive, malformed, or out-of-range values stop the migration with a diagnostic naming up to ten affected rows. - New offset-free timestamp input is rejected with a field-specific error by
memory_saveover MCP and REST,oc memory import, and the store. Supply an ISO 8601 timestamp withZor an explicit offset such as+00:00or-05:00. Aware input is accepted and stored in UTC. onboard_gitemits UTC memory timestamps while preserving the author's calendar-day presentation in summaries.- Project listings and per-source memory listings use an ID tie-breaker for equal creation timestamps, making their ordering deterministic.
Compatibility and verification
Exported timestamp text may change after migration even though the represented instants are preserved. Timestamp validation is classified as a MINOR change under the documented API stability exception.
docker-compose.nas.yml is unchanged from v3.6.0. Metrics remain disabled by default. Before upgrading, verify the image's build revision against the tag and require a successful preflight with Integrity: ok. After upgrading, verify package version 3.8.0, the tag's full build revision, schema 5, the migration-completion log, and zero non-UTC stored values using the runbook's read-only check. Schedule the change away from a nightly cloud push and begin latency sampling before recreation.
v3.7.0 — 2026-09-30
The hardening release: safer health responses and logging, guarded offline restore checks, more reliable maintenance, and configuration fixes. Database schema remains at version 4.
Security and reliability fixes
- When API-key authentication is enabled, unauthenticated
/api/v1/healthresponses omitdb_pathandconfig_dir. Authenticated callers retain the full payload; deployments with authentication disabled behave as before. - Non-ASCII API-key headers are handled as invalid credentials instead of triggering a server error.
- Human and JSON log formatting now redact URL userinfo from messages and tracebacks; JSON string extras are redacted too.
httpxandhttpx2request lines are suppressed outside DEBUG. oc serveconfigures logging before container construction so startup problems reachOC_LOG_FILE. Backup-directory errors distinguish missing paths, symlinks, non-directories, unwritable locations, and inaccessible parents.- Cancelled cloud-backup subprocesses are killed and their pipes drained with a five-second bound, preventing an inherited open pipe from indefinitely holding the maintenance lock.
- Maintenance health and scheduling share one state-file path and timestamp parser. Naive timestamps in the maintenance state file are interpreted as UTC. Total embedding-backfill failures receive the
failuremetric label, and the unimplementedgit_onboard_resyncjob reportsskipped. - The CLI and stdio MCP server close their database store on exit.
Restore and configuration changes
- Offline
activateandrollbacknow verify that the installed database accepts a write and record the result instate.json. A newprobeaction rechecks after a fix. The helper refuses root execution, unsafe sidecars, and databases written since the swap. See the offline restore runbook. - Boolean environment settings use a shared parser: blank values keep the default, and unrecognized values log a diagnostic and keep the default. In particular, an invalid
OC_SEARCH_FTS5_ENABLEDvalue now keeps FTS5 enabled instead of silently disabling it. InvalidOC_BACKUP_MCP_ENABLEDvalues remain ERROR-level diagnostics. - Removed the retired one-shot v2 cutover scripts,
scripts/migrate_v2_to_v3.pyandscripts/verify_v3_db.py.
Dependencies and development
- Raised dependency floors to
uvicorn>=0.54.0,openai>=3.19.2, andruff>=0.16.9(with the existing<0.17cap). These are minimum requirements, not a guarantee of the versions resolved into an image. - Added documentation/compose parity tests and strengthened commit-hook identity checks for both author and committer.
Upgrade notes
There is no schema migration, and docker-compose.nas.yml is unchanged from v3.6.0. Metrics remain disabled by default. Backup MCP tools, introduced earlier, remain opt-in and require OC_BACKUP_MCP_ENABLED=true, OC_API_KEY, and an explicit OC_BACKUP_DIR. Do not use db_restore_stage as a smoke test: an unactivated stage has no online discard operation. Manual snapshots are not automatically pruned.
Schedule container recreation away from an active nightly cloud push, and start any before/after latency sampling before the change. Verify package version, build revision, schema 4, health-path filtering, and startup logs. The documented rollback returns to v3.6.0; if backup MCP tools were enabled for this upgrade, disable that opt-in on rollback.
v3.6.0 — 2026-09-28
The offsite release: a nightly, encrypted, append-only push of the newest
backup snapshots to a cloud remote (design 0001 Phase 1, ROADMAP OPS-08).
Deployed 2026-09-29 in the A7 order: stack file version 144 on v3.5.0,
then OC_TAG and the cloud settings. The boot-time run pushed three
snapshots in about 15 seconds, and cloud_backup_status reads ok.
Deploy note (design 0001 amendment A7; the runbook is
cloud_backup.md). The job runs on its
first maintenance tick after boot, and a failed first run waits 24 hours, so
the order matters:
- The operator installs the minimal
[ocdrop]-onlyrclone.confinto the
running v3.5.0 container's/configvolume as uid 1000
(docker exec -i --user 1000:1000 ... 'umask 077; cat > /config/rclone.conf').
The named volume carries it into the new container. - Tag
v3.6.0and let its CI publish the image. - One stack update:
main'sdocker-compose.nas.yml(it adds the
OC_CLOUD_REMOTE,OC_CLOUD_AGE_RECIPIENTSandRCLONE_CONFIGlines)
together withOC_TAG=v3.6.0,OC_CLOUD_REMOTE=ocdrop:openchronicle/nas
andOC_CLOUD_AGE_RECIPIENTS=<primary>,<recovery>(the escrowed public keys). - Verify
health.package_version=3.6.0andhealth.build_revision; after the
boot-time run,cloud_backup_status.status=okand
/api/v1/maintenance/statusshowscloud_backuplast_outcome: ok; the
cloud_backup: age recipientslog line matches escrow.
Rollback is moving OC_TAG back to v3.5.0: no schema change, v3.5.0
ignores the new variables, and the remote keeps what was pushed.
- Nightly offsite push (
cloud_backup, daily). It selects the 3 newest
published snapshots plus the newest from each of the 3 most recent UTC days,
verifies each against its manifest, encrypts the.dband its.jsonwith
age to both escrowed recipients in a private temp dir under the backup root,
andrclone copy --ignore-existings them: the job never deletes or
overwrites. The whole run is bounded at 900 s and a hung child is killed.
It succeeds only when something fresh went offsite: an empty, stale (over
26 h) or future-stamped source, a root run, a missingrclone.conf, invalid
configuration, a manifest mismatch, or an age or rclone failure all fail
the run. WithOC_CLOUD_REMOTEunset it skips and never records a success. - Health gains
cloud_backup_status(additive, MINOR):disabled,ok,
stale(no push within 48 hours, or ever) ormisconfigured, which takes
precedence. It never touchesmaintenance_degradedor
backup_last_run_failed. - The image adds rclone 1.75.1 and age. The entrypoint sets an existing
rclone.confto 600, and reports a failedchmodinstead of hiding it. oc maintenance run-onceprintsSKIPPEDwith the reason when a job
did nothing, instead ofOK.- CI: a PR-only
Image smoke testjob (now a required check) builds the
image and runs the image smoke plus an end-to-end push, decrypt and
append-only check. - Design 0010: the release exception is extended to v3.6.0 (operator,
2026-09-28); no metrics code changed.
Full Changelog: v3.5.0...v3.6.0
v3.5.0 — 2026-09-28
The backup release: design 0017's catalogued, verified snapshots, written to
an exposed backup root that is separate from the live database's volume.
Deployed 2026-09-28 (ROADMAP OPS-04; the 0017 step-5 check passed).
Deploy note: stack 151 already runs the reconciled compose (OPS-03). It sets
OC_BACKUP_DIR=/exports/backups and binds /volume1/docker/openchronicle/exports
(owned by uid 1000, group users, mode 0750). The deploy moves only OC_TAG
to v3.5.0. Verify health.package_version=3.5.0 and health.build_revision.
Then run 0017 step 5: a snapshot lands in /exports/backups, its digest matches
when read over SMB, and a disposable restore passes integrity, foreign-key, row,
ID and search checks. No schema change separates v3.4.0 and v3.5.0, so rolling
back is moving OC_TAG back.
- Nightly backups move to
OC_BACKUP_DIR(0017).db_backup, and the
backupdb_vacuumtakes first, write toauto/underOC_BACKUP_DIR
(default${data_dir}/backups, the old location). Each snapshot is
published in rollback-journal mode with a verified JSON manifest. Retention
still keeps the 7 newest plus the newest per day for 7 days, and it counts
only published snapshots. Snapshots without a manifest, including every
pre-0017 file in/data/backups/auto, are never pruned, so the old ones stay
for explicit review. A snapshot that fails verification is kept as
*.failed-verifyor*.failed-quick-check, and the job fails. - A failing nightly backup shows in health (v3.5.0 pre-deploy review,
finding 1). A new health field,backup_last_run_failed, reads true when
the last scheduleddb_backuprun failed. It comes from the persisted run
and success stamps, so it survives a restart. The next scheduled success
clears it; manual backups do not. It is deliberately separate from
maintenance_degraded, which still means "the database may be corrupt"
(design 0001 section 6.2). Folding a backup failure into it would have sent
operators to a restore. Usually the backup root is at fault, but a live
database that fails its checks also fails the backup; the incident runbook
says how to tell the two apart. Before this, a broken backup root failed every
nightly backup while health read clean. The gap predates this release but
mattered more once backups moved to an operator-managed host bind. The
field is additive (MINOR). - Bad backup configuration never stops startup. An unusable
OC_BACKUP_DIRis logged at ERROR, and manual and scheduled backups fail
until it is fixed. Underrestart: unless-stopped, raising instead would
crash-loop the memory service. - Backup MCP tools, off by default (0017).
db_backup_create,
db_backup_list,db_backup_verify,db_restore_plananddb_restore_stage
register only when
OC_BACKUP_MCP_ENABLED=true,OC_API_KEYis set andOC_BACKUP_DIRis
explicit. The default tool inventory is unchanged, so this is MINOR under
STABILITY.md. Enabling them is ROADMAP OPS-07. - Guarded offline restore (
scripts/offline_restore.pyand the runbook):
stage,activate,rollbackandretire-stage, drilled on the NAS on
2026-09-24. - Design 0010 release gate: the operator extended the v3.4.0 exception to
this release (2026-09-28). v3.5.0 changes no metrics code, and metrics stay
off by default. The exception covers release, not enablement. - Housekeeping:
uv.lockrecorded the project as 3.3.0 because the v3.4.0
release did not refresh it. It now matches.
Full Changelog: v3.4.0...v3.5.0
v3.4.0 — 2026-09-24
The correctness release: the four fleet-review #27 fixes, the model-revision
fix, the query-revision race fix, and design 0010's metrics instrumentation,
shipped off by default under an operator exception (below).
Deploy note: the tag was not deployed when it was cut. It was deployed
2026-09-28 (ROADMAP OPS-01). By then OPS-03 had reconciled stack 151's
compose with the repository file and OC_LOG_FILE was already set, so the
deploy moved only OC_TAG to v3.4.0 (portainer_set_stack_env with an image
pull). Verify health.package_version=3.4.0, health.build_revision
(9b1e83e6), and model_revision_state. No schema change separates v3.3.0 and
v3.4.0, so rolling back is moving OC_TAG back.
- Blank content is refused before any write (fleet-review #27 item 1).
memory_update(content="")over MCP blanked the memory and deleted its
embedding while reporting success. REST and the CLI let whitespace-only
content through. The add and update use cases now refuse blank content
ahead of any write, on every surface. Versioning: STABILITY.md allows
tightening validation without a MAJOR bump when the value was already
documented as invalid. Memory content already was: the REST schema
declaredmin_length=1for add and update, and MCPmemory_saverefused
blank content. This applies that rule consistently and closes a data-loss
path, so it ships in a MINOR release. - Background backfill failures are visible (#27 item 2). A failed
memory_embed background=truerun is logged at ERROR, and health gains
last_background_backfill(outcome,finished_at, counts or
error_type). Additive. - Maintenance overlap warnings are truthful (#27 item 3). A job queued
behind another logs one INFO line; a real overlap warns once per run,
instead of once per tick (about 1,200 false warnings per long backfill). - OpenAI-compatible embedding responses are validated at the boundary
(#27 item 4). Non-finite, empty, mixed-length or surplus vectors are
refused, sharing Ollama's validator. - An unknown model revision is no longer "no revision" (design 0014 §1.1,
ADR 0005 §7). A failed revision probe used to be cached as "no revision",
which blanked semantic search while health readactiveand re-embedded
the corpus. Adapters now expose a revision snapshot, writes refuse while
it is unknown, and a service-owned refresher probes and reconciles. Health
gainsmodel_revision_stateandmodel_revision_verified_at, and reads
degradedwhile the revision is unknown. - Queries stay on one model revision (PR #34, design 0016 track 1). A
search snapshots the revision around the query embed, retries once on a
change, falls back to keyword results when changes continue, and
semantic-only search returnsMODEL_REVISION_CHANGED(HTTP 502). - The NAS log file exists.
OC_LOG_FILEdefaults to
/output/logs/openchronicle.log, on the output volume; the old default
was on no volume, so every boot fell back to stderr. httpx no longer logs
request URLs, or anyOLLAMA_HOSTcredentials, at INFO. - Smaller fixes:
onboard_git's local git calls get the allowlisted environment. An
inheritedGIT_DIRhad walked the wrong repository.- The per-request MCP lifespan lines log at DEBUG.
- A skipped maintenance backfill records
skippedrather thanok. - Reconciliation honours a disabled
embedding_backfilljob. oc memory update --content ""is refused.- The entrypoint's bootstrap line goes to stderr.
- The repository NAS compose keeps the REST Host-list fallback (PR #35);
it is compose-only, and stack 151 is detached.
- Metrics instrumentation (design 0010), off by default. Bounded
Prometheus metrics and a guarded/metricsendpoint exist only when
OC_METRICS_ENABLED=true; the disabled path bypasses the recorder. An
optional local Prometheus profile ships in the NAS compose. Operator
exception, 2026-09-24: design 0010 blocks any release carrying this code
while its B/A (disabled-path) overhead comparison is inconclusive, and it
still is. The operator released it anyway: metrics stay off, and the
measured disabled-path median losses (0.129% and 0.399%) sit inside host
noise. Enabling metrics still needs 0010's gates. - CI: every image is smoke-tested before it is pushed (build revision,
runtime imports,/health). - Housekeeping: line endings normalized to LF (
.gitattributes), and
dependency updates (the uv runtime group, uvicorn, setuptools, wheel, and
the smol-toml fix in the Markdown tooling).
Deliberately deferred low-severity follow-ups are listed in V3_PLAN item 9.
Full Changelog: v3.3.0...v3.4.0
v3.3.0 — The health-honesty release
The health-honesty release: ADR 0009 end-to-end permanent embed-failure classification, plus started-job reindexes. After redeploying, run one background backfill so over-length rows are parked as tombstones and health reports them as unembeddable.
Full Changelog: v3.2.0...v3.3.0
v3.2.0 — 2026-08-29
The embedding-identity release: ADR 0005 end-to-end (schema v2+v3
migrations, CAS publication, space-filtered search, truthful Ollama
contract, batched backfill), the generic cloud-provider path, and the
provider-benchmark hardening that cleared LAN-local nomic-embed-text
for the cutover this release deploys.
Deploy note: the migrations mark all pre-existing vectors stale;
run oc maintenance run-once embedding_backfill (or the memory_embed
MCP tool) after the redeploy — semantic search degrades to FTS5-only
until the reindex finishes (stale counts down in health).
- The generic cloud-provider path is safe by construction. The
openaiadapter'ssettings_fingerprintnow includesbase_url,
so the same model label on different OpenAI-compatible hosts (Voyage,
Gemini, Mistral, Together, ...) is a different vector space — a host
switch triggers the standard reindex instead of silently mixing
vectors.OPENAI_BASE_URLis Portainer-wired; the provider matrix
lives in design/0006. - Backfill logs known provider failures as one line. A categorized
ProviderError(e.g. atruncate:falseover-length rejection)
already carries the actionable upstream message; it no longer emits
a full traceback at WARNING — hundreds of them flooded the log
during benchmarking. Stacks move to DEBUG; unexpected exception
types keep theirs. - Local Ollama over HTTPS is first-class. A scheme-less
OLLAMA_HOSTgetshttp://prepended instead of producing a broken
URL;https://hosts work end-to-end (embed + capability probe);
andOLLAMA_VERIFY_TLS=0permits LAN self-signed certs (default:
verify, disabling logs a warning). - Content-egress notice for cloud embedding providers. When the
configured embedding endpoint is not clearly LAN-local, startup logs
a WARNING naming the endpoint and the consequence (every save's full
content and every semantic query leave this host), and health
carries an additivecontent_egress: "remote"|"local"field.
Fail-safe classification: ambiguous hosts warn. Applies toopenai
anywhere and toollamapointed at a cloud host alike. A notice,
not a control — the operator's cloud choice stands; the secure path
remains a LAN-local provider. OC_LOG_FILE: logs that survive a container recreate. A
Portainer redeploy recreates the container and its stderr history
dies with it — observed mid-diagnosis when the v3.1.0 deploy
destroyed the v3.0.0 logs a bug report needed. When set, the log
stream is mirrored to a size-rotating file (5 MiB × 4, same
format/level as stderr, fail-soft on unwritable paths); the NAS
compose defaults it onto the output volume. stderr stays primary.memory_stats.by_tagis bounded (dogfooding feedback). The tag
histogram now shows thetop_tagsmost frequent tags (count-ordered,
default 25, param on MCP + REST) with another_tagsrollup count —
an unscoped call used to return the corpus's entire tag tail
(~700 entries, mostly count-1) regardless of what the caller wanted.- Bounded batch backfill — ADR 0005 Phase D (design 0003, Finding
3). The backfill chunks throughembed_batch(32 per chunk) instead
of one HTTP round-trip per memory, with a per-item fallback that
isolates a poisoned input from its chunk-mates and catches
wrong-cardinality batch answers. The deploy-time reindex now runs at
batch speed. All ADR 0005 phases are complete. - Truthful Ollama adapter contract + full space identity — ADR 0005
Phase C (design 0003, Findings 1+2). Schema v3 adds
model_revision(nullable,IS-matched) andsettings_fingerprint
(one shared canonical-JSON hash, never per-adapter). The Ollama
adapter now sendstruncate: false(an over-length input fails
visibly with Ollama's own actionable message rather than silently
embedding a prefix), sendsdimensionsonly when configured and
validates the response against it, validates every returned vector
(cardinality, emptiness, finiteness, batch consistency), surfaces
structured error bodies, short-circuits empty batches, and derives
model_revisionfrom a cached non-fatal/api/tagsprobe. Health
gains the dimensions-truth trio (configured_dimensions/
dimensions/stored_dimensions) plusmodel_revision. - Composite embedding identity — ADR 0005 Phase B (accepted after
a three-critic adversarial review of the design). Schema v2 adds
provider+content_hashtomemory_embeddings; vector
publication is compare-and-swap on the memory's current content
(closing the slow-older-writer race); semantic search filters the
full space identity — a wrong-provider row under the same model
label, a wrong-dimensions row, or a pre-migration sentinel is
invisible to ranking, never mixed or crashed. Health'sstale
becomes the sum of disjointspace_mismatch+content_mismatch
buckets (a strictly more truthful refinement of the old
model-string-only count — documented MINOR). Deploy note: the
migration marks every pre-existing vector stale; run
oc maintenance run-once embedding_backfillafter the redeploy —
semantic search serves FTS5-only for the few minutes the reindex
takes, observable in health asstalecounts down to 0. memory_listgains filtered chronological enumeration (design
0002, batch B — the demonstrated Mnemosyne consumer). Additive on
every surface (MCP, REST, use case, port):tags(require-all),
exclude_tags(drop-any), andorder_by—"pinned_first"keeps
today's browsing order,"created_at"is pure chronology with no
pin float. All predicates apply in SQL before pagination, so "the N
newest rows carrying tag X but not tag Y" is one bounded call
instead of a full-project compact scan plus per-row gets. MINOR;
tool-schema snapshot regenerated.
Full Changelog: v3.1.0...v3.2.0
v3.1.0 — 2026-08-29
The comparative-review remediation release. A validation pass first
re-verified every claim in the four docs/design/ review documents
against HEAD (all SHIPPED claims confirmed; ~14 of 16 defect claims
still held), then every implementation-ready verified defect shipped
as its own commit — assessment revisions 116-126, 688 → 758 tests.
MINOR: the surface changes are additive (build_revision in
diagnostics, include_pinned and the new provider-health fields), and
top_k becoming a total budget enforces the already-documented
contract.
- Provider and maintenance health stop being success-shaped
(design 0003 Finding 4 + design 0004 Finding 9). An all-failed
embedding backfill now fails the maintenance job instead of
recording a nightly "ok" with zero vectors generated; provider
failure counters cover search, save, AND backfill (new
failure_count/last_failure_at/last_failure_ophealth fields
drive the degraded status — a dead provider used to readactive
until someone searched); andmaintenance_degradedderives from
persisted run/success evidence as well as the in-process flag, so a
container restart no longer clears a failed integrity check from
the health surface. All additive. - Staged backups are validated before publication (design 0003,
Finding 5). The nightly backup staged to.tmpand atomically
renamed with no check the artifact was openable. The staged file now
must passPRAGMA quick_check(read-only open) before it may
replace the previous backup; a failing artifact is quarantined as
<dest>.failed-quick-checkfor forensics — evidence the live DB may
be corrupt — never deleted, never published, never pruned by
retention. include_pinnedis available on every search surface (design
0002, batch A; operator decision). MCPmemory_searchand
GET /api/v1/memory/searchgain the visibility switch the CLI
already had:falsehides pinned items entirely — no float, no
ranking, strict project scope — distinct frompinned_limit=0,
which only stops the float. Additive optional parameter (MINOR);
tool-schema snapshot regenerated.top_kis now a total response budget (design 0002, batch A;
operator-decided contract). Floated pins and ranked results share
one combined stream thattop_kbounds andoffsetpaginates — a
top_k=8search returns at most 8 items, pins included, where it
could previously returntop_k + pinned_limit. Pin-heavy queries
now yield fewer ranked hits per page; pagination walks the combined
stream with no duplicates or gaps.context_recent.memory_limit
becomes truthful transitively.- A content update invalidates its embedding before re-embedding
(design 0002, batch A). A failed re-embed used to leave the OLD
vector in place with a current model string — semantic search ranked
the old content indefinitely and backfill never saw the row. The
vector is now deleted on every content change (provider configured
or not) before regeneration is attempted: failure leaves it missing
and backfill-visible, never stale. Tags-only updates keep it. - Semantic search filters eligibility before its top-N window
(design 0002, batch A). Similarity ranking used to run over the
whole model-scoped embedding table with project/tag filtering
applied only after the bounded top-N — unrelated vectors consumed
the window and the best in-scope results could be missed. A new
eligible_memory_idsstore primitive (same scope + tags rules as
ranked search, by construction) narrows the candidate set first. - Search tag filters apply in SQL, before LIMIT (design 0002,
batch A). Both branches used to over-fetch a bounded window
(limit * 4FTS rows, a 200-row recency scan on the fallback) and
tag-filter in Python — a valid tagged result past the window was
silently omitted. Tag containment now runs inside the query via
JSON1json_eachon both branches, so a tagged row is found
regardless of how many untagged rows outrank it. - MCP
onboard_gitclones from github.com only (design 0004,
Finding 3, destination-policy half; decided 2026-08-28 — no
non-GitHub consumer exists). The server-side tool used to point its
owngit cloneat any HTTPS/SSH host, including loopback, RFC1918,
and cloud-metadata addresses.repo_urlis now gated to
https://github.com/<owner>/<repo>before the worker thread runs;
other hosts and local paths remain available through the
oc onboard gitCLI. Widening path, if a non-GitHub consumer ever
appears: address-class rejection, not deleting the gate. - Deploy identity:
build_revisionin health,OC_TAGrequired
(design 0004, Finding 4). CI bakes the full git SHA to
/app/build-revision(a file, not an ENV — a compose edit cannot
assert a revision the image was never built from); the diagnostics
payload on both health surfaces andoc versionreport it, so a
same-version redeploy is finally verifiable from health alone.
Outside an image it honestly reads"unknown".
docker-compose.nas.ymlnow requiresOC_TAG(${OC_TAG:?...})
instead of silently falling back to:latest. onboard_git'sgit clonechild runs least-privilege (design
0004, Finding 3, child-env half). The subprocess env was
os.environ.copy()— every server secret crossed the boundary for an
operation that needs none of them. Now an allowlist (binary
discovery, Windows plumbing, locale, TLS roots, proxies, ssh-agent),
sentinel-tested so a future secret cannot cross either; the raw
OC_GIT_TOKENnever enters the child (only the derived host-scoped
header does);GIT_TERMINAL_PROMPT=0fails a token-less private
clone fast instead of blocking 300s on a prompt nothing can answer;
the clone is--no-checkout(the walk reads history, never the
worktree); https URLs carrying userinfo, query strings, or fragments
are refused; and clone stderr is scrubbed of token material before it
reaches an error message. Server-side destination policy (loopback /
private / metadata addresses) is deliberately not changed here — it
is an open product decision recorded in the review.- The portable JSON envelope is a real versioned contract, and export
publication is atomic. First implementation batch from the
comparative reviews (design 0004, Finding 2). Import used to check
only thatformat_versionexisted:format_version: 999imported
"successfully", an envelope missing its arrays read as a legitimate
empty restore, rows without anidleakedKeyError, and an unknown
project reference or duplicate in-envelope id died as a raw
sqlite3.IntegrityError. The whole envelope is now validated before
the write transaction opens — version dispatch, required arrays,
per-row types, project references against store ∪ envelope, duplicate
ids — with every rejection aValidationErrornaming the failing
collection, index, and row id. Id shape and unknown keys are
deliberately not validated (restore must not fail on data the store
already held; newer-build envelopes must import).oc memory export --outnow writes to amkstempsibling (0600 where supported) and
publishes withos.replace, so a failed export can no longer
truncate the previous good backup. 688 → 708 tests. - MCP migration readiness. An mcp 2.x migration was assessed and
deferred on named triggers rather than on a premise that turned out to
be false.pyproject.toml,.github/dependabot.ymland
docs/V3_PLAN.mdhad all justified themcp<2cap by claiming 2.0
"moved FastMCP to the standalonefastmcppackage". Verified against
the 2.1.1 wheel:mcp.server.fastmcpis a tombstone module raising
ModuleNotFoundError, and FastMCP was renamed in-tree to
mcp.server.mcpserver.MCPServer. The cap is still correct; the reason
was not, and the wrong reason made migration look like a third-party
dependency swap rather than a first-party rename. - The MCP tool signatures are now actually enforced.
STABILITY.mdhas
bound since the v3.0.0 tag — a breaking change to a tool signature is a
MAJOR event — and nothing checked it. All 18 schemas are snapshotted to a
committed fixture, with a test that fails on any change and names the tool.
Descriptions are deliberately excluded: they are the LLM-facing contract
and worth getting right, but they are documentation, not signature, and a
guard that fails on every reworded docstring gets regenerated reflexively
until it guards nothing. Verified in both directions — a required-ness
change fails, a description-only edit passes. It doubles as the
pre-migration baseline: the only way to later prove lifting themcp<2
pin was schema-invisible is to diff against a snapshot taken before it. - Three MCP baselines, so a future migration can be proved
schema-invisible rather than asserted. The tool-schema snapshot now
coversoutputSchemaas well asinputSchema; the client-visible
error envelope is pinned across five failure shapes on mcp 1.29.0
(previously zero coverage — no test referencedisError); and a forged
Hostis asserted to be rejected through the mounted/mcp, which
mutation testing showed was one line from silent disablement.
679 → 688 tests. - Dependency floor
mcp>=1.0→>=1.29in both themcpanddev
extras — the version those baselines were captured on. The old floor
was fiction; nothing had verified the server against mcp 1.0.
uv.lockregenerated, which also cleared a stale3.0.0rc8project
version left over from the release.
Full Changelog: v3.0.0...v3.1.0
v3.0.0 — 2026-08-28
The first stable v3 tag. From here docs/api/STABILITY.md binds: the
REST schemas, MCP tool signatures, and core.json schema are under
semver, and a breaking change to any of them is a MAJOR event rather
than "the surface as designed".
Everything below shipped between rc8 (2026-08-17) and this tag.
-
cluster_commitsno longer hangs forever on a non-positive cap. With
max_clusters <= 0the merge loop never terminated: once the list was down
to one entry,smallest_idxwas 0, thelen(merged) > 1arm was false, so
merge_intowas 0 too — the pop was skipped, nothing changed, and1 > 0
kept it spinning. This was reachable from outside: theonboard_gitMCP
tool passedmax_clustersthrough unbounded while clamping its immediate
neighbourmax_commits_per_clusterone line above, and
oc onboard git --max-memorieshad no bound either. An agent passing 0
would wedge an MCP worker thread permanently. Now floored at both the tool
boundary and inside the algorithm; a nonsensical cap degrades to one
cluster rather than failing the run. 676 → 678 tests. -
Ollama Cloud recorded as trigger-gated research, not a rejection.
Investigated against the live API 2026-08-28: it hosts 19 models, none
advertising theembeddingcapability, and every known embedding model
name 404s — so it offers OpenChronicle nothing today. That is not the
same as a dead end. The 19-model baseline is recorded so a future check
is a diff rather than a re-investigation, the re-check needs no API key
(/api/tagsis public), and the adopt-work is pre-scoped: the adapter
already targets/api/embed, the correct cloud path, so the change is one
Authorization: Bearerheader. Filed in V3_PLAN's follow-ups for the
quarterly audit cadence. -
_get_containerextracted from all five MCP tool modules. It was
byte-identical in each — one line, so the duplication cost nothing to
read, but it hardcoded the lifespan's"container"key five separate
times. That is the shape the extraction bar exists for: not volume, but a
contract that drifts the day someone renames the key and updates four of
five copies. Now one helper with the key as a named constant, pinned
againstserver.py's lifespan by a test that fails if either side is
renamed alone. -
PII scanning gets a CI backstop. The home-path and personal-email
checks existed only in the pre-commit hook, which is bypassable by design
—--no-verify, an unsetcore.hooksPath, a fresh clone before the
installer runs, a push from another machine. Now enforced in CI too, with
the patterns kept generic (the file is public, so baking in a real name
would leak what it guards) and findings masked in the failure message,
since a test that prints the PII it caught into a public log defeats
itself. Positive and negative controls ship with it — the negatives matter
more, because a pattern that also flaggedusers.noreply.github.comor
/data/...would fail on every commit and be deleted within a day.The repo's one historical gitleaks hit is now allowlisted by full SHA,
after being identified rather than assumed:generic-api-keyin a test
file absent from HEAD, whose "secret" gitleaks itself renders as
REDACT…— a placeholder. A full-history sweep now reports clean, so the
next hit means something instead of being the standing one everyone
learned to ignore.Also:
v3/developis no longer a CI trigger (121 commits behindmain,
none ahead, untouched since 2026-05-05) and SECURITY.md stops describing
the v3 cutover as a future event;.codex/**anduv.lockjoin
paths-ignore, both tracked and both exactly the docs/tooling commits the
list exists to keep from rebuilding the image.The
gitleaks.ymlcheckout pin was left to Dependabot rather than
hand-edited, and PR #20 has since merged it —actions/checkoutis now
@v7across all four uses, andgitleaks-actionis at@v3, verified
green against the new commit-allowlist. The stated reason for deferring
was wrong, though: this file is fleet-canonical, but the fleet had
already split 2-2 on these versions, so the merge moved this repo onto
the majority side rather than forking it.downloader-mcpand
portainer-mcpstill lag at v6/v2. -
oc config showsurvives the config being broken. It is the command
an operator runs because core.json is wrong, and it was the one command
with no error handling: pre-container commands bypass_build_container,
and itstry/exceptwith them. Three compounding defects — a non-UTF-8
core.json escaped as a rawUnicodeDecodeError(the file read sits inside
the sametryas the parse, but onlyJSONDecodeErrorwas caught, losing
the filenameConfigLoadErrorexists to attach); malformed JSON produced a
traceback rather than an exit code; and an existing-but-empty core.json
reported as "not found", sending the operator to look for a missing file
instead of an empty one. All three reproduced before the fix and verified
after. 659 → 666 tests. -
Two conventions promoted from prose to enforcement. The agent
instructions say to useutc_now()rather than an inline
datetime.now(UTC); seven sites across six files had drifted past it.
All now route through the helper, and an AST-based guard fails if a new
one appears — AST rather than text matching, because the text version
flagged a comment that merely mentions the call, and a guard that cries
wolf gets deleted. This is not only style: a naked clock read is what
makes time un-fakeable in a test.Separately,
scan_repository()now refuses to return a corpus smaller
than 50 files. Eight zero-tolerance tests iterate it and assert nothing
matches their forbidden pattern, so a scan returning nothing turned all
eight green while checking nothing — and the realistic trigger, the repo
root resolving somewhere unexpected, is silent by construction. The real
corpus is 197 files, so the floor only ever catches a broken scan. -
The content cap is enforced in one place instead of four. The
100,000-character limit lived as hardcoded literals in two driver files
and nowhere in between: MCP hand-rolled the check twice, the REST routes
declared it twice via Pydantic, and the use cases had none. So the same
store rejected a 200KB memory over MCP and HTTP whileoc memory addand
oc memory importaccepted it — not a drift risk, an existing
inconsistency.MAX_CONTENT_CHARSnow sits besideMemoryItem;
add_memoryandupdate_memoryenforce it, so every caller inherits it,
and the drivers reference the constant as fast-fail decoration. Raising or
lowering the cap is now one edit.oc memory importdeliberately reports rather than enforces: a restore
is not new input, an over-cap row can already exist from before this fix,
and failing the disaster-recovery path on data the operator already owns
would be the worse bug. Such rows import intact, are counted as
oversized_content, and are named in a warning. A structural test now
fails if any surface hardcodes the number again. Found by the first
phase-end audit. 647 → 658 tests. -
A blank path env var no longer outranks the default.
env_vars.md
states that an empty-string env var counts as unset at every config
boundary; the path boundary was the one place that didn't honour it.
os.environ.getreturns""for a blank var,""is notNone, and
Path("")isPath(".")— soOC_DB_PATH=silently relocated the SQLite
store to the working directory, and a blankOC_DATA_DIRdemoted every
derived path to a bare relative name instead of falling through. Both are
one${VAR:-}line away in a compose file, which is exactly the form the
fleet convention pushes operator-tunable values toward. Empty and
whitespace-only now fall through, matching theis_disabled()/
env_override()precedent. The value itself is never rewritten — only the
emptiness test strips — so a path an operator actually meant survives
verbatim. Constructor params are deliberately not normalized: those are
code, not operator input. A sweep of every other env read insrc/
confirmed no sibling violators. Found by the first phase-end audit.
637 → 647 tests. -
pyproject.toml's description drops the semantic-search overclaim, the
same correction the GitHub repository description got earlier the same day.
Semantic retrieval is opt-in, not shipped behaviour: the provider defaults
tononeand both compose files pass an emptyOC_EMBEDDING_PROVIDER, so
a stock container is keyword-only. Deliberately held back from its own push
—pyproject.tomlis not inpaths-ignore, so shipping it alone would have
rebuilt the image and bounced the live stack for one docstring. It rode
along with the maintenance-merge fix instead. -
maintenance.jobsmerges onto the defaults instead of replacing them.
Acore.jsonthat named one job silently deleted every other — an
operator halving the backup interval lostdb_vacuum,
db_integrity_checkandembedding_backfillwith no warning, because
load_jobsonly ever warned about unknown job names, never missing
ones. The latent half was worse: the entrypoint seeds/configfrom
core.json.examplewithcp -rn, and that example enumerates all five
jobs, so the first release to add a sixth would have found every existing
deployment quietly ignoring it. An entry now overrides the matching
default by name and unmentioned jobs keep their defaults, which makes both
failures impossible. Omitting a job no longer disables it — set
"enabled": false, the way the shipped example already expresses "off"
forgit_onboard_resync. Job ordering follows `_DEFAUL...
v3.0.0-rc8 — 2026-08-17
- Bounded pinned prepend (
pinned_limit):memory_searchcaps the
pinned prepend at the newest 10 pins by default (tunable 0–1000 via
pinned_limiton MCP/REST/CLI; 0 = none). Observed live right after
the rc7 deploy: atop_k=2unscoped search against the 85-pin NAS
store returned 87 results. Capped-out pins are omitted entirely —
the exclusion set still covers all pins, so they can't re-enter
through the keyword or semantic ranking. Enumerate every standing
rule withmemory_list(pinned_only=true). 589 tests.
Full Changelog: v3.0.0-rc7...v3.0.0-rc8