Skip to content

merge: land PRs 3, 4, 5, 6 and 9 of the v7 audit stack in working_branch - #69

Merged
9thLevelSoftware merged 80 commits into
working_branchfrom
devil/modest-mendel-bmtv20
Oct 3, 2026
Merged

9thLevelSoftware merged 80 commits into
working_branchfrom
devil/modest-mendel-bmtv20

Conversation

@9thLevelSoftware

@9thLevelSoftware 9thLevelSoftware commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Lands the parts of the v7 audit stack that never reached working_branch: PR 3, 4, 5, 6 and 9. It also fixes two lock-contention races that CI exposed in that code. Afterwards, every v7/bf-* branch tip is an ancestor of working_branch, so the stack branches can be deleted without losing any commits.

Why they were missing

The stacked PRs were squash-merged into each other in an order that left most of the stack on intermediate branches:

Work Where it landed In working_branch?
PR 1 (#63) + PR 2 (#64) working_branch via #63 (squashed when bf-01 sat at PR 2's squash d8842de) yes
PR 5 (#67), PR 3 (#65, carrying PR 4 via #66) v7/bf-01, after #63 had already been squashed no
PR 9 (#68) v7/bf-04, in the same second that #66 squashed bf-04 at ff9dcf5 (pre-PR 9) into bf-03 no (not in bf-01 either)
PR 6 (v7/bf-06) never had a PR; it carries PR 9 through its merge of bf-09 no

Commits

  1. a2297f1 merge v7/bf-01 (PR 3, 4, 5). working_branch's tree equals d8842de, an ancestor of bf-01's tip, so the result is bf-01's tree byte-for-byte. Its one textual conflict (the isolation sweep test) is PR 5's change against PR 1's squashed copy of the same lines.
  2. deab88e record v7/bf-02, bf-03, bf-05 as merged (-s ours, tree unchanged). Their content is already in through the squash commits: bf-02's tree equals d8842de, bf-03's tree equals cd5842f, and bf-05's patch is identical to b003c03's (same git patch-id). Linking them also gives the next merge a closer merge base.
  3. 1a45237 merge v7/bf-06 (PR 6 + PR 9). Four conflicts, all taken from bf-06's side:
    • api/v7/pinned.py: both sides made tokenless-preflight warnings an empty tuple (edfaa57 on bf-03, 0bc432b on bf-06). bf-06's form drops redundant parentheses.
    • retrieval/lexical_config.py, verification_v7.py: bf-06 exports RETRIEVAL_PROJECTION_NAMES and builds _SUPPORTED_PROJECTIONS from it. The set is the same 12 names.
    • tests/api_v7/test_production.py: bf-06's copy of the preflight no-subprocess test also waits for the brief's projection drains (f376ff3).
    • CHANGELOG.md: folded the duplicate ### Changed heading the merge produced.
  4. f949884 record v7/bf-04 as merged (-s ours, tree unchanged). bf-04's tip is PR 9/12: Auto-enable installed profiles, fix the Windows launcher and OpenCode installer #68's squash, and its tree equals bf-09's tip, which step 3 brought in.

Fixes for two races CI exposed on this PR

The first CI run failed two legs, each in a different test. Both failures came from the same cause: a concurrent writer held SQLite's write lock past the 5 s busy timeout, so an import finalize answered the retryable DATABASE_IN_USE. The code involved is identical on bf-06's tip, whose CI happened not to hit the race. PR 9 is what lands it here.

  1. cf5bba5 fix: let a finalize retried after DATABASE_IN_USE release its dead lease (ubuntu-latest / 3.12, test_process_portability). The failed attempt's cleanup release_import_finalization hit the same lock. Its error was suppressed, so the attempt's lease stayed for its full 15 minutes, and every retry the error invites hit claim_import_finalization's live-lease branch: a non-retryable TASK_REQUIRED. A lease whose release fails is now kept in-process and released before the session's next claim. A still-held lock fails that release as DATABASE_IN_USE again, so the client keeps retrying. A live concurrent finalizer still gets TASK_REQUIRED, as test_concurrent_finalizer_is_lease_blocked_and_cannot_delete_winner pins. New regression test: test_retry_after_a_contended_finalize_releases_its_abandoned_lease. It fails with TASK_REQUIRED without the fix.
  2. 42f929c test: retry DATABASE_IN_USE in the path-policy import round trip (windows-latest / 3.10, test_path_policy). The server answered correctly with the retryable code, but PR 5's round trip asserted success on the first call. It now retries within a bounded deadline, as PR 9's portability test does.

How the result was checked

  • bf-06's tip plus PR 2's patch (f7f024a..27d1682, applied with git apply --3way) reproduces this head's tree exactly. The one exception is the CHANGELOG heading fold.
  • Against bf-01's tip, every one of the 56 files that differ traces back to a PR 6 or PR 9 commit.
  • Both inputs were green in CI on their own: bf-01 cd5842f (run 35530809596) and bf-06 f376ff3 (run 35619920017).
  • main and every v7/bf-* tip are ancestors of this head (git merge-base --is-ancestor).

Tests run

Linux, Python 3.11, pip install -e ".[dev,apps,graph]" (ruff 0.16.8, mypy 2.3.1):

  • ruff check daem0nmcp/ tests/: all checks passed
  • ruff format --check daem0nmcp/ tests/: 475 files already formatted
  • mypy daem0nmcp/api/v7 --ignore-missing-imports --follow-imports=silent: no issues in 46 files (also clean with --platform win32 and --platform darwin)
  • python scripts/v7_release_inventory.py --check: valid (75 tools, 10 resources, 572 atomic requirements)
  • Targeted runs on the PR 2 × PR 6/9 interaction surface (Claude hooks, CLI, migrate-v7 CLI, edit bridge/host, OpenCode installer, test_docs_hooks): 166 passed, 34 subtests passed
  • Targeted runs on the hand-resolved files (api_v7/test_production.py, test_verify_v7.py, api_v7/test_pinned_handlers.py, lexical/projection-growth tests): 138 passed, 24 subtests passed
  • Full pytest tests/ --asyncio-mode=auto on the merge head f949884: 2913 passed, 120 skipped, 0 failed (1390 subtests passed) in 14m30s. The skips are the expected ones: optional extras not installed (tiktoken, models-local, e2b, numpy, rank_bm25), external services not configured (Valkey, E2B_API_KEY), and Windows-only tests.
  • Full suite after the fixes (42f929c): 2914 passed, 120 skipped, 0 failed (1390 subtests passed) in 13m53s. The one extra pass is the new regression test, which fails with TASK_REQUIRED before cf5bba5 and passes after. test_portable_projections, test_process_portability and api_v7/test_operations: 54 passed. test_path_policy: 20 passed. ruff, format, mypy and the inventory are clean.

Config and migration notes

These ride in with PR 6 and PR 9; the details are in CHANGELOG.md:

  • Installed profiles turn themselves on (PR 9). An unset or empty DAEM0NMCP_<PROFILE>_ENABLED now means "auto". To opt out, set =false.
  • Re-migrate development stores (PR 6). A store migrated by an earlier v7 dev build fails verify-v7 until it is migrated again: migrate-v7 --rollback, then migrate-v7 --apply.
  • Rollback can refuse (PR 6). migrate-v7 --rollback refuses with ROLLBACK_WOULD_HIDE_WRITES when events were recorded after activation. --discard-v7-writes overrides it.

Known defects carried in, not introduced here

Reviewers flagged three defects on #65 (PR 3) as unfixed, and #65 was merged into bf-01 anyway. They are still present at this head. This PR moves the code unchanged, so they are left for a follow-up rather than mixed into the merge:

  • P1: the durable task dispatcher stores a retryable DATABASE_IN_USE envelope as completed (api/v7/task_dispatcher.py:898). Idempotent resubmits are then served that cached failure (:1309-1312, :1063-1068) until the TTL prunes it.
  • P2: SpecializedProjectionBuildError("DATABASE_IN_USE") (graph, temporal, procedure, outcome) is not a ProjectionBuildError. retrieval/operations.py:77-78 therefore turns it into PROJECTION_REBUILD_FAILED, and the caller gets a non-retryable CAPABILITY_DEGRADED (api/v7/operations.py:979).
  • P2: _lexical_catchup_pending_sync (api/v7/discovery_operations.py:2073-2081) matches any queued or running retrieval.projection_rebuild job, not only lexical ones. A graph or temporal rebuild therefore makes a missing entity retryable.

Merge method

Merge with a merge commit, not a squash. PR 6's 17 commits (1783c07..f376ff3) exist only on v7/bf-06, which never had a PR. A squash followed by deleting the branch would orphan them.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UBLLzyphecQABpqVorHunX

9thLevelSoftware and others added 30 commits September 18, 2026 19:48
…re absent

Add pythonpath = . to pytest.ini so tests/conftest.py can import the
tests package under a bare pytest run, and drop the duplicate
[tool.pytest.ini_options] block from pyproject.toml.

Guard modules that import numpy or rank_bm25 at import time with
module-level pytest.importorskip, and mark only the individual tests
that need tiktoken, numpy, huggingface_hub, the models-local profile or
the E2B SDK, so the rest of those modules keeps running under CI's
[dev,apps,graph] install. Also apply ruff format to
tests/test_retrieval_runtime.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… Linux

mypy narrows sys.platform but not os.name, so the ctypes.WinDLL and
msvcrt.locking calls failed the Linux type-check job. Declare the job
handle closer's type instead of ignoring the attribute error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pin ruff 0.16.8 (uv.lock) in the lint job and the pre-commit hook, and
drop the continue-on-error legacy mypy step. Rewrite the AGENTS.md
contributor commands to the gates ci.yml actually runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mcp 1.26.0 -> 1.30.0, cryptography 46.0.3 -> 50.0.1, urllib3 2.6.1 ->
2.8.0 and idna 3.11 -> 3.20 clear the published advisories against the
locked versions. httpx is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…th leaks

Pass 1 runs the production server in-process as the loopback principal
with two registered workspaces. It seeds A with canary text, records, an
outcome, a relationship, a rule, a trigger, active context, cursors and
selection tokens, then calls every manifest tool and workspace resource
against B with A's IDs, cursors, tokens and idempotency keys. No B
response may contain A's text or IDs, A's ledger tables may not change,
a rejected call may not change B, and no field outside user text may
carry a registered root, a storage path or the home directory.
Consolidation and federated recall are refused without a directional
link, and a B-side link is what makes consolidation from A possible.

Pass 2 relaunches the same workspaces over HTTP with a JWT principal
granted only A and requires UNAUTHORIZED_WORKSPACE for every call that
targets B, including B as a consolidation or federated-recall source.

A fast guard fails when a manifest tool is neither swept nor declared
cross-workspace. The JWT issuer and workspace setup move into
process_client.py so the access test and the sweep share them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
inventory_database opened the source with the unix-none VFS, which has
no shared-memory support, so SQLite could not open any WAL database and
every migrate-v7 dry run and apply failed on POSIX with 'unable to open
database file'. A closed database with no -wal or -shm is now read as
immutable (the main file is the whole database and nothing is created);
a live WAL is read through the default VFS, whose only write is the
volatile -shm wal-index. Windows keeps the win32-none VFS.

The dangling-pointer guard test now matches the redacted error code; it
was skipped wherever symlinks are unavailable, so it never ran before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fresh-workspace bootstrap staged its database under the system temp
directory, which on macOS sits below the /var -> /private/var (or /tmp)
symlink, so the linked-ancestry guard rejected it and every new
workspace reported ACTIVE_V7_UNAVAILABLE. Resolve the temp root first.

The Leiden worker capped itself with setrlimit(RLIMIT_AS), which macOS
rejects because a process already reserves more address space than the
cap, so every graph build ended CAPABILITY_DEGRADED. Skip the cap on
macOS as on Windows; the deadline and request bounds still apply.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The default socket path was ~/.daem0nmcp/edit-bridges/<64 hex>/run/
bridge-<24 hex>.sock, about 140 bytes, beyond the 104-108 byte AF_UNIX
limit, so the local edit bridge could never start on Linux or macOS.
Sockets now live in <root>/run/<16 hex>/b-<24 hex>.sock, and a test
bounds the suffix. Tests that open sockets use a short socket_tmp_path
because the repository-local tmp_path is too deep on CI runners.

A backslash in a POSIX edit path now raises NativeEditNormalizationError
instead of leaking a ValueError; the apply_patch test uses a backslash
separator only on Windows, where OpenCode emits one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tors

An elevated administrator's new files are owned by BUILTIN\Administrators,
so the owner-only ACL check always failed for them (and on the Windows CI
runners). After applying the owner-only DACL, which grants this user
WRITE_OWNER, set the owner to the current user when it differs. The
verification is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…uming

The test cancelled the task and released the blocked worker in the same
step, so a fast worker could record the next archive before the task
saw its cancellation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dev extra now pins ruff 0.16.8 and mypy 2.3.1 (locked), CI installs
the same mypy, and the pre-commit mypy mirror matches it, so following
AGENTS.md reproduces the lint and type-check gates. AGENTS.md also notes
the platforms the locked cryptography ships wheels for. The stdio
duplicate-key docstring no longer names a specific MCP SDK version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…k checks

- Sweep a third registered root C whose storage is a byte copy of A's, so
  only the workspace_id predicates keep A's rows out; dropping the
  predicate from _load_record_row now fails the sweep.
- Give B and C their own src/ file and run own-argument variants of
  code_index, code_search, code_todos_scan, code_refactor_propose and
  workspace_export, so their success responses are scanned.
- Pin every call's outcome, including the cross-workspace refusals; the
  consolidate refusals are rechecked with B's own valid preview token
  after unlinking, so only the missing link can refuse them.
- Scan dictionary keys, forbid the bare canary (except an exact echo of
  the request), and drop the field-name exemption from the host-path
  check: no response carries a host path in any field.
- Require the core seeds to be real, and report gate-only tools and
  placeholder seeds next to the swept list.
- Pass 2 names the tool when a denial unexpectedly succeeds, requires
  unchanged A and B ledgers, and matches resource denials exactly with a
  positive control on A.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ownership is reassigned to the current user only when a new protected
path is owned by BUILTIN\Administrators (an elevated administrator's
default); any other foreign owner is still refused by verification.
Unit tests replay both owners without needing elevation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…urnal

A -journal file beside the source may be a hot journal that SQLite must
see, so it now selects the ordinary mode=ro path like -wal and -shm do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
models-local needs Python 3.11+ (its extra installs nothing on 3.10), so
on 3.10 the registry correctly reports upgrade_python before probing
packages. The missing-extra and ready-probe tests now pin 3.11 like the
python-floor test pins 3.10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The competitor ran a fixed 80 ms after the builder released the writer
lock, so a slow runner could let the 100 ms lease lapse before the
heartbeat renewed it. The test now polls for a lease renewed after the
commit (5 s deadline) and uses a 500 ms lease with a 750 ms lock hold, so
the lock still outlasts the lease as before (F-092).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The graph-generation test read system_health after a fixed 2 s sleep, so a
slow runner could still see community_refresh running. It now polls every
250 ms (30 s deadline) until the refresh is no longer running and has
succeeded, then makes the same assertions (F-092).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pre_edit is now stdlib-only: outside a .daem0nmcp project it exits 0
silently; inside one it adds a one-line additionalContext reminder to call
memory_recall_file and memory_preflight, and never sets permissionDecision.
The stop hook reads cwd, transcript_path and session_id from its stdin
event, unwraps Claude Code's nested transcript records, and shows its
suggestions as a systemMessage; it never emits "decision" or exits 2.
post_edit only imports the edit bridge when the project is paired.

uninstall-claude-hooks with --project-path strips the pairing env keys from
the project's settings.local.json; --remove-credentials also deletes the
local authority credential and socket directories. The manual hooks/
scripts become exit-0 deprecation stubs and settings.json.example is gone.

The bridge integration tests now drive the bridge through a test-only host
driver instead of the deleted handle_pre_edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace the hook sections of the README, Summon section 9 and the v7
operations guide with one table of the installed hooks, the user-level
install location, and what pairing writes. Delete the README manual hooks
block and tell manual-hook users to re-run install-claude-hooks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Any INSERT failure (I/O, lock) used to become a retryable
EVENT_STREAM_CONFLICT. Only sqlite3.IntegrityError is a real conflict;
everything else now propagates to the caller's own mapping (F-066).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…error

- A full writer/resource worker pool or a SQLite "database is locked"/busy
  timeout now surfaces as DATABASE_IN_USE (retryable, retry_after_ms=250)
  instead of a non-retryable INTERNAL_ERROR (F-020, F-027, A-011). This
  covers memory_store, memory_record_outcome, session_brief and
  memory_preflight through one is_database_busy() classifier in errors.py.
- ResponseContext.internal_error logs the cause under the correlation ID the
  client receives (stderr via logging, never the MCP wire; the envelope is
  unchanged). Runtime service errors chain their cause (`from exc`) so the
  log shows the real failure (F-021, F-028).
- memory_preflight with target arguments that do not validate stays ok:true
  but adds a PREFLIGHT_TOKEN_NOT_ISSUED warning naming the failing fields
  (field names and pydantic error types only) (F-070, F-082).
- Tests: 8 parallel preflight+store flows, a held write lock, a logged
  induced failure, a full resource pool, and two stdio server processes
  firing 5 same-key and 10 distinct-key stores (F-055).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ing else

Memory content, rationale, tags, context, queries, rule and trigger text,
entity names and the server text that quotes them are typed UserText and
are exempt from the absolute-path rule in both directions (UD-3, KD-2).
Every other string on every WireModel, input and output, keeps the rule
through the single is_host_absolute_path predicate, so nothing can be
stored and then brick a read.

Delete the drifted output guards (_path_safe_success, both
_contains_raw_path copies, the resource repository's root scan, and the
path checks on content in entity evolution, intelligence summaries, TODO
text, capture records and edit descriptions).  Event payload checks keep
refusing raw path columns and host paths in relative-path keys.
context.visibility is validated at the input model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test_path_policy stores the UD-3 examples, reads back a pre-fix record,
seeds every free-text input field found from the tool models plus
migrated-style memory, rule, trigger, entity and community rows with
paths, then calls every read tool and resource.  The isolation sweep now
stores a note naming each target's root and scans for host paths only
outside UserText fields, located through the response models.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Readers turn a row that fails its output model into CAPABILITY_DEGRADED,
which the first version accepted as a gate.  Compare each read with its
outcome before any path was stored, count CAPABILITY_DEGRADED as a
refusal for seeded calls, and retire the graph rebuild the inserted rows
queue so entity IDs stay current.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d 1)

- resource_repository._run: a read that overran its deadline and then
  finished cleanly is contention, not a fault; report DATABASE_IN_USE instead
  of RESOURCE_REPOSITORY_UNAVAILABLE (-> PREFLIGHT_FAILED -> INTERNAL_ERROR),
  the chain behind the Windows CI failures (R-9, R-10).
- application.py: non-pinned tools now send DATABASE_IN_USE as retryable with
  retry_after_ms=250, like pinned tools (R-1, R-15).
- memory_store and token-bound non-pinned tools: the gate spends the preflight
  token before the write, so DATABASE_IN_USE carries a memory_preflight remedy
  and says to retry with a fresh token and the same idempotency_key (R-4).
- is_database_busy uses sqlite_errorcode (BUSY/LOCKED) when available and
  SQLite's fixed lock messages on 3.10 (R-6); the retry hint is one constant.
- governance_events INSERT: only IntegrityError is a stream conflict (R-2).
- internal_error logs pydantic ValidationError links without input values
  (R-13); token-not-issued docstring states what is echoed (R-3/R-7/R-14).
- Tests: service-level preflight/briefing contention (full pool, overrun read)
  through the production readers; repository overrun maps to DATABASE_IN_USE;
  same-token retry is TOKEN_REPLAYED with the preflight remedy; governance
  conflict rule; log redaction; server log tails in the two-process test's
  assertion messages; retry counts printed, not asserted (R-11, R-12).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- pre_edit and stop find the nearest ancestor of cwd (then of
  CLAUDE_PROJECT_DIR) with .daem0nmcp/, never counting the home directory
  whose ~/.daem0nmcp/ is Daem0n's own state.
- Model-supplied paths are checked lexically (normpath + commonpath; UNC
  and other-drive paths dropped) and never resolved, so a \host\share
  path cannot trigger SMB authentication. The path in the reminder is
  JSON-escaped.
- stop JSON-parses only the transcript tail, keeps the full record count
  for its anti-loop state, prunes state files untouched for a week, and
  words its systemMessage for the user.
- install/uninstall refuse a malformed ~/.claude/settings.json instead of
  replacing it; settings files with a UTF-8 BOM are read.
- uninstall reads everything that can fail before writing, reports partial
  success accurately, only strips DAEM0NMCP_PROJECT_ROOT/STORAGE_PATH from
  a project pairing marked, checks each credential directory for links
  before removing it, and says when no project was given. The CLI defaults
  the project to the current directory and also accepts --project-path
  after the subcommand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Under load, memory_preflight overran its 2 s read and then the 15 s tool
deadline (DEADLINE_EXCEEDED / INTERNAL_ERROR on CI). Stack captures at the
deadline showed every resource worker inside _read_git_changes_sync: each
briefing snapshot runs two git subprocesses (on Windows also a system-wide
Toolhelp thread walk to resume them), and guidance discards git_changes.
read_briefing_snapshot gains include_git_changes; the guidance reader passes
False. session_brief still reads git changes.

Also: a shared storage lock refused because another process holds it
exclusively (bootstrap, migration) is DATABASE_IN_USE, not
ACTIVE_V7_UNAVAILABLE; and ACTIVE_V7_UNAVAILABLE, opaque on the wire, is now
logged with its cause and correlation ID.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9thLevelSoftware and others added 22 commits September 20, 2026 02:40
On POSIX a source with a WAL sidecar is read through the default VFS,
whose volatile ``-shm`` write the comment already describes, so the
opening sentence no longer claims no byte is written anywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ion-gener' into v7/bf-09-auto-enable-installed-profiles-a
Both failed on the slowest Python 3.10 CI legs of run 35495906995 and
neither touches this PR's code.

`test_write_locked_builder_outlasts_heartbeat_busy_timeout` held the
writer lock for a fixed 1.25 s against a 100 ms lease, so its whole
margin was the 100 ms between the heartbeat's final refresh and the
lease check in `_record_success`; a descheduled runner lost it and the
job that had in fact completed was recorded as PROJECTION_JOB_LEASE_LOST.
The builder now holds the lock until the heartbeat has provably been
through its busy timeout more than once (counted on a connection
subclass, so real contention is asserted rather than assumed) and the
lease it was claimed under has expired, both to a bounded deadline, and
the lease is long enough that the terminal update cannot lose a race it
was never about.  Every assertion is unchanged and one is added.

Proved on python:3.10-slim by injecting a 150 ms stall into exactly
that window: the previous test fails 5/5 with the CI's error, the new
one passes 5/5, and the previous test passes without the stall.

`test_production_ritual_and_restart` polled to a deadline for the
record to become visible before the restart but asserted immediately
after it, although a restarted server republishes the lexical
generation in the background.  Both now use one bounded poll.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rofiles-a' into v7/bf-06-make-migrated-v6-workspaces-usab

# Conflicts:
#	daem0nmcp/api/v7/operations.py
#	daem0nmcp/api/v7/pinned.py
#	daem0nmcp/api/v7/resource_repository.py
#	daem0nmcp/api/v7/runtime_services.py
#	daem0nmcp/retrieval/lexical_config.py
#	daem0nmcp/verification_v7.py
… hides

R-11/R-38 (major): `reconcile_retained_governance` read "absent from the
retained v6 rows" as "deleted in v6", and ran on the reactivate path, whose
candidate is the database that was live.  So apply → author a rule or trigger
through v7 → rollback --discard-v7-writes → apply disabled every v7-authored
rule and deleted every v7-authored trigger.  It now reconciles only streams
whose creating governance event was written by the migration itself.

R-32 (major, security): `_lossless_row` copied each v6 column as a
`[name, value]` pair, so the host-absolute `file_path` rode inside every
migrated event's payload and out through `workspace_export` — including from
stores migrated by this branch.  `_reject_raw_paths` only inspected mapping
keys and the wire sweep exempts `payload.data`, so nothing caught it.  Host
path columns, and a derived relative form that is not workspace-relative, are
now written as a `{"$host_path": {sha256,length}}` digest (self-consistent:
`_source_row_hash` is only ever compared against itself, and the original
stays in `source.snapshot.db` and the retained v6 tables).  `_reject_raw_paths`
also refuses the pair-list shape, so a regression fails loudly.

R-2/R-34/R-13/R-17 (major/minor): the rollback guard counted two of the four
tables a v7 client can add user data to.  One named `_POST_ACTIVATION_TABLES`
now covers `workspace_link_events` (federation) and `memory_capture_candidates`
(unapproved user content, plain inserts with no event), and the comparison is
against row counts recorded at activation rather than a wall clock, so a
backward clock step cannot hide the writes this exists to refuse.

R-33/R-36/R-39 (minor): five more readers still refused a migrated row on the
legacy `file_path` — local_state, maintenance, discovery, relationship and
code_entity — and two passed the stored relative path into `RelativePath`
unchecked.  All now behave like the readers the plan named.

R-5 (minor): the four MCP resources answered "outside its workspace" for a v6
store, which is more misleading than what it replaced; they now raise
MIGRATION_REQUIRED like every other entry point.

R-12 (minor): the rollback refusal's count and retained candidate never
reached the operator, and `migrate-v7` never printed its warnings without
--json.  Both do now; other migration details stay redacted to their code.

R-3 (minor): `RecordSummary.tags` required uniqueness while every tag input
allowed duplicates, so an ordinary `memory_store(tags=["a","a"])` wrote a
record no reader could render.  `UniqueTags` fixes the asymmetry at the input.

R-7: a trigger recording no project root is foreign again.  R-9/R-15/R-41:
removals, edits and unreconcilable trigger edits are counted and warned
separately.  R-8/R-16: a row that recorded any link shape and kept none is
counted.  R-14/R-43: one `sole_recorded_project_root`, normalized, for both
file links and triggers.  R-18/R-40: only the three `memory_*` snapshots skip
the live manifest invariants.  R-42: a pointerless store mid-creation is not
capped, so its recovery branch still heals it.  R-35: `_sqlite_backup` opens
the source through `_readonly_connection`.  R-10: `functools.partial` instead
of a parameter two importers never read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-22 (major): the "already-migrated store" test looped over `recall["items"]`,
which was empty, so both reader-side repairs were unproved — neutering
`stored_relative_path` or deleting the bound clipping left it green.  It now
reads the affected record through `memory_search_text` with metadata (so the
assertions run on values the test controls), over each shape v6 could store:
`../outside`, the drive-lettered `resolved.as_posix()` fallback that
`_v6_file_paths` was missing, and a POSIX absolute.  It asserts no link is
emitted, the host path is absent from the whole response, the tags are 32
unique names of ≤80 characters, and the excerpt of an empty v6 content is the
migration marker.  `test_the_bounded_summary_clips_what_v6_had_no_limit_for`
pins the model itself.

That test found a real defect: `RecordSummary` substitutes the marker, but
four readers refused an empty stored content *before* building one, so a
single empty migrated record still failed session_brief for the whole
workspace — the exact failure mode PR 5 escalated.  Fixed in
resource_repository, record_operations (summary and search excerpt) and
runtime_services.

R-21 (major): `verify_v7` is asserted `verified` right after a migration and
again after the first write through the server, with the table-snapshot
manifests asserted present so it cannot pass vacuously.

R-11/R-38: a reactivate test that creates a rule and a trigger through v7,
removes the retained v6 rows their compatibility write made, rolls back with
--discard-v7-writes and re-applies.  Measured, and reported in the summary:
the reviewers' sequence does not reproduce as written, because `rule_create`
and `context_trigger_create` also write the retained v6 row, so the stream is
found and left alone.  The guard is still right, and this pins it.

R-2/R-13/R-34: a federation link event and a pending capture candidate each
make rollback refuse.  R-28: the refusal and the discard warning are asserted
for the count, the candidate and the flag, not for an absent substring.
R-26: MIGRATION_REQUIRED over brief, recall, store, the resource surface and
the transfer boundary (reached by rolling a briefed session's store back to
v6).  R-6: the post-repair `content_hash` move is pinned as a decision.
R-24/R-25: the fixture builds all sixteen v6 ORM tables and its DDL digest is
pinned — it is now byte-identical to the database the v6 code produced, 23
tables and the same sha256.  R-20: a store a legacy command touched verifies
after migrating.  R-29 `entity_match`, R-31 the control-character tag, R-27
the single-value parametrize, R-30 `__import__`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…arries

R-5: "every tool answers MIGRATION_REQUIRED" was an overclaim — store and
recall are blocked behind a briefing that cannot succeed on a v6 store, so
they answer COMMUNION_REQUIRED first.  R-19: state positively that exports
carry no host path and that the lossless v6 row lives in the migrated
database rather than in the bundle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR 9's `_path_safe_success` accepted `list | None`; PR 5 deleted that wrapper,
so the call reaches `ResponseContext.success`, whose `warnings` is a Sequence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pair-shape check R-32 added asked `value[0] in <frozenset>` before
knowing `value[0]` was a string.  Any ordinary two-element list whose first
member is a list or dict — which the legacy-identity import path carries —
raised `TypeError: unhashable type: 'list'`, and the catch-all turned it into
IMPORT_INVALID.  It now requires a string first, which is what a column name
is, and the union is hoisted out of the recursion.

The guard still rejects a `["file_path", …]` or escaping
`["file_path_relative", …]` pair; `test_export_structured_fields_stay_guarded`
now pins both halves, so neither the rejection nor the unhashable shapes can
regress.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the index

**CI (ubuntu 3.11).** `test_a_database_migrated_by_the_pre_fix_code_still_reads`
rewrote `memory_records.content` with direct SQL and then searched for the new
marker, but `memory_search_text` is served by an FTS index built at migration
time from the original content, and nothing forced or awaited a rebuild.  It
no longer depends on re-indexing at all: one warning row now carries every
shape at once — the host-absolute `file_path`, the escaping relative form,
v6's unbounded tags and empty content — and is read back through
`session_brief` and the warnings resource, which select by record type.  That
is also the reader whose failure PR 5 escalated, so the test now exercises the
path the requirement came from.

**MIGRATION_REQUIRED on the resource surface — wontfix in code.**
`production._active_database` raises for format 6 first and that is a
`RuntimeError`, so the branch I added could never match, `format_version == 6`
was unreachable with that resolver, and every reader funnels exceptions
through `_capture` into the deliberate `RESOURCE_UNAVAILABLE` invariant that
stops a caller enumerating workspaces by reading errors.  Surfacing a distinct
code there would punch a hole in that invariant for no gain, so the class, its
branch, the re-raise and the assertion that could not fail are deleted, and
`docs/v7-operations.md` now says the resource surface answers the invariant by
design and that `session_brief` is where the reason is named.

**Also:** the `RecordSummary` docstring no longer claims clipping is a no-op
for a v7-written record — it names the three v6 bounds a v7 write cannot
violate and separates the timeline clamp, which is not a v6-only repair.  The
export assertion now runs over the export pages, including an events page,
rather than only the recall response.  The `<empty>` substitution lives in the
one place that needs it (the model, plus the bounded excerpt the model does
not reach); the three copies in the readers were dead, and what those readers
actually needed was to stop rejecting empty content.  The reactivate test says
why it deletes the retained rows.

**CHANGELOG:** `_lossless_row`'s host-path digest is a payload-format change,
so a store migrated by an earlier development build fails `verify-v7` with
"retained legacy source provenance differs" and must be migrated again from
its intact v6 source.  v7 is unreleased, so no shipped store is affected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**Brownfield run 3403b448, PR 2 of 12** — audit-and-fix stack for the v7 core (#61/#62). Base: `v7/bf-01-make-ci-green-and-pin-workspace`.

## Plan (contract)

### PR 2: Make the Claude Code hooks remind instead of block
- Depends on: PR 1
- Files: daem0nmcp/claude_hooks/pre_edit.py, daem0nmcp/claude_hooks/post_edit.py, daem0nmcp/claude_hooks/stop.py, daem0nmcp/claude_hooks/_client.py, daem0nmcp/claude_hooks/install.py, hooks/daem0n_pre_edit_hook.py, hooks/daem0n_post_edit_hook.py, hooks/daem0n_prompt_hook.py, hooks/daem0n_stop_hook.py, tests/test_claude_hooks_pre_edit.py, tests/test_claude_hooks_stop.py, tests/test_claude_hooks_post_edit.py, tests/test_claude_hooks_install.py, tests/api_v7/test_docs_hooks.py, docs/release/v7/requirements.json, docs/release/v7/inventory.md, README.md, Summon_Daem0n.md, docs/v7-operations.md
- Traces to: FP-4, FP-7, F-009, F-019, F-045, F-050, F-042, F-044, F-084, F-052, F-037, F-038, A-025, A-031, A-036, UD-1
- Risk: low
- Description (KD-1):
  1. **`pre_edit.py` rewrite.** Stdlib + `_client.read_hook_event` only.
     - Take the project from `event["cwd"]`, falling back to `CLAUDE_PROJECT_DIR`.
     - With no `.daem0nmcp/` there, exit 0 silently.
     - Otherwise print `{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"<one-line reminder naming memory_preflight and memory_recall_file with the relative path>"}}` and exit 0.
     - Never set `permissionDecision`.
     - Delete `async_main`, `PreEditResult` and `handle_pre_edit`.
  2. **`post_edit.py`.** Import `edit_host` lazily, only when the bridge credential env is configured.
  3. **`stop.py`** (R-7).
     - Read `transcript_path` and `session_id` from the stdin event, and parse transcript records nested under `message`.
     - Emit the reminder only as a user-visible `systemMessage` JSON field or as plain stdout. Record in the PR which one Claude Code displays.
     - Never emit `"decision"` and never exit 2.
     - It still never writes memory.
  4. **`_client.py`.** Delete `get_tool_input` and `get_file_path_from_input`.
  5. **Uninstall cleanup** (F-038).
     - `uninstall-claude-hooks --project-path` strips the Daem0n env keys it added to `<project>/.claude/settings.local.json`.
     - With `--remove-credentials`, it also deletes `~/.daem0nmcp/edit-bridges/<authority>/`.
     - Both `install-claude-hooks` and `uninstall-claude-hooks` remove any `hooks/daem0n_*` command entries from `~/.claude/settings.json` (R-20; `install.py:45-48` already detects them).
  6. **Legacy scripts** (R-13/R-20). Replace each `hooks/daem0n_*.py` with a stdlib-only stub. The stub prints `Daem0n: this manual hook is deprecated; run python -m daem0nmcp.cli install-claude-hooks` to stderr and exits 0. Delete the README "Manual Hooks (Legacy)" block. The PR description and CHANGELOG note tell manual-hook users to re-run `install-claude-hooks`.
  7. **Doc sections.** Only the hook sections of README §Claude Code Integration, Summon §9 and v7-operations §Claude Code: one table of the 6 hooks (event, what it does, input channel, reminds only), the user-level install location, and what pairing writes.
  8. **Release inventory** (R-2/R-22). Run `python scripts/v7_release_inventory.py --write` and commit the regenerated `requirements.json`/`inventory.md`, because hook entry points changed.
  9. **Tests** (F-052).
     - stdin-driven `main()` subprocess tests for `pre_edit`: non-Daem0n dir → exit 0 and empty stdout; Daem0n project → exit 0 plus the reminder JSON.
     - An import-set test: no `daem0nmcp.api`/`daem0nmcp.retrieval` module loaded.
     - `stop` driven by a nested-`message` JSONL fixture, asserting its output never contains `"decision"`.
     - Each legacy stub fed a PreToolUse payload exits 0.
     - The uninstall test.
     - `test_docs_hooks.py` updated.
- Acceptance:
  - A Claude-style Write event piped into `pre_edit` from a non-Daem0n temp dir exits 0 with empty stdout. From a Daem0n project it exits 0 with the reminder JSON.
  - `pre_edit` median runtime is < 150 ms (was 583 ms).
  - No Daem0n hook and no legacy stub can exit 2 (the tests above).
  - The release-inventory CI job is green.
  - The PR states whether `additionalContext` was confirmed visible to the model in a live session (A-036).

#### Carry-over from PR 1 review (orchestrator, binding)
- PR 1 (b881ca6) moved local edit-bridge sockets to `edit-bridges/run/<authority[:16]>/b-<24hex>.sock`. `uninstall-claude-hooks --remove-credentials` (step 5) must also remove that socket directory.

---

## What landed

# PR 2 summary: Make the Claude Code hooks remind instead of block

(Saved by the orchestrator from the implementer's reply.)

Branch `v7/bf-02-make-the-claude-code-hooks-remin`, base PR 1 tip f7f024a. Worktree `D:\Daem0n-MCP\.claude\worktrees\agent-ab437c58b407838c4`.
- 220f98a `fix: make the Claude Code hooks remind instead of block`
- a9e7d7a `docs: describe the six remind-only Claude Code hooks and pairing` (final)

## Files changed
- `daem0nmcp/claude_hooks/pre_edit.py` (rewritten); `stop.py`, `post_edit.py`, `_client.py`, `install.py`.
- Outside plan file list: `daem0nmcp/cli.py` (`--remove-credentials`, `--project-path` to uninstall), `daem0nmcp/edit_host.py` (extracted `local_bridge_layout()`).
- `hooks/daem0n_{pre_edit,post_edit,prompt,stop}_hook.py` → exit-0 deprecation stubs; `hooks/settings.json.example` deleted.
- Tests: `test_claude_hooks_{pre_edit,stop,post_edit,install,client}.py`, `api_v7/test_docs_hooks.py`; bridge tests `test_edit_host.py`, `test_edit_bridge_transport.py`, `api_v7/test_process_edit_capture.py`; new helper `tests/native_edit_host.py`.
- Docs: `README.md`, `Summon_Daem0n.md`, `docs/v7-operations.md`, `CHANGELOG.md`.

## What and why
1. **pre_edit**: stdlib + `read_hook_event` only; project from `event["cwd"]` then `CLAUDE_PROJECT_DIR`; no `.daem0nmcp/` → exit 0 silent; else prints PreToolUse `additionalContext` reminder (memory_recall_file + memory_preflight) and exits 0; path omitted if outside project; never sets `permissionDecision`; exceptions swallowed → exit 0. Deleted `async_main`, `PreEditResult`, `handle_pre_edit`. Lazy `asyncio` import in `_client.run_async`. Median runtime 87 ms (was 503 ms), 21 subprocess runs on Windows.
2. **post_edit**: `edit_host`/`native_edit` imported lazily; only runs when `DAEM0NMCP_EDIT_BRIDGE_CREDENTIAL_FILE` is set.
3. **stop**: reads `cwd`, `transcript_path`, `session_id` from stdin; state keyed by session_id; unwraps nested `message` records; emits `{"systemMessage": ...}` exit 0; never `decision`, never writes memory. Live `claude -p --output-format stream-json` run showed the systemMessage as an informational system event.
4. **_client**: deleted `get_tool_input`, `get_file_path_from_input`; env parsing folded into `get_command_from_input` (pre_bash needs it until PR 10).
5. **Uninstall**: `cli --project-path P uninstall-claude-hooks [--remove-credentials] [--dry-run]` strips pairing env keys from `<project>/.claude/settings.local.json` (credential file, runtime dir, mode, workspace-binding file, `DAEM0NMCP_PROJECT_ROOT`, `DAEM0NMCP_STORAGE_PATH`), keeping others; `--remove-credentials` deletes `edit-bridges/<authority>/` and `edit-bridges/run/<authority[:16]>/` (PR 1 carry-over). Legacy `hooks/daem0n_*` entries removed via `_is_daem0n_entry` (test added).
6. **Legacy scripts**: import only `sys`, print deprecation to stderr, exit 0. README "Manual Hooks (Legacy)" block deleted; CHANGELOG tells manual-hook users to re-run `install-claude-hooks`.
7. **Docs**: README, Summon §9, v7-operations share one six-hook table (event, behaviour, input channel, reminds-only, install location, what pairing writes); states pre_bash is inert (reads unset `TOOL_INPUT`) and post_edit hooks are no-ops.
8. **Release inventory**: `--write` produced no diff; `--check` valid (75 tools, 10 resources, 571 requirements).

## Tests and results
- New pre_edit subprocess tests: non-Daem0n dir silent exit 0; Daem0n project exit 0 + reminder JSON with no permissionDecision; CLAUDE_PROJECT_DIR fallback; outside path and garbage stdin exit 0; import set excludes `daem0nmcp.api`, `retrieval`, `edit_host`, `database`, `sqlalchemy`, `pydantic`.
- Stop: `main()` over stdin with nested-message JSONL; asserts no `"decision"`, systemMessage mentions memory_store/memory_record_outcome, no storage written, state file created; silent outside Daem0n project.
- post_edit: unpaired project exits 0 without importing edit_host; paired project with no consumed edit exits 0.
- Install/uninstall: legacy entries removed; project cleanup dry-run, env keys, both credential dirs; credentials kept without the flag.
- `test_docs_hooks.py`: stubs sys-only, never exit(2); each stub fed a PreToolUse payload exits 0 with deprecation on stderr and no workspace mutation.
- Hook + bridge tests: 105 passed (Windows); 86 passed (Docker python:3.12 Linux).
- Full suite (Windows): 2799 passed, 121 skipped, 13 min 08 s.
- ruff check/format clean; mypy api/v7 clean (also clean on claude_hooks and edit_host.py).
- A-036 live check: `claude -p` (Claude Code 2.1.277) with only the new pre_edit hook in a temp dir with `.daem0nmcp/`: the Write went through and the model quoted the additionalContext. Confirmed.

## Deviations
1. Deleting `handle_pre_edit` broke bridge integration tests that used it as the native host → test-only driver `tests/native_edit_host.py::drive_native_edit` (raises instead of swallowing; two test_edit_host "fails before network" tests now assert `pytest.raises(ValueError)` and `calls == 0`).
2. `edit_host.py`: extracted `local_bridge_layout()` so uninstall computes the same dirs as provisioning; provisioning unchanged.
3. `cli.py`: `--remove-credentials`, `--project-path` passed through.
4. Deleted `hooks/settings.json.example`.
5. Deleted old pre_edit tests asserting blocking/permissive mode and `test_remote_mode_without_workspace_binding_fails_recoverably` (config error still covered by `EditHostConfig.from_environment`).

## Follow-ups (not fixed)
- Claude-side edit bridge now unreachable (only pre_edit created bridge requests) → post_edit_preflight/post_edit are no-ops in Claude Code (documented). Fits UD-13 follow-up; pairing on install could be dropped then.
- pre_bash still reads `TOOL_INPUT`; `block()` still in `_client` (PR 10). `run_hook_safely` maps exit 2 to 0.
- Uninstall doesn't remove a remote pairing's binding file.
- `daem0nmcp/__init__.py` `importlib.metadata.version()` costs ~36 ms per hook start.
- README historical v6 section still says pre_edit "fails closed" (PR 12 scope).
- `.gitignore` lacks `.venv/`.

## Note for PR description
Manual-hook users: `hooks/daem0n_*.py` now only print a deprecation notice and exit 0. Re-run `python -m daem0nmcp.cli install-claude-hooks`; it also removes the old entries.

## Round 2
134b724 (hooks hardening: find_project_root walk-up stopping at home; lexical relative_project_path rejecting UNC/NUL/outside; json.dumps path; uninstall reads-before-writes with partial-success reporting; utf-8-sig settings reads; install refuses malformed ~/.claude/settings.json; pairing-marker-gated env strip; --project-path after subcommand; CLI cwd default; reject_linked_ancestry before rmtree; stop reads transcript tail, user-facing systemMessage, prunes state >7 days) and 27d1682 (docs: OpenCode edit gating gone; project detection). 26 items: 23 addressed, 3 wontfix (R-14 pre-existing bootstrap flake → PR 8; R-24 UD-13 follow-up list; R-25 plan amendments done by orchestrator). Security reviewer re-verified R-19..R-22 resolved. CI run 35422498587 green on all 9 legs + lint/type-check/release-inventory.
UD-13 follow-up items: stop pairing on install; unregister post_edit/post_edit_preflight and strip them on install; delete tests/native_edit_host.py with the bridge; surface pre_edit stdout as OpenCode reminder; drop failClosed (incl. 5 s timeout block); fix daem0n.ts header/comments (lines 6, 216).

---

## Review

# PR 2 review — merged (round 2: all addressed; security re-verified R-19..R-22; CI run 35422498587 green)

Reviewer slots: general, general-2, tests, security, plan-alignment (effort 5). Item statuses: 23 addressed, 3 wontfix.

CI: https://github.com/9thlevelsoftware/Daem0n-MCP/actions/runs/35422498587 (green)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
… recall) (#66)

**Brownfield run 3403b448, PR 4 of 12** — audit-and-fix stack for the v7 core (#61/#62). Base: `v7/bf-03-report-transient-failures-as-ret`.

## Plan (contract)

### PR 4: Garbage-collect projection generations and take git out of preflight
- Depends on: PR 3
- Files: daem0nmcp/retrieval/projections.py, daem0nmcp/retrieval/specialized_projection.py, daem0nmcp/retrieval/jobs.py, daem0nmcp/retrieval/lexical_config.py, daem0nmcp/verification_v7.py, daem0nmcp/api/v7/production.py, daem0nmcp/api/v7/resource_repository.py, tests/test_retrieval_projection_growth.py, tests/api_v7/test_production.py
- Traces to: FP-6, F-001, F-010, F-026, F-039, F-057, F-060, F-040, A-002, A-009, A-019
- Risk: medium
- Description:
  1. **Generation GC** (KD-5; R-24).
     - Move `_GENERATION_TABLES` from `verification_v7.py` into `retrieval/lexical_config.py`, alongside the FTS table naming for lexical and procedure. Verification imports it.
     - Add `collect_superseded_generations(connection, workspace_id, projection, limit=5)`. It opens its own `BEGIN IMMEDIATE`. It selects manifests with `status IN ('ready','failed')` for that workspace and projection, excluding the active manifest and the most recently deactivated (`MAX(activated_at_us)` among `ready`), oldest first, `LIMIT 5`. For each it runs `DROP TABLE IF EXISTS` on the regex-validated FTS table names, deletes the generation-table rows, and deletes the manifest.
     - Call it *after* the activation commit in `LexicalProjectionBuilder.rebuild` and in the specialized builder activation.
     - Catch lock/busy errors, log them at debug, and return; the next activation retries.
     - Next generation numbers stay `MAX(generation)+1`, so deleted manifests below the maximum are harmless.
  2. **Lock contention** (F-060). In `rebuild`, classify lock contention with `jobs._is_lock_contention` as retryable instead of `LEXICAL_UNAVAILABLE`.
  3. **Preflight** (F-040). Give `production._guidance_reader` a narrow reader in `resource_repository.py` that returns only the active rules and recent decisions/warnings, with no `_read_git_changes_sync`. `session_brief` keeps the git probe.
  4. **Tests.**
     - 50 stores + forced drains leave ≤ 2 generations per projection in `sqlite_master`, ≤ 2 manifests per projection, and `retrieval_documents` ≤ 2 × live records.
     - A DB seeded with 120 stale generations shrinks over successive writes (5 per activation per projection) to ≤ 2.
     - A failed or never-activated generation just below the new one never causes the previously active generation to be dropped.
     - A GC pass while another connection holds a read transaction does not error the write.
     - `memory_preflight` spawns no subprocess.
- Acceptance:
  - The growth test passes in under 10 s on 3 OSes, including POSIX in DELETE journal mode (pre-PR-8 DBs).
  - `verify-v7 --repair-projections` tests stay green.
  - The preflight profile shows no `git` calls.

#### Carry-over from PR 3 (orchestrator, binding)
- "Take git out of preflight" is DONE in PR 3 (987bd00: `read_briefing_snapshot(include_git_changes=...)`, guidance passes False) — it was the root cause of preflight timeouts. Drop that part of this PR; keep only the acceptance check that preflight spawns no git process, if not already covered.
- Pre-existing race (seen on CI ubuntu 3.10 and locally under load): `memory_recall_entity` raises non-retryable CAPABILITY_DEGRADED when the retrieval projection doesn't yet contain every record the entity points to (`discovery_operations.py:2096`). This PR owns projection freshness: make "projection not yet caught up" a retryable outcome (e.g. DATABASE_IN_USE/RETRIEVAL_UNAVAILABLE with retry_after_ms) and make `test_process_discovery_surface` wait/poll for recall to see the records, like `test_process_client` does.
- [REASSIGNED to PR 10 by the orchestrator after PR 4's general review: it shares no code or data path with generation GC and holds nothing up] Recall `bounded_excerpt` repeats short content twice (`retrieval/composer.py:561-566`). Also confirm the "LEXICAL_REBUILD_REQUIRED"/stale marker set on every write (`event_store.py:612`) always clears once the rebuild job runs (GC must not leave it stuck).

---

## What landed

# PR 4 summary: Garbage-collect projection generations (and retryable entity recall)

(Saved by the orchestrator from the implementer's reply.)

Branch `v7/bf-04-garbage-collect-projection-gener`, base 987bd00 (PR 3 tip). Worktree `D:\Daem0n-MCP\.claude\worktrees\agent-aacf6400555e70fbc`.
- 556c1b4 `fix: garbage-collect superseded projection generations`
- 10bf9d6 `fix: report entity recall on a lagging lexical index as retryable`
- d0773dd `test: expect collected lexical generations after reactivation` (final)

## What changed
1. **Generation GC (KD-5, R-24; F-001/F-010/F-026/F-039/F-057)**: `GENERATION_TABLES` moved to `retrieval/lexical_config.py` (verification_v7 imports it). New `retrieval/projections.py::collect_superseded_generations(connection, workspace_id, projection, limit=5)`: no-op inside a caller's transaction, for `graph` (discovery FKs), or invalid workspace id; else own `BEGIN IMMEDIATE` with 250 ms busy timeout (restored after); selects manifests `status IN ('ready','failed')`, keeping the most recently deactivated `ready` (highest `activated_at_us`, ties by generation) — active/building/rebuild_required excluded by the filter; oldest first, ≤5 per pass; per generation: `DROP TABLE IF EXISTS` FTS table (`lexical_fts_table_name`/`procedure_fts_table_name`), delete generation-table rows, delete manifest; busy/locked → debug log, other `sqlite3.Error` → warning; returns 0, never raises. Called after the activation commit in `LexicalProjectionBuilder.rebuild` and `SpecializedProjectionBuilder.rebuild` (procedure, outcome, temporal) only when the builder owns the transaction, never on dry runs or when reusing a current generation.
2. **Lock contention (F-060)**: lexical and specialized rebuilds report SQLite busy/locked (`ProjectionJobRunner._is_lock_contention`) as `DATABASE_IN_USE`; job runner re-queues instead of dead-lettering. `api/v7/operations.py` passes projection DATABASE_IN_USE through (projection_rebuild retryable, not CAPABILITY_DEGRADED).
3. **Preflight git**: done in PR 3; acceptance test added.
4. **Entity recall race**: `_memory_recall_entity` returns retryable DATABASE_IN_USE (250 ms) when entity members are missing and the lexical provider isn't `ready`; stays non-retryable CAPABILITY_DEGRADED when lexical is `ready`. `test_process_discovery_surface` polls recall with a 30 s deadline accepting only ok or retryable.

## Tests
- New `tests/test_retrieval_projection_growth.py` (forces DELETE journal mode, `synchronous=OFF`): 50 spaced writes via real `drain_projection_jobs` → lexical/procedure/outcome/temporal ≤2 manifests, lexical/procedure ≤2 FTS tables matching manifests, `retrieval_documents` ≤ 2× live records; 120-generation backlog (GC patched out while seeding) shrinks by exactly 5 per activation to {active−1, active} for lexical and procedure; failed/never-activated generations dropped while previous active survives; GC while another connection holds a read transaction on an old FTS table → rebuild still active, GC returns 0 after 250 ms busy, connection clean, next activation collects; contended lexical job → `queued`, reason DATABASE_IN_USE; GC in caller transaction or for graph → no-op. Reverted-source: 5 tests fail (e.g. 51 lexical manifests left).
- `test_production.py::test_memory_preflight_spawns_no_subprocess` (control: session_brief spawns git; preflight spawns nothing; fails if guidance sets include_git_changes=True).
- `test_discovery_operations.py::test_memory_recall_entity_missing_members_retry_only_while_stale`.
- `test_migrate_v7.py` reactivation expects `[(2,ready),(3,active)]` (gen 1 collected).
- Full suite Windows 3.12: 2825 passed, 121 skipped, 0 failed (1671 s). Linux Docker 3.10: growth file 6 passed in 3.2 s (<10 s acceptance); jobs/specialized/runtime/verify_v7 (incl. --repair-projections)/discovery/production/process_discovery: 149 passed, 3 skipped. Windows related modules: 504 passed. ruff/format clean; CI mypy clean.
- Entity recall race not reproduced in 12+12 parallel runs; classification rests on the carry-over diagnosis and code reading.

## Deviations
1. Outside file list: `api/v7/operations.py` (DATABASE_IN_USE passthrough), `tests/test_migrate_v7.py` (expectation).
2. Specialized builder also got the F-060 fix.
3. GC catches every `sqlite3.Error`.
4. `resource_repository.py`/`production.py` unchanged (PR 3 did the git change).

## Follow-ups (not fixed)
- Graph generations still accumulate (discovery rows reference them).
- `rebuild_required` manifests from verify-v7 repair never collected (by design).
- Existing DBs shrink 5 generations per activation per projection; VACUUM needed to reclaim disk (PR 12 docs).
- Lexical provider reads manifest then FTS table in separate implicit transactions; keeping the previous generation makes it safe for one activation; two activations between reads → "unavailable", not a crash.
- `_generation_is_current_sync` in recall_entity still non-retryable CAPABILITY_DEGRADED if a graph rebuild lands mid-request.
- Carry-over not addressed (added to the plan after launch): recall `bounded_excerpt` repeats short content (`retrieval/composer.py:561-566`); confirm stale/LEXICAL_REBUILD_REQUIRED marker always clears.

## Round 2 (ff9dcf5)
21 addressed, 3 wontfix (bounded_excerpt trio reassigned to PR 10), 1 wontfix (graph GC — discovery FKs; tracked follow-up).
- Entity recall: `_lexical_catchup_pending_sync` — retryable only when the missing record_ids are absent from the ACTIVE lexical generation's `retrieval_documents` AND a `retrieval.projection_rebuild` job is queued/running; dead-lettered rebuild, missing FTS5, config/table mismatch, missing diagnostic, or indexed-but-unmatched → terminal CAPABILITY_DEGRADED. Kept DATABASE_IN_USE (RETRIEVAL_UNAVAILABLE isn't in the wire registry).
- Growth test: `include_optional=True`, 20 writes, asserts all four projections rebuilt each drain (5.0 s Windows / 2.07 s Linux); asserts no active manifest keeps `rebuild_required_*`.
- One-shot compaction: `retrieval.operations.compact_projections` + CLI `python -m daem0nmcp.cli --project-path <root> compact-projections --workspace-id ws_…` (`--no-vacuum` to skip VACUUM). Takes the exclusive storage lock (server must be stopped); loops GC per projection until 0 then VACUUMs; reports collected and before/after superseded counts. Release inventory regenerated. **PR 12 must document this as the step BEFORE VACUUM.**
- Keep-rule by `ORDER BY generation DESC` (no clock); `rebuild_required` generations below active now collected (COALESCE guard); per-victim SAVEPOINT so one undroppable victim doesn't wedge GC; handlers catch `Exception`; `typeof(generation)='integer'` and delete by manifest_id; `jobs.is_lock_contention` public; single `COLLECTABLE_PROJECTIONS`; verify-v7 emits `checks["projection_generations"]`; generation-row deletion asserted for all three tables; WAL blocked-reader test; process-surface poll asserts DATABASE_IN_USE.
Results: Windows full suite 2830 passed / 121 skipped (2 errors = unrelated disk-I/O flake in test_graph.py under container load; module passes alone). Linux 3.10 container green; ruff/format, CI mypy, release inventory clean. Mutations: GC disabled → 3 tests fail; GENERATION_TABLES shrunk to lexical → growth test fails.

---

## Review

# PR 4 review — merged (round 2: verified by tests, general-2 and plan-alignment; CI run 35490179442 green)

Reviewer slots: general, general-2, tests, security, plan-alignment (effort 5). Item statuses: 20 addressed, 4 wontfix.

CI: https://github.com/9thlevelsoftware/Daem0n-MCP/actions/runs/35490179442 (green)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
… OpenCode installer (#68)

**Brownfield run 3403b448, PR 9 of 12** — audit-and-fix stack for the v7 core (#61/#62). Base: `v7/bf-01-make-ci-green-and-pin-workspace`.

## Plan (contract)

### PR 9: Auto-enable installed profiles and fix the Windows launcher and OpenCode installer
- Depends on: PR 1
- Files: daem0nmcp/capabilities.py, daem0nmcp/api/v7/discovery_operations.py, daem0nmcp/cli.py, start_server.py, start_daem0nmcp_server.bat, daem0nmcp/opencode_install.py, tests/test_capabilities.py, tests/test_start_server.py, tests/test_opencode_install.py
- Traces to: FP-6, FP-7, F-036, F-083, F-032, F-049, F-077, A-016, A-017, UD-7
- Risk: medium
- Description (R-9: runtime behavior split out of the docs PR):
  1. **Profiles** (KD-8, UD-7). `capabilities._configured` returns "auto" when the variable is unset:
     - auto → ready if all distributions are present, disabled otherwise;
     - explicit false → disabled;
     - explicit true with packages missing → degraded.

     The disabled-but-installed remediation says to unset the variable or set it to true. Fix the `index` remediation (`tree-sitter-language-pack`) and `discovery_operations.py:1427`.
  2. **Launcher** (F-032/F-049).
     - `start_server.py` uses `--project`, else an existing `DAEM0NMCP_PROJECT_ROOT`, else cwd, and never overwrites an exported value.
     - `start_daem0nmcp_server.bat` captures `%CD%` before `cd /d "%~dp0"` (or takes `%1`) and passes `--project`.
  3. **OpenCode** (F-077). `opencode_install.py` copies the four repo command files into `.opencode/commands/`, create-if-absent.
  4. **Windows stdio** (A-016).
     - Confirm that the stdio variant of `tests/api_v7/test_process_client.py` runs, not skipped, on the Windows leg.
     - Run the manual Claude Code check on Windows: `claude mcp add daem0nmcp -- python -m daem0nmcp` → `session_brief` → store → recall. Record the Claude Code version and the result in the PR. PR 12 documents it.
  5. **Tests.**
     - `test_capabilities.py` covers all four profile states.
     - A launcher test: an exported `DAEM0NMCP_PROJECT_ROOT` survives, and `--project` wins.
     - The installer creates the command files and does not overwrite existing ones.
- Acceptance:
  - A green 3-OS CI run with `[graph]` (installed by CI) now auto-enabled across the whole suite. Any test that assumed "disabled" is fixed here.
  - `start_daem0nmcp_server.bat` launched from a project directory registers that project.
  - The PR records the A-016 result.

---

## What landed

# PR 9 summary: Auto-enable installed profiles and fix the Windows launcher and OpenCode installer

(Saved by the orchestrator from the implementer's reply.)

Branch `v7/bf-09-auto-enable-installed-profiles-a`, base f7f024a (PR 1 tip). Worktree `D:\Daem0n-MCP\.claude\worktrees\agent-a51f5956496f2f3bf`.
- 6eb091e `feat: enable an optional profile automatically when its extra is installed`
- e63f1e0 `fix: make the Windows launcher serve the caller's project`
- f2e6a5d `fix: install the OpenCode slash-command files`
- 5e731b1 `test: disable installed profiles explicitly where a test needs them off`
- 46008ec `test: import the launcher before snapshotting sys.modules` (final)

## What and why
1. **Profiles (KD-8, UD-7)**, `capabilities.py`: `_configured` returns "auto" when the variable is unset or empty (empty used to mean false). unset + installed → ready; unset + missing → disabled with `install_extra`; `false` → disabled with `enable_configuration` ("Unset X or set it to true…"), packages not probed; `true` + missing → degraded (unchanged); invalid → failed. models-local on <3.11 keeps `upgrade_python` (disabled under auto, degraded when true). All consumers go through `CapabilityRegistry`.
2. **Remediation text**: discovery_operations apps remediation ("pip install 'daem0nmcp[apps]'; turns on automatically unless DAEM0NMCP_APPS_ENABLED=false"); `cli.py index` names `tree-sitter-language-pack` and `daem0nmcp[apps]`.
3. **Launcher (F-032, F-049)**: `start_server.py` uses `--project`, else a non-empty exported `DAEM0NMCP_PROJECT_ROOT`, else cwd; never overwrites an exported value. `.bat` captures `%CD%` or `%~f1` before `cd /d "%~dp0"`, passes `--project "%DAEM0N_PROJECT%\."`; header no longer says HTTP is required; CRLF kept. Live check: real `.bat` from a temp project → `session_brief` on the project ok; on the repo's workspace → UNAUTHORIZED_WORKSPACE.
4. **OpenCode (F-077)**: the four command files packaged under `daem0nmcp/opencode_assets/commands/` (package-data `commands/*.md`); `install_opencode` writes them via `_ensure_file` (existing kept `[exists]`, `--force` overwrites, dry-run writes nothing); byte-equality test against repo `.opencode/commands`.
5. **Windows stdio (A-016) — confirmed working**: `test_process_client.py` stdio variant runs (6 passed, 0 skipped) incl. brief → preflight → store → outcome → recall → restart → recall; scripted MCP SDK stdio_client with `python -m daem0nmcp` in a fresh project (initialize 13.2 s cold, 75 tools, brief/preflight/store/recall ok; graph and apps auto-ready); real Claude Code 2.1.278 `claude mcp add` → Connected, `claude -p --mcp-config` brief/preflight/store/recall ok (entry removed afterwards). Line for PR 12: "Stdio (`python -m daem0nmcp`) works on Windows 11 with Claude Code 2.1.278; HTTP via the launcher is optional."

## Tests
- `test_capabilities.py`: 5 new (auto+installed ready; auto+missing disabled/install; false+installed disabled/unset message; true+missing degraded; auto models-local on 3.10 disabled/upgrade_python).
- New `test_start_server.py`: exported root survives; `--project` wins; cwd default; Windows-only: real `.bat` via `cmd /c` with python.cmd shim checks `%CD%` and a `%1` with spaces/parentheses reach `--project`.
- `test_opencode_install.py`: command files created, existing `recall.md` kept, others match repo.
- Tests that assumed "disabled" now set the variable false: `test_graph_operations.py`, `test_process_code_impact.py` (also new remediation text), `test_process_dreaming.py`, `test_vectors.py`.
- `test_process_portability.py[*-streamable-http]`: with graph auto-on, `storage.rename()` right after HTTP server exit hit WinError 5 intermittently (1/3); passes 4/4 with graph/apps off. Test-only rename retry (15 s deadline) → 5/5. Root cause not pinned.
- Isolation sweep: no EXPECTED pin changed.
- Full suite (Windows 3.12, `.[dev,apps,graph]`) at 5e731b1: 2813 passed, 121 skipped, 0 failed (1263 s). First run before test fixes: 8 failed (6 "disabled" assumptions + portability rename + one-off consolidation INTERNAL_ERROR, 8/8 on rerun). ruff/format clean; mypy api/v7 clean; release inventory valid. Linux Docker python:3.10-slim affected modules: 115 passed, 5 skipped, 2 failed in test_start_server (stale module after `patch.dict(sys.modules)`) → fixed in 46008ec (3 passed, 2 skipped).

## Deviations
1. Command files packaged (pip installs have no repo) with an equality test.
2. Empty `DAEM0NMCP_<P>_ENABLED=` treated as auto.
3. Manual check via `claude mcp add`/`list` + headless `claude -p`, not interactive.
4. Edited tests outside file list (graph_operations, process_code_impact, process_dreaming, vectors, process_portability) and pyproject package-data.

## Follow-ups (not fixed)
- Portability rename race on Windows with graph on (lingering handle — langgraph checkpoint / Leiden / antivirus?).
- `production._capability_states` reports generic "Review the <name> capability profile." remediation in system_health.
- `.opencode/plugins/daem0n.ts` also duplicated in the package (kept in sync by test).
- Recall on a fresh workspace reported LEXICAL_REBUILD_REQUIRED/stale projections, and `bounded_excerpt` repeated content twice (not investigated).
- Stdio cold start ~13 s on Windows.
- `.gitignore` lacks `.venv/`.

## Rounds 2–3
2c6ac4d/a68b205/7df3bc2 addressed all 26 review items (22 addressed, 3 partially, 1 wontfix), then 0543b14 merged PR 3 and 2dc3d23 merged PR 4 — PR 9 now stacks on v7/bf-04.
- **Root cause of the recurring export→import IMPORT_INVALID (hit CI on PRs 1, 3, 5, 9): SQLITE_BUSY, not a bad bundle.** `prepare_import` does session bookkeeping in short autocommit write transactions after `claim_import_finalization` commits; a concurrent writer (projection drain / dreaming job, busier with graph auto-on) holds the write lock past the 5 s busy_timeout (`portable_projections.py:410 _account_physical_files`). `operations._portable_failure_code()` now walks `__cause__` (only), maps busy/locked via PR 3's `errors.is_database_busy` to retryable DATABASE_IN_USE, and logs the cause on both import and export; the test retries only that code, bounded to 8 attempts/30 s and asserting success within the first 4.
- Per-profile preload at build time: a broken-but-installed extra (import raises, e.g. a DLL OSError) degrades only that profile everywhere and names the module and exception type; numpy removed from the graph preload (false warning on every default start).
- `capability_states` render the registry's real remediation; `sandbox_execute_python` requires the agency-e2b capability so `=false` really disables it; launcher refuses a non-existent `--project`, uses `setlocal DisableDelayedExpansion`, omits `--project` when an exported root exists, refuses System32; `_ensure_file` refuses symlinks and creates exclusively; invalid-value message mentions "leave it unset".
- CHANGELOG "Behaviour change" entry (auto-on profiles, startup/download/re-embed costs, empty = auto, per-profile opt-out). Process harness dumps the server log tail on failure.
- Results: Windows full suite 2854 passed / 122 skipped; discovery surface 9/9 across 3 parallel lanes after the PR 4 merge; CI run 35501238206 green on all 9 test legs + lint/type-check/release-inventory.

---

## Review

# PR 9 review — merged (round 3: all addressed/wontfix; PR 3 and PR 4 merged in; CI run 35501238206 green on all 12 jobs)

Reviewer slots: general, general-2, tests, security, plan-alignment (effort 5). Item statuses: 25 addressed, 1 wontfix.

CI: https://github.com/9thlevelsoftware/Daem0n-MCP/actions/runs/35501238206 (green)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
… everything else (#67)

**Brownfield run 3403b448, PR 5 of 12** — audit-and-fix stack for the v7 core (#61/#62). Base: `v7/bf-01-make-ci-green-and-pin-workspace`.

## Plan (contract)

### PR 5: Exempt user text from the path guard, with one predicate for everything else
- Depends on: PR 1
- Files: daem0nmcp/api/v7/models.py, daem0nmcp/api/v7/tools.py, daem0nmcp/api/v7/pinned.py, daem0nmcp/api/v7/runtime_services.py, daem0nmcp/api/v7/operations.py, daem0nmcp/api/v7/record_operations.py, tests/api_v7/test_path_policy.py
- Traces to: FP-2, FP-3, F-005, F-006, F-025, F-008, F-061, F-064, F-071, A-006, A-012, UD-3
- Risk: medium
- Description (KD-2):
  1. **`UserText` type.** In `models.py`, add `UserText = Annotated[str, …]` and keep one `is_host_absolute_path()`. `WireModel`'s model validator skips fields whose annotation is `UserText` (or `list[UserText]`/`dict[str, UserText]` for tags and context values) and checks every other string, input and output. `RelativePath` keeps its validator.
  2. **Annotate the user-text fields** in the input and output models (`tools.py`/`models.py`):
     - memory content, rationale, tags, context values;
     - recall queries, trigger patterns, outcome text, preflight description;
     - rule lists `must_do`/`must_not`/`ask_first` (`tools.py:381-383, 603-605, 1410-1423`) and entity names;
     - output record summaries, recall excerpts/highlights, `meta.warnings` that quote content, dreaming and community summaries, and code-index symbols.
  3. **Delete the drifted guards.** Delete `_path_safe_success` and both `_contains_raw_path` copies (`pinned.py:80-110`, `runtime_services.py:124-140`). Route `operations.py:_reject_raw_paths` through `is_host_absolute_path`.
  4. **Visibility** (F-061). Validate `context.visibility` as a `Literal` of the values `retrieval/projections.py` accepts.
  5. **Filesystem-sink checklist** (R-23), in the PR description. List every input field that reaches `Path(...)`, `open`, `os.*` or subprocess, and confirm each is `RelativePath` (or an https URL for ingest).
  6. **Tests** (`test_path_policy.py`).
     - Store `\\d+`, `/health`, `GET /api/v1/users` and `C:\proj\src\app.py` content as `decision` and as `warning`: the store returns a `record_id`, and brief, recall, preflight and outcome all succeed.
     - A pre-fix poisoned record inserted directly reads back fine.
     - `relative_file_path` values `C:\x` and `/etc/x` are rejected.
     - Round-trip property: every string accepted in a `UserText` input field validates in every output model field that can carry it.
     - **Mechanical coverage** (R-26). Enumerate every free-text-typed field (`str`/`MediumText`/`ShortText` aliases, including inside lists and dicts) of every write tool's input model *from the models themselves*, not from a list. Seed each with `C:\proj\x.py`, `/health` and `\d+`: rules, triggers, entities, context and memories. Also insert the same strings directly into `memory_records`, `governance_*` and entity rows, the way a migrated v6 DB carries them. Then call every read tool and resource template (reusing PR 1's harness), and assert that none returns an error. An unannotated user-derived field fails this test.
     - Model-wide check: a server-generated field (for example a health diagnostic) set to `C:\x` fails validation.
     - PR 1's sweep, including its leak assertion, stays green.
- Acceptance:
  - The new tests pass.
  - `_path_safe_success` and `_contains_raw_path` no longer exist.
  - Exactly one absolute-path regex set remains in `daem0nmcp/api/v7`.
  - The checklist is in the PR description.

---

## What landed

# PR 5 summary: Exempt user text from the path guard, with one predicate for everything else

(Saved by the orchestrator from the implementer's reply.)

Branch `v7/bf-05-exempt-user-text-from-the-path-g`, base f7f024a (PR 1 tip). Worktree `D:\Daem0n-MCP\.claude\worktrees\agent-ab8e615d18037cdb7`.
- 224e7ee `fix: let user text mention paths, with one path predicate for everything else`
- e6a087e `test: prove path-bearing user text round-trips through every tool`
- 9ef43d1 `test: pin every read against its pre-path baseline` (final)

20 files, +1046/−344. Production: `api/v7/{models,tools,resources,pinned,runtime_services,operations,resource_repository,discovery_operations,code_entity_operations,intelligence_operations,utility_operations}.py`, `capture_candidates.py`, `edit_bridge.py`. Tests: new `tests/api_v7/test_path_policy.py`; updated `test_process_workspace_isolation_sweep.py`, `test_models.py`, `test_operations.py`, `test_pinned_handlers.py`, `test_resource_repository.py`, `test_runtime_services.py`.

## What and why
1. **One predicate (`models.py`)**: `is_host_absolute_path(str)` is the only predicate/regex set (Windows, POSIX, `file:` URI; `_WINDOWS_DRIVE` for RelativePath). `UserText = Annotated[str, _USER_TEXT]` and `UserJsonObject` (ContextJsonObject built on it). `guarded_strings(value)` walks fields by their own annotations, skipping any carrying the marker (bare/Optional/list/set/dict). `WireModel.reject_absolute_filesystem_paths` checks every guarded string on input and output. `contains_absolute_filesystem_path` kept as a thin wrapper (edit-bridge native-argument check, legacy_compat). `discovery_operations._safe_code_path` uses `_relative_path`.
2. **Annotated fields**: tools.py aliases `UserShortText`, `UserNameText`, `UserMediumText`, `UserLongText`, `OptionalUserMediumText`, `RecordContext` (unused ShortText/LongText/OptionalMediumText deleted). Inputs: memory content/rationale/context/tags/procedure steps (store, batch, capture promote); all queries; preflight description and target_arguments; edit description; outcome text; rule trigger/must_do/must_not/ask_first/warnings (create + RulePatch); trigger pattern/recall query; entity names, qualified_name; compress/verify text; sandbox code; link description/label; active-context reason; ingest topic; refactor objective; compaction summary/query; debate topic/positions; brief focus_areas. Outputs: TriggerView pattern/query, OutcomeSummary excerpt, PreflightGuidance/RuleCheckData lists, TriggerMatch.matched_value, community/graph-node/entity labels and names, TextSearchHit excerpt, compress text, DocumentSource topic, verify claims, sandbox stdout/stderr, CodeEntitySummary.qualified_name, TodoFinding.text, refactor proposal, relationship description, mermaid text, EntityEvolutionItem.summary, export event/vector/legacy-row payloads, CaptureCandidateView.proposed_record, link label, debate rounds/synthesis; models.py RecordSummary.excerpt, EvidenceItem.bounded_excerpt, RetrievalData.rendered_context, Tag; resources.py RuleView text fields, ActiveContextItem.reason. **Still guarded**: diagnostics, remediation, covenant reasons, UpdateSummary, differences, rule-evolution summary, contradiction explanation, sanitized_logs, refactor warnings, health, manifests, provenance.
3. **Drifted guards deleted**: pinned.py `_path_safe_success`, `_contains_raw_path` + regexes, guidance internal_error branch; runtime_services.py `_contains_raw_path` + 7 call sites (all on validated WireModels); `operations._reject_raw_paths` no longer scans free text — refuses non-empty `file_path`/`project_path`/`database_path` and absolute `relative_file_path`/`file_path_relative`, not descending into `context`, still refuses bytes (before, any memory mentioning `/health` broke export/versions/at-time); also removed `resource_repository._reject_canonical_root` (a warning naming the root made the resource unreadable) and path checks in `code_entity._safe_event_content`, `intelligence._safe_text`, TODO scan text, capture-candidate record, edit-preflight description (only hash stored).
4. **Visibility (F-061)**: `RecordContext` validates `context.visibility` ∈ {workspace, private, shared} (the set projections accept) on store/batch/promote; before, "team" was committed then INTERNAL_ERROR.
5. **Filesystem-sink checklist**: `code_index.relative_root`, `patterns[]`, `code_todos_scan*.relative_root`, `code_refactor_propose.relative_file_path` → RelativePath; `memory_recall_file`/`context_triggers_match.relative_file_path` → SQL/glob only; `memory_store.relative_file_path` stored never opened; `document_ingest_url.url` HttpsUrl (https, no credentials); `sandbox_execute_python.code` → remote E2B. No v7 input reaches subprocess (only fixed git invocation in resource_repository).
6. **Sweep (PR 1)**: B and C each store a warning naming `<root>/src/neutral.py`. Scan validates each response against its own model (`ApiResponse[TOOL_DATA_MODELS[tool]]` / resource document model, strict=False), then checks only `guarded_strings()` for root/home needles — exemption by annotation only. New `test_leak_check_exempts_user_text_by_annotation_only` proves a server-generated field carrying the root fails. Pinned outcomes unchanged (70 tools, 4 resources, B and C, JWT pass).

## Tests
- `test_path_policy.py` (13 tests): model-level rejects `C:\x`, `/etc/x` in relative_file_path; user-text inputs accept 7 strings; guarded diagnostics/remediation with paths fail; >30 UserText output fields accept each string via WireModel probe, guarded MediumText refuses. `test_path_content_is_stored_and_read_back`: pre-fix `\\d+` warning inserted via EventStore appears in session_brief; `\\d+`, `/health`, `GET /api/v1/users`, `C:\proj\src\app.py` stored as decision and warning → brief, recall, text search, preflight, outcome succeed, 4 resources read. `test_every_free_text_field_accepts_paths_and_every_read_survives` (R-26): ~60 free-text fields across 31 tools discovered from model annotations, seeded with `C:\proj\x.py`, `/health`, `\d+`; seeded outcome equals neutral or is a non-refusal; plus direct inserts (records, rule, file trigger, entities, community); every read tool + 4 resources compared to pre-path baseline — none degrades. Mutations (guarding TriggerView.pattern on output or input; guarding EntitySummary.name) fail the test.
- `test_path_policy.py` 13 passed (~75 s); sweep 3 passed (37 s); ruff/format clean; mypy api/v7 clean (native, linux); release inventory valid.
- Full suite: 2815 passed, 121 skipped, 1 failed (`test_database.py::TestSQLitePragmas::test_foreign_keys_enabled`, WinError 32 on `.migrate-v7.lock` — file-lock flake with other agents on the machine; passes on rerun). Earlier `-x` run hit one `disk I/O error` in test_runtime_services; passes on rerun. CI not yet run.

## Deviations
1. Files beyond plan list (each had a user-text refusal or leftover regex found by the mechanical test): resources.py, resource_repository.py, discovery_operations.py, code_entity_operations.py, intelligence_operations.py, utility_operations.py, capture_candidates.py, edit_bridge.py. record_operations.py unchanged (file_path integrity check is PR 6).
2. PR 1 sweep changed to pass with host paths in user text.
3. Six existing tests rewritten (asserted old refusal of paths in free text).
4. Whole JSON fields exempt incl. keys (ExportEvent.payload, vector payloads, legacy_rows, proposed_record, preflight target_arguments, context); structured path keys in event payloads still refused by `_reject_raw_paths`.
5. `document_ingest_url.topic` and `memory_compact.summary` seeds only reach a pre-content refusal (acceptance checked, not storage).
6. `workspace_import` bundles not seeded.

## Follow-ups (not fixed)
- Control characters accepted on input but refused on output by RecordSummary.excerpt and Tag — same stored-then-bricked pattern, not paths.
- Model-wide POSIX regex refuses some valid relative paths (`src/__pycache__/x`) and globs (`src/**/foo.py`).
- edit_bridge refuses native edit arguments with absolute paths, but Claude Code's Edit sends absolute file_path (PR 2 area).
- Migrated absolute `memory_records.file_path` is PR 6 scope.

## Round 2
0a204d3 + 88e66ff + 797685e. 24 addressed, 1 wontfix (non-path migrated-row limits → PR 6). Key: `ErrorRemedy.arguments` → UserJsonObject (token errors no longer crash on path content); control characters read back (`_sanitized` removed from Tag/RecordSummary.excerpt/EvidenceItem.bounded_excerpt and from intelligence/code-entity checks); mechanical coverage test walks nested models from annotations, asserts on unclassified shapes, covers workspace_import via a real export→import round trip, requires non-refused neutral calls, pins GATED_READS and a ≥60 seed floor; vector payloads guarded again, ExportEvent.payload exempt only for `data`, legacy pages through `_reject_raw_paths` with RelativePath validation; leak sweep scans every string allowing root/home only in a marked UserText note; sanitized_logs UserText(4096); RelativePath skips the substring scan (globs OK); legacy_compat keeps path context; `_safe_code_path` deleted; `test_memory.py::test_v7_legacy_id_keeps_opaque_canonical_origin` updated to expect kept context (still asserts opaque ids/evidence). All 8 reviewer mutations now fail. CI run 35439309031 green on all 9 legs + lint/type-check/release-inventory.

---

## Review

# PR 5 review — merged (round 2: all addressed; general/tests/plan-alignment verified; CI 35439309031 green)

Reviewer slots: general, general-2, tests, security, plan-alignment (effort 5). Item statuses: 24 addressed, 1 wontfix.

CI: https://github.com/9thlevelsoftware/Daem0n-MCP/actions/runs/35439309031 (green)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Keep PR 3's retryable DATABASE_IN_USE mapping, exception chaining,
and tokenless-preflight warnings. Drop the old pinned/runtime path
predicates now that WireModel owns the absolute-path rule and
exempts user text.

Co-authored-by: Devil <9thLevelSoftware@users.noreply.github.com>
response.success expects Sequence[ApiWarning], not None. The merge
replaced _path_safe_success, which previously coerced None to ().

Co-authored-by: Devil <9thLevelSoftware@users.noreply.github.com>
…l error (#65)

**Brownfield run 3403b448, PR 3 of 12** — audit-and-fix stack for the v7 core (#61/#62). Base: `v7/bf-01-make-ci-green-and-pin-workspace`.

## Plan (contract)

### PR 3: Report transient failures as retryable and log every internal error
- Depends on: PR 1
- Files: daem0nmcp/api/v7/responses.py, daem0nmcp/api/v7/runtime_services.py, daem0nmcp/api/v7/resource_repository.py, daem0nmcp/api/v7/pinned.py, daem0nmcp/event_store.py, daem0nmcp/api/v7/errors.py, tests/api_v7/test_error_honesty.py, tests/api_v7/test_process_concurrent_writers.py, tests/api_v7/test_pinned_handlers.py
- Traces to: FP-2, F-020, F-027, F-021, F-028, F-055, F-066, F-070, F-082, A-011
- Risk: low
- Description (KD-6):
  1. **Busy mapping.** In `SQLiteMemoryEventWriter._run_mutation` and the outcome path (`runtime_services.py:570-597, 757-770, 883-886`) and in `ResourceRepository._run` (`resource_repository.py:947-984`), map `BoundedWorkerBusyError` and `sqlite3.OperationalError` "database is locked"/"busy" to `DATABASE_IN_USE` (retryable, `retry_after_ms=250`) in `pinned._EXPECTED_SERVICE_ERRORS`.
  2. **Logging.** In `ResponseContext.internal_error`, replace `del error` with `logger.exception("v7 internal error correlation_id=%s", …, exc_info=error)` to stderr. Service raises chain `from exc`. The wire envelope is unchanged.
  3. **Conflict mapping.** In `event_store.py:539-542`, only `sqlite3.IntegrityError` becomes `EventStreamConflict` (F-066).
  4. **Null token.** `memory_preflight` with target arguments that don't normalize keeps `ok:true`, but adds a `meta.warnings` entry with the field errors and "no preflight token issued" (F-070/F-082).
  5. **Tests.**
     - 8 parallel preflight+store flows: every response is either ok or retryable `DATABASE_IN_USE`, and after retries there are exactly 8 records and 8 events.
     - A forced lock returns `DATABASE_IN_USE`.
     - An induced exception is logged with the correlation ID returned to the client.
     - Two `process_client` stdio servers on one workspace fire 5 same-key and 10 distinct-key stores concurrently: exactly one event per key and no `INTERNAL_ERROR` (F-055).
- Acceptance:
  - The new tests pass on 3 OSes.
  - There is no `del error` left in `responses.py`.

---

## What landed

# PR 3 summary: Report transient failures as retryable and log every internal error

(Saved by the orchestrator from the implementer's reply.)

Branch `v7/bf-03-report-transient-failures-as-ret`, base f7f024a (PR 1 tip). Worktree `D:\Daem0n-MCP\.claude\worktrees\agent-a0bb29ebdbe88c9ac`.
- 358256f `fix: report only SQLite integrity errors as event stream conflicts`
- 83a1b7b `fix: report transient contention as retryable and log every internal error` (final)

## What changed
1. **Busy mapping (F-020, F-027, A-011)**
   - `api/v7/errors.py`: `is_database_busy(error)` — true for `BoundedWorkerBusyError`, `code == "DATABASE_IN_USE"`, or `sqlite3.OperationalError` containing "locked"/"busy".
   - `runtime_services.py`: `SQLiteMemoryEventWriter._run_mutation` maps `BoundedWorkerBusyError` → `RuntimeServiceError("DATABASE_IN_USE")`; `_store_sync`/`_record_outcome_sync` catch-alls map busy → DATABASE_IN_USE (else MEMORY_STORE_FAILED/MEMORY_OUTCOME_FAILED); `_open_database` busy → DATABASE_IN_USE (else ACTIVE_V7_UNAVAILABLE); `BasicBriefingService.assemble` and `BasicPreflightService.guidance` pass DATABASE_IN_USE through instead of BRIEFING_FAILED/PREFLIGHT_FAILED.
   - `resource_repository.py`: `ResourceRepositoryError` optional `code`; `_run` raises `ResourceRepositoryError("DATABASE_IN_USE")` when busy; always chains the cause.
   - `pinned.py`: `_expected_service_failure` adds `retry_after_ms=250` for DATABASE_IN_USE (already retryable).
2. **Logging (F-021, F-028)**
   - `responses.py`: `del error` removed; `internal_error` logs `"v7 internal error correlation_id=%s"` with `exc_info` (error or active exception) via `logging.getLogger(__name__).error`. Goes to stderr (last-resort handler / v6 basicConfig), never the MCP wire; wire envelope unchanged; no "NoneType: None" when no exception.
   - `runtime_services.py`: all 28 `raise RuntimeServiceError(...) from None` → `from exc`. MCP-facing resource/middleware errors keep `from None` (tests assert `__cause__ is None`).
3. **Conflict mapping (F-066)**: `event_store.py` INSERT into `memory_events` maps only `sqlite3.IntegrityError` → `EventStreamConflict`; I/O and lock errors propagate. aiosqlite path raises the same class.
4. **Tokenless preflight (F-070, F-082)**: still `ok:true` with null token, now plus `meta.warnings` `PREFLIGHT_TOKEN_NOT_ISSUED` listing schema field names and pydantic error types only (unknown keys as `?`, values never echoed). `_path_safe_success` gained optional `warnings`.

## Tests
- New `tests/api_v7/test_error_honesty.py` (in-process `create_v7_server("stdio")` + `fastmcp.Client`): 8 parallel preflight+store flows → each ok or retryable DATABASE_IN_USE with 250 ms; after retries exactly 8 records and 8 `memory.created` (13–14 busy retries per run locally). Held `BEGIN IMMEDIATE` → store returns retryable DATABASE_IN_USE after 5 s busy timeout; retry succeeds after release. Induced failure in writer transaction → INTERNAL_ERROR; response lacks the canary; log record contains the returned correlation_id and traceback shows canary + MEMORY_STORE_FAILED. Full resource-repository pool → `code == "DATABASE_IN_USE"`.
- New `tests/api_v7/test_process_concurrent_writers.py` (F-055): two real stdio servers on one workspace; 5 same-key + 10 distinct-key concurrent stores → one `memory.created` per key, 11 records, exactly one non-replay among same-key, no code except DATABASE_IN_USE; bounded 120 s deadline honouring `retry_after_ms`.
- `test_pinned_handlers.py`: description-only preflight checks the warning; new DATABASE_IN_USE → retryable, 250 ms.
- `test_runtime_services.py`: cancelled-worker test now expects `RuntimeServiceError("DATABASE_IN_USE")` caused by `BoundedWorkerBusyError`.
- `test_event_store.py`: `test_only_integrity_errors_on_insert_are_stream_conflicts`.
- Reverted-source check: all 8 new/changed tests failed on old code; two-process test reproduced F-055 (INTERNAL_ERROR).
- Full suite Windows 3.12: 2809 passed, 121 skipped, 0 failed (834 s). New tests 5× in a row: all pass (~21 s each). Linux Docker python:3.10-slim affected modules: 94 passed. ruff check/format clean; mypy api/v7 0 errors on win32/linux/darwin; release inventory valid. Sweep EXPECTED pins unchanged and green. macOS unverified until CI.

## Deviations
1. `tests/api_v7/process_client.py` gained `log_name` (default unchanged) so two servers don't share `stdio-server.log`.
2. IntegrityError test in `tests/test_event_store.py` (not in file list) to reuse its schema fixture.
3. DATABASE_IN_USE also flows through `_open_database` and briefing/preflight services, beyond the plan's line ranges — needed for contended session_brief/preflight (F-020's 8 concurrent briefs).
4. `logger.error(..., exc_info=...)` instead of `logger.exception` (some call sites aren't in an except block).
5. Two commits rather than one per item.

## Follow-ups (not fixed)
- `application.py:286-300`: non-pinned tools send any registered error code, incl. DATABASE_IN_USE, with `retryable=False` — contention on non-pinned tools still non-retryable. One-line fix, outside file list.
- `local_state_operations.py` has its own busy check (`_translate_error`) and maps a full pool to TASK_REQUIRED; could use `is_database_busy`/DATABASE_IN_USE.
- Other pools map `BoundedWorkerBusyError` to inconsistent codes (F-071); recall maps full pool to retryable RETRIEVAL_UNAVAILABLE (left as is).
- `.gitignore` lacks `.venv/`.

## Round 2
ce94143 (overrun-then-clean reads → retryable DATABASE_IN_USE; application.py non-pinned DATABASE_IN_USE retryable + remedy; busy memory_store carries memory_preflight remedy; sqlite_errorcode-based busy detection on 3.11+; governance_events IntegrityError-only; ValidationError logged without input values; locked_current storage lock → DATABASE_IN_USE; "workspace unavailable" responses logged with cause) and 987bd00 (root cause of preflight timeouts: guidance read no longer spawns git — `read_briefing_snapshot(include_git_changes=False)`; removed from PR 4's plan). 14 addressed, 2 wontfix (R-8 branch is reachable; R-16 deferred with F-071). Tests reviewer verified R-9..R-12 (20/20 + 12/12 loaded runs). CI run 35423993350 green after re-run of one unrelated portability flake (carried to PR 6).

---

## Review

# PR 3 review — merged (round 2: all addressed; tests reviewer verified; CI 35423993350 green after flake re-run)

Reviewer slots: general, general-2, tests, security, plan-alignment (effort 5). Item statuses: 14 addressed, 2 wontfix.

CI: https://github.com/9thlevelsoftware/Daem0n-MCP/actions/runs/35423993350 (green)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
CI run 35520333257 (ubuntu 3.11) saw `memory_preflight` "spawn" the Leiden
graph worker.  It does not: the recorder patches `subprocess.Popen`
process-wide, and the spawn came from `daem0nmcp-projection-runtime_0` —
the projection drain the *brief's* write scheduled, which with the graph
profile now auto-enabled rebuilds communities and runs Leiden in its own
thread.  Traced it: `_drain_projection_jobs_sync` -> `jobs.run_once` ->
`GraphProjectionBuilder.rebuild` -> `_community_seeds` ->
`run_leiden_bounded`.  Nothing on the preflight path reaches it, so PR 4's
guarantee holds and preflight latency is unaffected.

The fix closes the overlap rather than narrowing the assertion: the test
awaits the drains the brief scheduled before clearing the recorder, so the
window is quiet by construction and `assert spawned == []` stays literal —
no subprocess of any kind, not "no git specifically", and with the graph
profile left enabled.  Deterministic across 14 runs where the unquiesced
window leaked on 1 of 2.

Verified it still fails on a regression: injecting a `Popen` into the
per-request guidance closure fails the test both for `git` (F-040's original
regression) and for any other worker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR 5 (#67) and PR 3 (#65, which carried PR 4 via #66) were squash-merged
into v7/bf-01 after PR 1 (#63) had already been squash-merged into
working_branch, so none of them reached working_branch.

working_branch's tree equals d8842de (bf-01 after PR 2), an ancestor of
bf-01's tip, so the merge result is bf-01's tree exactly. The one textual
conflict, tests/api_v7/test_process_workspace_isolation_sweep.py, is
PR 5's change against PR 1's squashed copy of the same lines; it takes
bf-01's side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLLzyphecQABpqVorHunX
Their content is already on this branch through the squash commits, so
this merge keeps the tree unchanged and only links their history:

- v7/bf-02 (PR 2, #64): tree identical to d8842de.
- v7/bf-03 (PR 3, #65): tree identical to cd5842f.
- v7/bf-05 (PR 5, #67): patch identical to b003c03.

Linking them also gives the v7/bf-06 merge a closer merge base, since
bf-06 descends from bf-05 and carries PR 3's original commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLLzyphecQABpqVorHunX
v7/bf-06 (PR 6, "make migrated v6 workspaces usable") never had a pull
request. It also carries PR 9 (#68) through its merge of v7/bf-09; #68
was squash-merged into v7/bf-04 after bf-04 had already been squashed
into bf-03 (#66), so PR 9 never reached bf-01 or working_branch.

Conflicts, all taken from bf-06's side:
- api/v7/pinned.py: both sides made the tokenless-preflight warnings
  an empty tuple (edfaa57 on bf-03, 0bc432b on bf-06); bf-06's form
  drops the redundant parentheses.
- retrieval/lexical_config.py, verification_v7.py: bf-06 also exports
  RETRIEVAL_PROJECTION_NAMES and builds _SUPPORTED_PROJECTIONS from it;
  the set is unchanged.
- tests/api_v7/test_production.py: bf-06's copy of the preflight
  no-subprocess test also waits for the brief's projection drains.

CHANGELOG.md: fold the second "### Changed" heading the merge produced
into the first.

The result equals bf-06 plus PR 2's patch (f7f024a..27d1682), and
bf-01 plus only PR 6 and PR 9 files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLLzyphecQABpqVorHunX
bf-04's tip is #68's squash of PR 9; its tree equals v7/bf-09's tip
(2dc3d23), which the v7/bf-06 merge already brought in. This merge
keeps the tree unchanged and only links bf-04's history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLLzyphecQABpqVorHunX
Copilot AI balanced review requested due to automatic review settings October 3, 2026 20:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@9thLevelSoftware 9thLevelSoftware changed the title Introduce Daem0n v7 API with workspace-scoped memory and strict protocols merge: land PRs 3, 4, 5, 6 and 9 of the v7 audit stack in working_branch Oct 3, 2026
@9thLevelSoftware
9thLevelSoftware changed the base branch from main to working_branch October 3, 2026 20:09
claude added 2 commits October 3, 2026 20:40
A finalize claims a durable lease, so a concurrent writer that holds the
SQLite write lock past the busy timeout fails the attempt as the retryable
DATABASE_IN_USE and then fails its cleanup release the same way. The
release error was suppressed, so the lease stayed for its full 15 minutes
and every retry the error invites hit the live-lease branch of
claim_import_finalization: a non-retryable TASK_REQUIRED.

Keep a lease whose release failed, and release it before the next finalize
of the session claims. A still-held lock then fails that release as
DATABASE_IN_USE, so the client keeps retrying until the writer finishes. A
live concurrent finalizer is unaffected and still gets TASK_REQUIRED.

Seen in CI on #69: test_process_portability's retried finalize got
TASK_REQUIRED on ubuntu-latest / Python 3.12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLLzyphecQABpqVorHunX
The graph profile's projection drains can hold the write lock past the
busy timeout, and the server then answers the import with the retryable
DATABASE_IN_USE. The path-policy round trip asserted success on the first
call; retry the preflight and import within a bounded deadline, as the
portability test does.

Seen in CI on #69 on windows-latest / Python 3.10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLLzyphecQABpqVorHunX
@9thLevelSoftware
9thLevelSoftware merged commit abfadf0 into working_branch Oct 3, 2026
14 checks passed
@kilo-code-bot

kilo-code-bot Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 0 Issues Found | Recommendation: Merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

Verdict

Approve. The two new commits (cf5bba5, 42f929c) are small, targeted fixes for real CI races, each backed by a regression test. The four merge commits carry work already reviewed in PRs #63, #64, #65, #66, #67, #68.

Correctness / Safety Findings

No correctness or safety findings.

cf5bba5 (daem0nmcp/api/v7/operations.py:160-168, 1213-1239, 1336-1341, 1600-1610) keeps a finalize lease that could not be released under a held SQLite write lock, and lets the next finalize of the same session complete the release before it claims. The retry is bounded because the busy release still raises DATABASE_IN_USE, and a live concurrent finalizer still gets TASK_REQUIRED (covered by test_concurrent_finalizer_is_lease_blocked_and_cannot_delete_winner). The new regression test (test_retry_after_a_contended_finalize_releases_its_abandoned_lease) asserts the exact path the CI failure took.

42f929c (tests/api_v7/test_path_policy.py:794-810) wraps the import call in a bounded retry loop (deadline 30 s, max 8 attempts, 0.5 s sleep) and only retries the retryable DATABASE_IN_USE.

The three defects listed in the PR body as "Known defects carried in" (task dispatcher DATABASE_IN_USE envelope, SpecializedProjectionBuildError not subclassing ProjectionBuildError, _lexical_catchup_pending_sync matching non-lexical rebuilds) are explicitly out of scope here.

Ponytail Review

daem0nmcp/api/v7/operations.py:167: yagni: Module-level _ABANDONED_FINALIZATION_LEASES dict only grows on contended finalizes and only shrinks when the same (storage, session_id) retries. In practice it is bounded by imports; in the pathological case of a one-shot failed import it is a tiny leak. The dict is the minimal fix for the race; anything leaner (e.g. shorter lease TTL) costs user-visible 15-minute lockouts.

Otherwise the diff is two patches totalling ~150 lines, both delete/stdlib candidates that already exist: the try/except swap replaces the existing with suppress(Exception) and the test helper wraps an existing call.

Ponytail net: 0 lines.

Suggested Minimal Patch

No patch needed.

Final Merge Guidance

Can merge as-is. (Already merged at 42f929c.) The three carried-in defects are tracked separately.

Files Reviewed (2 files in the new commits)
  • daem0nmcp/api/v7/operations.py
  • tests/api_v7/test_portable_projections.py
  • tests/api_v7/test_path_policy.py

Reviewed by minimax-m3 · Input: 71.2K · Output: 8.8K · Cached: 906.2K

Review guidance: REVIEW.md from base branch working_branch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants