Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
950ab82
test: make bare pytest collect and skip tests whose optional extras a…
9thLevelSoftware Sep 18, 2026
946c55a
fix: use sys.platform guards so mypy type-checks Windows-only code on…
9thLevelSoftware Sep 18, 2026
c483bc1
ci: pin ruff to the locked version and document the CI gates
9thLevelSoftware Sep 18, 2026
ce49580
fix(deps): refresh locked mcp, cryptography, urllib3 and idna
9thLevelSoftware Sep 18, 2026
1db1d1e
test: sweep every v7 tool and resource for workspace isolation and pa…
9thLevelSoftware Sep 19, 2026
ebe5e48
fix: let migrate-v7 read v6 databases on Linux and macOS
9thLevelSoftware Sep 19, 2026
fef1ba1
fix: make fresh bootstrap and graph builds work on macOS
9thLevelSoftware Sep 19, 2026
b881ca6
fix: keep local edit-bridge sockets under the AF_UNIX path limit
9thLevelSoftware Sep 19, 2026
8b03dfb
fix: take ownership of protected files created by elevated administra…
9thLevelSoftware Sep 19, 2026
a8ac946
test: let the cancelled consolidation observe cancellation before res…
9thLevelSoftware Sep 19, 2026
9a40570
ci: pin ruff and mypy in the dev extra, CI and pre-commit
9thLevelSoftware Sep 19, 2026
ba469eb
test: pin sweep outcomes, add a copied-storage target and tighten lea…
9thLevelSoftware Sep 19, 2026
a0ca83a
fix: take ownership only from BUILTIN\Administrators
9thLevelSoftware Sep 19, 2026
e65b401
fix: never read a migration source as immutable next to a rollback jo…
9thLevelSoftware Sep 19, 2026
44bd8a9
test: require the own-argument code search to return results
9thLevelSoftware Sep 19, 2026
ad8f8a0
test: name the recording connect parameter so uri=True binds on POSIX
9thLevelSoftware Sep 19, 2026
57b2e3e
test: pin a supported Python for the models-local readiness tests
9thLevelSoftware Sep 19, 2026
76e4641
test: wait for the post-lock lease renewal instead of a fixed sleep
9thLevelSoftware Sep 19, 2026
f7f024a
test: poll for the dreaming graph refresh to finish instead of sleeping
9thLevelSoftware Sep 19, 2026
220f98a
fix: make the Claude Code hooks remind instead of block
9thLevelSoftware Sep 19, 2026
a9e7d7a
docs: describe the six remind-only Claude Code hooks and pairing
9thLevelSoftware Sep 19, 2026
358256f
fix: report only SQLite integrity errors as event stream conflicts
9thLevelSoftware Sep 19, 2026
83a1b7b
fix: report transient contention as retryable and log every internal …
9thLevelSoftware Sep 19, 2026
224e7ee
fix: let user text mention paths, with one path predicate for everyth…
9thLevelSoftware Sep 19, 2026
e6a087e
test: prove path-bearing user text round-trips through every tool
9thLevelSoftware Sep 19, 2026
9ef43d1
test: pin every read against its pre-path baseline
9thLevelSoftware Sep 19, 2026
ce94143
fix: keep contended reads and non-pinned tools retryable (review roun…
9thLevelSoftware Sep 19, 2026
134b724
fix: harden the remind-only hooks and the uninstall cleanup
9thLevelSoftware Sep 19, 2026
27d1682
docs: note OpenCode edit gating is gone and document project detection
9thLevelSoftware Sep 19, 2026
987bd00
fix: stop preflight guidance spawning git, the cause of its timeouts
9thLevelSoftware Sep 19, 2026
6eb091e
feat: enable an optional profile automatically when its extra is inst…
9thLevelSoftware Sep 19, 2026
e63f1e0
fix: make the Windows launcher serve the caller's project
9thLevelSoftware Sep 19, 2026
f2e6a5d
fix: install the OpenCode slash-command files
9thLevelSoftware Sep 19, 2026
0a204d3
fix: address PR 5 review round 1
9thLevelSoftware Sep 19, 2026
88e66ff
test: mark the sweep note's file name so extracted entities carry it
9thLevelSoftware Sep 19, 2026
5e731b1
test: disable installed profiles explicitly where a test needs them off
9thLevelSoftware Sep 19, 2026
46008ec
test: import the launcher before snapshotting sys.modules
9thLevelSoftware Sep 19, 2026
556c1b4
fix: garbage-collect superseded projection generations
9thLevelSoftware Sep 19, 2026
10bf9d6
fix: report entity recall on a lagging lexical index as retryable
9thLevelSoftware Sep 19, 2026
797685e
test: legacy recall keeps context that mentions a path
9thLevelSoftware Sep 19, 2026
d0773dd
test: expect collected lexical generations after reactivation
9thLevelSoftware Sep 19, 2026
2c6ac4d
fix: report a busy database during transfer as retryable, not as a ba…
9thLevelSoftware Sep 20, 2026
ff9dcf5
fix: narrow the retryable recall race and harden generation GC
9thLevelSoftware Sep 20, 2026
a68b205
fix: honour the profile switch in health, the sandbox, and the launcher
9thLevelSoftware Sep 20, 2026
7df3bc2
fix: keep the OpenCode installer inside the project, and cover the ne…
9thLevelSoftware Sep 20, 2026
0543b14
merge: take PR 3's retryable-contention mapping under PR 9
9thLevelSoftware Sep 20, 2026
5c72fc3
test: show the server's own log when a process test fails
9thLevelSoftware Sep 20, 2026
4a50c04
test: dump the http server log after its file is closed
9thLevelSoftware Sep 20, 2026
1783c07
fix: brief a migrated workspace instead of refusing its table snapshots
9thLevelSoftware Sep 20, 2026
0d60ca7
fix: read a migrated v6 record instead of calling it corrupt
9thLevelSoftware Sep 20, 2026
fc5f57f
fix: migrate v6 rows into values v7 can read, and say what changed
9thLevelSoftware Sep 20, 2026
02fa2b0
fix: stop legacy commands upgrading the database migrate-v7 rolls bac…
9thLevelSoftware Sep 20, 2026
06bdd1e
feat: tell an un-migrated workspace what to run
9thLevelSoftware Sep 20, 2026
15601c4
test: prove a migrated v6 workspace is usable end to end
9thLevelSoftware Sep 20, 2026
bbf7341
docs: say that the v6-to-v7 format migration is an explicit command
9thLevelSoftware Sep 20, 2026
0d9b004
refactor: delete the drive regex RelativePath no longer uses
9thLevelSoftware Sep 20, 2026
53c8f2c
docs: state the dry-run guarantee as PR 1 left it
9thLevelSoftware Sep 20, 2026
2dc3d23
Merge remote-tracking branch 'origin/v7/bf-04-garbage-collect-project…
9thLevelSoftware Sep 20, 2026
4947268
test: wait on the fact, not on a duration, in two timing-sensitive tests
9thLevelSoftware Sep 20, 2026
2c89d1b
Merge remote-tracking branch 'origin/v7/bf-09-auto-enable-installed-p…
9thLevelSoftware Sep 20, 2026
db47846
fix: round 1 review — provenance, host paths, and the writes rollback…
9thLevelSoftware Sep 20, 2026
e288e23
test: pin the repairs the review found untested, and clip empty content
9thLevelSoftware Sep 20, 2026
3c4ee7c
docs: say which tools answer MIGRATION_REQUIRED, and what an export c…
9thLevelSoftware Sep 20, 2026
0bc432b
fix: keep the preflight warning sequence typed after the PR 9 merge
9thLevelSoftware Sep 20, 2026
e227d80
fix: do not test an unhashable pair member for membership
9thLevelSoftware Sep 20, 2026
5891e01
fix: round 2 — drop the unreachable resource branch, and stop racing …
9thLevelSoftware Sep 20, 2026
d8842de
PR 2/12: Make the Claude Code hooks remind instead of block (#64)
9thLevelSoftware Sep 20, 2026
3ee4628
PR 4/12: Garbage-collect projection generations (and retryable entity…
9thLevelSoftware Sep 20, 2026
789ceac
PR 9/12: Auto-enable installed profiles, fix the Windows launcher and…
9thLevelSoftware Sep 20, 2026
b003c03
PR 5/12: Exempt user text from the path guard, with one predicate for…
9thLevelSoftware Sep 20, 2026
a246e06
merge: resolve conflicts with path-guard base
cursoragent Sep 20, 2026
edfaa57
fix: pass an empty warning sequence for tokenless preflight
cursoragent Sep 20, 2026
cd5842f
PR 3/12: Report transient failures as retryable and log every interna…
9thLevelSoftware Sep 20, 2026
f376ff3
test: quiesce the brief's drains before the preflight window
9thLevelSoftware Sep 20, 2026
a2297f1
merge: bring PR 3, 4 and 5 from v7/bf-01 into working_branch
claude Oct 3, 2026
deab88e
merge: record v7/bf-02, bf-03 and bf-05 as merged
claude Oct 3, 2026
1a45237
merge: bring PR 6 and PR 9 from v7/bf-06 into working_branch
claude Oct 3, 2026
f949884
merge: record v7/bf-04 as merged
claude Oct 3, 2026
cf5bba5
fix: let a finalize retried after DATABASE_IN_USE release its dead lease
claude Oct 3, 2026
42f929c
test: retry DATABASE_IN_USE in the path-policy import round trip
claude Oct 3, 2026
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
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,46 @@
- OpenCode edits are no longer gated either: the OpenCode plugin runs the same `pre_edit` hook, which now always allows the edit. The plugin does not yet show the reminder; that rework comes with the edit-bridge removal.
- `install-claude-hooks` and `uninstall-claude-hooks` refuse to touch a `~/.claude/settings.json` that is not valid JSON, instead of replacing it.
- The manual `hooks/daem0n_*.py` scripts are deprecated stubs that print a notice and exit 0, and `hooks/settings.json.example` is removed. If you installed them by hand, re-run `python -m daem0nmcp.cli install-claude-hooks`; it also removes the old entries.
- **Behaviour change — installed profiles now turn themselves on.** An unset (or
empty) `DAEM0NMCP_<PROFILE>_ENABLED` means "auto": the profile is ready when
its extra is installed and disabled otherwise. `false` still turns it off, and
`true` with packages missing is still degraded. An empty value used to mean
`false`. What this costs if you installed an extra and never set the flag:
`[graph]` imports networkx, igraph and leidenalg at startup, adds a graph
provider to recall (so rankings change) and lets dreaming refresh communities;
`[local]`/`[models-local]` import sentence-transformers and onnxruntime at
startup, download the embedding model on first use, and rebuild the dense
projection, which re-embeds the whole workspace once. Opt out per profile with
`DAEM0NMCP_<PROFILE>_ENABLED=false`.
- `sandbox_execute_python` now honours `DAEM0NMCP_AGENCY_E2B_ENABLED=false`; it
previously required only `E2B_API_KEY`.
- `system_health` and `meta.capability_states` report the profile's real
remediation (install command or environment variable) instead of "Review the
&lt;name&gt; capability profile."
- `start_daem0nmcp_server.bat` serves the directory you run it from (or its first
argument, or an exported `DAEM0NMCP_PROJECT_ROOT`) instead of the Daem0n-MCP
checkout, and `install-opencode` installs the four `.opencode/commands/` files.
- **Migration payload change — re-migrate a store migrated by an earlier
v7 development build.** `migrate-v7` no longer copies a v6 host path into the
event it writes: the columns that hold one (`file_path`, `project_path`,
`database_path`, and a `file_path_relative` that is not workspace-relative)
are recorded as a `{"$host_path": {...}}` digest, so an export bundle from a
migrated workspace no longer discloses the origin host's directory layout.
The raw value stays in the retained v6 tables and in
`.daem0nmcp/storage/migrations/v7/<id>/source.snapshot.db`. Because the
digest changes the lossless payload, a store migrated before this build fails
`verify-v7` with "retained legacy source provenance differs" and must be
migrated again from its intact v6 source (`migrate-v7 --rollback`, then
`migrate-v7 --apply`). v7 is unreleased, so this affects development stores
only.
- **Migrated v6 workspaces are usable end to end.** `session_brief`, recall,
outcomes, the four resources and `workspace_export` all work on a migrated
store; `migrate-v7` reports in `warnings` whatever it could not copy
faithfully; an un-migrated v6 workspace answers the new non-retryable
`MIGRATION_REQUIRED` naming the command instead of a retryable error with no
remedy; and `migrate-v7 --rollback` refuses with `ROLLBACK_WOULD_HIDE_WRITES`
rather than silently stranding writes made since activation (pass
`--discard-v7-writes` to accept that).

## [6.0.0] - 2026-01-29

Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1037,9 +1037,23 @@ python -m daem0nmcp.cli install-opencode --force

This regenerates `opencode.json` and the plugin to match the latest version.

### 5. Migrations Run Automatically
### 5. Migrations

Database schema migrations are applied automatically when any MCP tool runs. No manual migration step required.
Within a storage format, schema migrations are applied automatically when any
MCP tool runs, and no manual step is required.

Moving an existing v6 workspace to the v7 storage format is **not** automatic.
It is an offline, reversible command, and a v6 workspace answers
`MIGRATION_REQUIRED` until it has run:

```bash
# Stop the server first, then:
python -m daem0nmcp.cli --project-path /path/to/project migrate-v7 --apply

# Undo it; refuses if events were recorded since activation unless
# --discard-v7-writes is passed.
python -m daem0nmcp.cli --project-path /path/to/project migrate-v7 --rollback
```

### 6. Index Your Codebase

Expand Down
48 changes: 37 additions & 11 deletions daem0nmcp/api/v7/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from ...covenant import CovenantGate, InvocationScope
from ...workspace import Workspace
from .errors import ErrorCode
from .errors import DATABASE_IN_USE_RETRY_AFTER_MS, ErrorCode
from .models import CapabilityState
from .policy import V7_TOOL_LEVELS
from .responses import ResponseFactory
Expand Down Expand Up @@ -119,18 +119,26 @@ def _remedy(
if code in {"COUNSEL_REQUIRED", "TOKEN_MISSING"} and isinstance(
workspace_id, str
):
target_arguments = {
key: value
for key, value in effective.items()
if key not in {"workspace_id", "preflight_token"}
}
return "memory_preflight", {
"workspace_id": workspace_id,
"target_tool": tool_name,
"target_arguments": target_arguments,
}
return self._preflight_remedy(workspace_id, tool_name, effective)
return None, {}

@staticmethod
def _preflight_remedy(
workspace_id: str,
tool_name: str,
effective: Mapping[str, Any],
) -> tuple[str, dict[str, Any]]:
target_arguments = {
key: value
for key, value in effective.items()
if key not in {"workspace_id", "preflight_token"}
}
return "memory_preflight", {
"workspace_id": workspace_id,
"target_tool": tool_name,
"target_arguments": target_arguments,
}

def handler(self, tool_name: str) -> Callable[..., Any]:
try:
input_model = TOOL_INPUT_MODELS[tool_name]
Expand Down Expand Up @@ -294,6 +302,24 @@ async def invoke(**arguments: Any) -> object:
states = ()
if any(not isinstance(state, CapabilityState) for state in states):
states = ()
if error_code == ErrorCode.DATABASE_IN_USE.value:
# The gate already spent any preflight token, so a
# retry needs a fresh one.
workspace_id = effective.get("workspace_id")
remedy_tool, remedy_arguments = (
self._preflight_remedy(workspace_id, tool_name, effective)
if isinstance(token, str) and isinstance(workspace_id, str)
else (None, {})
)
return response.failure(
ErrorCode.DATABASE_IN_USE,
"The workspace database is currently in use.",
retryable=True,
retry_after_ms=DATABASE_IN_USE_RETRY_AFTER_MS,
remedy_tool=remedy_tool,
remedy_arguments=remedy_arguments,
capability_states=tuple(states),
)
return response.failure(
ErrorCode(error_code),
"The operation could not be completed.",
Expand Down
18 changes: 6 additions & 12 deletions daem0nmcp/api/v7/code_entity_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
from .models import (
EvidenceRef,
RecordSummary,
contains_absolute_filesystem_path,
stored_relative_path,
)
from .public_ids import (
PublicObjectIdNotFound,
Expand Down Expand Up @@ -79,7 +79,6 @@
_MAX_ENTITY_EVENTS = 200
_MAX_IMPACT_ENTITIES = 500
_MAX_IMPACT_EDGES = 5_000
_CONTROL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
_EVENT_TYPE_RE = re.compile(r"^[a-z][a-z0-9_.-]{2,79}$")
_EVENT_COLUMNS = (
"event_id,workspace_id,stream_id,stream_kind,stream_version,event_type,"
Expand Down Expand Up @@ -366,7 +365,6 @@ def _validated_record_row(
or row["stream_version"] != expected_version
or row["state_hash"] != memory_state_hash(state)
or row["content_hash"] != memory_content_hash(state)
or row["file_path"] is not None
):
raise CodeEntityOperationError("CAPABILITY_DEGRADED")
return row
Expand All @@ -382,17 +380,17 @@ def _event_record_summary(
content = state["content"]
if not isinstance(content, str) or not content:
raise CodeEntityOperationError("CAPABILITY_DEGRADED")
# A record's valid time may precede or follow the transaction time it was
# written at; ``RecordSummary`` shows the earlier of the two.
created = _datetime_from_us(occurred_at_us)
updated = _datetime_from_us(recorded_at_us)
if created > updated:
raise CodeEntityOperationError("CAPABILITY_DEGRADED")
try:
return RecordSummary(
record_id=record_id,
record_type=state["record_type"],
excerpt=content[:4000],
tags=state["tags"],
relative_file_path=state["file_path_relative"],
relative_file_path=stored_relative_path(state["file_path_relative"]),
current_status=(
"invalidated"
if state["deleted_at_us"] is not None
Expand Down Expand Up @@ -935,12 +933,8 @@ def _entity_row(

def _safe_event_content(state: Mapping[str, Any]) -> str:
content = state.get("content")
if (
not isinstance(content, str)
or not content
or _CONTROL_RE.search(content) is not None
or contains_absolute_filesystem_path(content)
):
# Stored user text reads back as stored (UD-3).
if not isinstance(content, str) or not content:
raise CodeEntityOperationError("CAPABILITY_DEGRADED")
return content

Expand Down
106 changes: 80 additions & 26 deletions daem0nmcp/api/v7/discovery_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import inspect
import json
import os
import posixpath
import re
import secrets
import sqlite3
Expand Down Expand Up @@ -51,7 +50,13 @@
)
from .application import AdmittedRequest
from .errors import STABLE_ERROR_CODE_SET
from .models import CapabilityState, Page, RecordSummary, RetrievalData
from .models import (
CapabilityState,
Page,
RecordSummary,
RetrievalData,
stored_relative_path,
)
from .public_ids import (
PublicObjectIdNotFound,
PublicObjectIdRepository,
Expand Down Expand Up @@ -755,24 +760,6 @@ def _partition_metadata(
return row_count, content_hash


def _safe_code_path(value: object) -> bool:
if (
not isinstance(value, str)
or not 1 <= len(value) <= 1024
or "\\" in value
or "\x00" in value
or value.startswith(("/", "~"))
or re.match(r"^[A-Za-z]:", value) is not None
or value in {".", ".."}
):
return False
components = value.split("/")
return (
all(component not in {"", ".", ".."} for component in components)
and posixpath.normpath(value) == value
)


def _validate_entity_partition(
connection: sqlite3.Connection,
workspace_id: str,
Expand Down Expand Up @@ -1061,24 +1048,24 @@ def _record_summary(row: sqlite3.Row) -> RecordSummary:
try:
content = row["content"]
tags = json.loads(str(row["tags_json"]))
# A migrated v6 row keeps the host-absolute ``file_path`` v6 wrote and
# may carry a valid time ahead of its transaction time; neither is
# corruption, and ``RecordSummary`` bounds what is emitted.
if (
not isinstance(content, str)
or not content
or not isinstance(tags, list)
or row["file_path"] is not None
or row["deleted_at_us"] is not None
):
raise ValueError
created = _datetime_from_us(row["created_at_us"])
updated = _datetime_from_us(row["updated_at_us"])
if updated < created:
raise ValueError
return RecordSummary(
record_id=row["record_id"],
record_type=row["record_type"],
excerpt=content[:4000],
tags=tags,
relative_file_path=row["file_path_relative"],
relative_file_path=stored_relative_path(row["file_path_relative"]),
current_status="archived" if bool(row["archived"]) else "current",
content_hash=row["content_hash"],
created_at=created,
Expand Down Expand Up @@ -1424,7 +1411,10 @@ def _code_capability_error(
else "CAPABILITY_DEGRADED"
)
remediation = (
"Set DAEM0NMCP_APPS_ENABLED=true and install the apps dependency profile."
"Set DAEM0NMCP_APPS_ENABLED to true, false, or leave it unset."
if capability_status == "failed"
else "Install the apps profile with pip install 'daem0nmcp[apps]'; "
"it turns on automatically unless DAEM0NMCP_APPS_ENABLED=false."
)
return DiscoveryOperationError(
code,
Expand Down Expand Up @@ -2046,6 +2036,56 @@ def reader(connection: sqlite3.Connection) -> _EntitySelection:
raise _translate_error(error) from None


def _lexical_catchup_pending_sync(
dependencies: DiscoveryOperationDependencies,
workspace: Workspace,
record_ids: tuple[str, ...],
) -> bool:
"""Return whether the lexical projection still owes these records.

True only when the records are absent from the active lexical generation
*and* a rebuild job is queued or running, i.e. a retry can actually make
progress. A dead-lettered rebuild, a permanently unavailable index, or a
record that simply does not match the entity name is not retryable.
"""

if not record_ids:
return False

def reader(connection: sqlite3.Connection) -> bool:
active = connection.execute(
"SELECT generation FROM projection_manifests WHERE workspace_id=? "
"AND projection_name='lexical' AND status='active'",
(workspace.workspace_id,),
).fetchone()
indexed = 0
if active is not None:
placeholders = ",".join("?" for _ in record_ids)
indexed = int(
connection.execute(
"SELECT COUNT(*) FROM retrieval_documents WHERE workspace_id=? "
f"AND projection_generation=? AND record_id IN ({placeholders})",
(workspace.workspace_id, int(active[0]), *record_ids),
).fetchone()[0]
)
if indexed >= len(record_ids):
return False
return (
connection.execute(
"SELECT 1 FROM background_jobs WHERE workspace_id=? "
"AND job_type='retrieval.projection_rebuild' "
"AND status IN ('queued','running') LIMIT 1",
(workspace.workspace_id,),
).fetchone()
is not None
)

try:
return bool(_read_snapshot(dependencies, workspace, reader))
except Exception:
return False


def _generation_is_current_sync(
dependencies: DiscoveryOperationDependencies,
workspace: Workspace,
Expand Down Expand Up @@ -2093,7 +2133,21 @@ async def _memory_recall_entity(
raise DiscoveryOperationError("CAPABILITY_DEGRADED")
indexed[record_id] = item.record
if set(indexed) != set(selection.record_ids):
raise DiscoveryOperationError("CAPABILITY_DEGRADED")
missing = tuple(
record_id
for record_id in selection.record_ids
if record_id not in indexed
)
# Only a projection that has genuinely not caught up yet is worth
# retrying. A permanently unavailable or dead-lettered lexical
# index stays a terminal CAPABILITY_DEGRADED.
pending = await _run_blocking(
dependencies,
lambda: _lexical_catchup_pending_sync(dependencies, workspace, missing),
)
raise DiscoveryOperationError(
"DATABASE_IN_USE" if pending else "CAPABILITY_DEGRADED"
)
await _run_blocking(
dependencies,
lambda: _generation_is_current_sync(
Expand Down
Loading
Loading