Skip to content

Releases: CarlDog/openchronicle-mcp

v3.8.0 — 2026-10-03

Choose a tag to compare

@CarlDog CarlDog released this 08 Oct 02:50
3e9fa8d

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-null memory_items.updated_at in 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_save over MCP and REST, oc memory import, and the store. Supply an ISO 8601 timestamp with Z or an explicit offset such as +00:00 or -05:00. Aware input is accepted and stored in UTC.
  • onboard_git emits 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.

Full changelog · Tagged release notes

v3.7.0 — 2026-09-30

Choose a tag to compare

@CarlDog CarlDog released this 08 Oct 02:49
7feac57

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/health responses omit db_path and config_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. httpx and httpx2 request lines are suppressed outside DEBUG.
  • oc serve configures logging before container construction so startup problems reach OC_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 failure metric label, and the unimplemented git_onboard_resync job reports skipped.
  • The CLI and stdio MCP server close their database store on exit.

Restore and configuration changes

  • Offline activate and rollback now verify that the installed database accepts a write and record the result in state.json. A new probe action 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_ENABLED value now keeps FTS5 enabled instead of silently disabling it. Invalid OC_BACKUP_MCP_ENABLED values remain ERROR-level diagnostics.
  • Removed the retired one-shot v2 cutover scripts, scripts/migrate_v2_to_v3.py and scripts/verify_v3_db.py.

Dependencies and development

  • Raised dependency floors to uvicorn>=0.54.0, openai>=3.19.2, and ruff>=0.16.9 (with the existing <0.17 cap). 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.

Full changelog · Tagged release notes

v3.6.0 — 2026-09-28

Choose a tag to compare

@CarlDog CarlDog released this 29 Sep 06:04
99bd68c

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:

  1. The operator installs the minimal [ocdrop]-only rclone.conf into the
    running v3.5.0 container's /config volume as uid 1000
    (docker exec -i --user 1000:1000 ... 'umask 077; cat > /config/rclone.conf').
    The named volume carries it into the new container.
  2. Tag v3.6.0 and let its CI publish the image.
  3. One stack update: main's docker-compose.nas.yml (it adds the
    OC_CLOUD_REMOTE, OC_CLOUD_AGE_RECIPIENTS and RCLONE_CONFIG lines)
    together with OC_TAG=v3.6.0, OC_CLOUD_REMOTE=ocdrop:openchronicle/nas
    and OC_CLOUD_AGE_RECIPIENTS=<primary>,<recovery> (the escrowed public keys).
  4. Verify health.package_version=3.6.0 and health.build_revision; after the
    boot-time run, cloud_backup_status.status=ok and
    /api/v1/maintenance/status shows cloud_backup last_outcome: ok; the
    cloud_backup: age recipients log 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 .db and its .json with
    age to both escrowed recipients in a private temp dir under the backup root,
    and rclone 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 missing rclone.conf, invalid
    configuration, a manifest mismatch, or an age or rclone failure all fail
    the run. With OC_CLOUD_REMOTE unset it skips and never records a success.
  • Health gains cloud_backup_status (additive, MINOR): disabled, ok,
    stale (no push within 48 hours, or ever) or misconfigured, which takes
    precedence. It never touches maintenance_degraded or
    backup_last_run_failed.
  • The image adds rclone 1.75.1 and age. The entrypoint sets an existing
    rclone.conf to 600, and reports a failed chmod instead of hiding it.
  • oc maintenance run-once prints SKIPPED with the reason when a job
    did nothing, instead of OK.
  • CI: a PR-only Image smoke test job (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

Choose a tag to compare

@CarlDog CarlDog released this 29 Sep 06:04
d1c8be2

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
    backup db_vacuum takes first, write to auto/ under OC_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-verify or *.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 scheduled db_backup run 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_DIR is logged at ERROR, and manual and scheduled backups fail
    until it is fixed. Under restart: 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_plan and db_restore_stage
    register only when
    OC_BACKUP_MCP_ENABLED=true, OC_API_KEY is set and OC_BACKUP_DIR is
    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.py and the runbook):
    stage, activate, rollback and retire-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.lock recorded 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

Choose a tag to compare

@CarlDog CarlDog released this 29 Sep 06:04
9b1e83e

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
    declared min_length=1 for add and update, and MCP memory_save refused
    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=true run 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 read active and 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
    gains model_revision_state and model_revision_verified_at, and reads
    degraded while 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 returns MODEL_REVISION_CHANGED (HTTP 502).
  • The NAS log file exists. OC_LOG_FILE defaults 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 any OLLAMA_HOST credentials, at INFO.
  • Smaller fixes:
    • onboard_git's local git calls get the allowlisted environment. An
      inherited GIT_DIR had walked the wrong repository.
    • The per-request MCP lifespan lines log at DEBUG.
    • A skipped maintenance backfill records skipped rather than ok.
    • Reconciliation honours a disabled embedding_backfill job.
    • 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 /metrics endpoint 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

Choose a tag to compare

@CarlDog CarlDog released this 01 Sep 21:27

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

Choose a tag to compare

@CarlDog CarlDog released this 29 Sep 06:04

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
    openai adapter's settings_fingerprint now includes base_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_URL is Portainer-wired; the provider matrix
    lives in design/0006.
  • Backfill logs known provider failures as one line. A categorized
    ProviderError (e.g. a truncate:false over-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_HOST gets http:// prepended instead of producing a broken
    URL; https:// hosts work end-to-end (embed + capability probe);
    and OLLAMA_VERIFY_TLS=0 permits 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 additive content_egress: "remote"|"local" field.
    Fail-safe classification: ambiguous hosts warn. Applies to openai
    anywhere and to ollama pointed 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_tag is bounded (dogfooding feedback). The tag
    histogram now shows the top_tags most frequent tags (count-ordered,
    default 25, param on MCP + REST) with an other_tags rollup 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 through embed_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) and settings_fingerprint
    (one shared canonical-JSON hash, never per-adapter). The Ollama
    adapter now sends truncate: false (an over-length input fails
    visibly with Ollama's own actionable message rather than silently
    embedding a prefix), sends dimensions only 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_revision from a cached non-fatal /api/tags probe. Health
    gains the dimensions-truth trio (configured_dimensions /
    dimensions / stored_dimensions) plus model_revision.
  • Composite embedding identity — ADR 0005 Phase B (accepted after
    a three-critic adversarial review of the design). Schema v2 adds
    provider + content_hash to memory_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's stale
    becomes the sum of disjoint space_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_backfill after the redeploy —
    semantic search serves FTS5-only for the few minutes the reindex
    takes, observable in health as stale counts down to 0.
  • memory_list gains 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), and order_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

Choose a tag to compare

@CarlDog CarlDog released this 29 Sep 06:04

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_op health fields
    drive the degraded status — a dead provider used to read active
    until someone searched); and maintenance_degraded derives 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 .tmp and atomically
    renamed with no check the artifact was openable. The staged file now
    must pass PRAGMA quick_check (read-only open) before it may
    replace the previous backup; a failing artifact is quarantined as
    <dest>.failed-quick-check for forensics — evidence the live DB may
    be corrupt — never deleted, never published, never pruned by
    retention.
  • include_pinned is available on every search surface (design
    0002, batch A; operator decision). MCP memory_search and
    GET /api/v1/memory/search gain the visibility switch the CLI
    already had: false hides pinned items entirely — no float, no
    ranking, strict project scope — distinct from pinned_limit=0,
    which only stops the float. Additive optional parameter (MINOR);
    tool-schema snapshot regenerated.
  • top_k is now a total response budget (design 0002, batch A;
    operator-decided contract). Floated pins and ranked results share
    one combined stream that top_k bounds and offset paginates — a
    top_k=8 search returns at most 8 items, pins included, where it
    could previously return top_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_ids store 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 * 4 FTS 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
    JSON1 json_each on both branches, so a tagged row is found
    regardless of how many untagged rows outrank it.
  • MCP onboard_git clones 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
    own git clone at any HTTPS/SSH host, including loopback, RFC1918,
    and cloud-metadata addresses. repo_url is now gated to
    https://github.com/<owner>/<repo> before the worker thread runs;
    other hosts and local paths remain available through the
    oc onboard git CLI. Widening path, if a non-GitHub consumer ever
    appears: address-class rejection, not deleting the gate.
  • Deploy identity: build_revision in health, OC_TAG required
    (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 and oc version report it, so a
    same-version redeploy is finally verifiable from health alone.
    Outside an image it honestly reads "unknown".
    docker-compose.nas.yml now requires OC_TAG (${OC_TAG:?...})
    instead of silently falling back to :latest.
  • onboard_git's git clone child 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_TOKEN never enters the child (only the derived host-scoped
    header does); GIT_TERMINAL_PROMPT=0 fails 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 that format_version existed: format_version: 999 imported
    "successfully", an envelope missing its arrays read as a legitimate
    empty restore, rows without an id leaked KeyError, 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 a ValidationError naming 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 --out now writes to a mkstemp sibling (0600 where supported) and
    publishes with os.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.yml and
    docs/V3_PLAN.md had all justified the mcp<2 cap by claiming 2.0
    "moved FastMCP to the standalone fastmcp package". Verified against
    the 2.1.1 wheel: mcp.server.fastmcp is 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.md has
    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 the mcp<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
    covers outputSchema as well as inputSchema; the client-visible
    error envelope is pinned across five failure shapes on mcp 1.29.0
    (previously zero coverage — no test referenced isError); and a forged
    Host is 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.29 in both the mcp and dev
    extras — the version those baselines were captured on. The old floor
    was fiction; nothing had verified the server against mcp 1.0.
    uv.lock regenerated, which also cleared a stale 3.0.0rc8 project
    version left over from the release.

Full Changelog: v3.0.0...v3.1.0

v3.0.0 — 2026-08-28

Choose a tag to compare

@CarlDog CarlDog released this 29 Sep 06:04

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_commits no longer hangs forever on a non-positive cap. With
    max_clusters <= 0 the merge loop never terminated: once the list was down
    to one entry, smallest_idx was 0, the len(merged) > 1 arm was false, so
    merge_into was 0 too — the pop was skipped, nothing changed, and 1 > 0
    kept it spinning. This was reachable from outside: the onboard_git MCP
    tool passed max_clusters through unbounded while clamping its immediate
    neighbour max_commits_per_cluster one line above, and
    oc onboard git --max-memories had 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 the embedding capability, 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/tags is public), and the adopt-work is pre-scoped: the adapter
    already targets /api/embed, the correct cloud path, so the change is one
    Authorization: Bearer header. Filed in V3_PLAN's follow-ups for the
    quarterly audit cadence.

  • _get_container extracted 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
    against server.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 unset core.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 flagged users.noreply.github.com or
    /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-key in 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/develop is no longer a CI trigger (121 commits behind main,
    none ahead, untouched since 2026-05-05) and SECURITY.md stops describing
    the v3 cutover as a future event; .codex/** and uv.lock join
    paths-ignore, both tracked and both exactly the docs/tooling commits the
    list exists to keep from rebuilding the image.

    The gitleaks.yml checkout pin was left to Dependabot rather than
    hand-edited, and PR #20 has since merged it — actions/checkout is now
    @v7 across all four uses, and gitleaks-action is 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-mcp and
    portainer-mcp still lag at v6/v2.

  • oc config show survives 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 its try/except with them. Three compounding defects — a non-UTF-8
    core.json escaped as a raw UnicodeDecodeError (the file read sits inside
    the same try as the parse, but only JSONDecodeError was caught, losing
    the filename ConfigLoadError exists 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 use utc_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 while oc memory add and
    oc memory import accepted it — not a drift risk, an existing
    inconsistency. MAX_CONTENT_CHARS now sits beside MemoryItem;
    add_memory and update_memory enforce 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 import deliberately 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.get returns "" for a blank var, "" is not None, and
    Path("") is Path(".") — so OC_DB_PATH= silently relocated the SQLite
    store to the working directory, and a blank OC_DATA_DIR demoted 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 the is_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 in src/
    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
    to none and both compose files pass an empty OC_EMBEDDING_PROVIDER, so
    a stock container is keyword-only. Deliberately held back from its own push
    — pyproject.toml is not in paths-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.jobs merges onto the defaults instead of replacing them.
    A core.json that named one job silently deleted every other — an
    operator halving the backup interval lost db_vacuum,
    db_integrity_check and embedding_backfill with no warning, because
    load_jobs only ever warned about unknown job names, never missing
    ones. The latent half was worse: the entrypoint seeds /config from
    core.json.example with cp -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"
    for git_onboard_resync. Job ordering follows `_DEFAUL...

Read more

v3.0.0-rc8 — 2026-08-17

Pre-release

Choose a tag to compare

@CarlDog CarlDog released this 29 Sep 06:04
  • Bounded pinned prepend (pinned_limit): memory_search caps the
    pinned prepend at the newest 10 pins by default (tunable 0–1000 via
    pinned_limit on MCP/REST/CLI; 0 = none). Observed live right after
    the rc7 deploy: a top_k=2 unscoped 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 with memory_list(pinned_only=true). 589 tests.

Full Changelog: v3.0.0-rc7...v3.0.0-rc8