Skip to content

Release MCP 3.0.0rc6: projection tick scaling#104

Merged
Foxfire1st merged 9 commits into
mainfrom
ar/260712_projection-tick-scaling
Jul 13, 2026
Merged

Release MCP 3.0.0rc6: projection tick scaling#104
Foxfire1st merged 9 commits into
mainfrom
ar/260712_projection-tick-scaling

Conversation

@Foxfire1st

Copy link
Copy Markdown
Owner

Cuts MCP 3.0.0rc6. Carries the 260712_projection-tick-scaling work that removes the observer daemon's per-tick O(contracts × tree × readers) filesystem storm.

What changed

  • PTS-L1 — walk-free load_contract. Leaf-id normalization moved off the hot read path to write time plus an explicit idempotent heal-leaf-ids migration sweep. A contract load no longer walks the whole tasks tree.
  • PTS-L2 — one shared contract snapshot per tick. read_enclosures, read_engine_process_facts, and drift pruning share a single enumeration, backed by a stat-identity (mtime_ns, size, ctime_ns) parse cache across ticks.
  • PTS-L3 — change-driven projection with idle heartbeat. The projector wakes on debounced coordination-input changes (watchfiles) or a 15s heartbeat instead of unconditionally every second. Watch set is scoped to real projection inputs and never descends into worktree/container runtime; failure degrades loudly to fixed-interval ticking.

Measured impact

  • Observer tick read cost ~750 ms → ~9 ms.
  • Idle re-projections ~20 / 20s → ~3 / 20s; change latency ~1.2 s.
  • Steady daemon CPU 160% → ~7–22% (residual is the independent metrics/supervisor loops).

Verification

  • Full mcp suite: 2126 passed, 3 skipped.
  • Repository quality wrapper: green (also enforced by the pre-push hook).
  • Live-tree verification with provider containers running: watcher establishes with zero permission errors.

Version bumped to 3.0.0rc6 in the three repository-owned locations (pyproject, SERVER_VERSION fallback, README).

🤖 Generated with Claude Code

Readone Mohamed and others added 9 commits July 12, 2026 19:37
…keyed parse cache

Each projection tick enumerated and re-parsed every leaf enclosure contract
three times (read_enclosures, read_engine_process_facts, drift-snapshot
pruning each ran their own iter_leaf_enclosure_contracts + load_contract
pass). observer/contract_snapshot.py now builds ONE immutable ContractSnapshot
per tick inside project_and_write, backed by a cross-tick parse cache keyed by
stat identity (path + mtime_ns + size) and pruned to the live enumeration each
build. The three readers take a keyword-only contracts=None parameter; a
standalone call builds a local snapshot, so public signatures and behavior are
unchanged. Parse failures are never cached (skip + retry next tick, the exact
pre-existing containment). The landing refresher and supervisor sweep keep
their own passes: the refresher walks on the asyncio loop thread while the
cache is single-writer on the projection worker thread, so joining them is not
trivially safe (R5).

Tests: ContractSnapshotSharedPassTests proves N parses on tick 1, zero on an
unchanged tick 2, exactly the changed file on tick 3 (R7); one enumeration per
full project_and_write tick (R1); reader-output equality standalone vs shared
vs warm-cache (R4); bounded retention across churn (R3); malformed-contract
retry containment (R2). Full mcp suite 2096 passed / 3 skipped; ruff repo-wide
clean; pyright clean on touched files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKStH52GkNm7UZ37eijbLh
load_contract no longer runs normalize_contract_leaf_id per read (the
hidden resolve_leaf_ref tasks-tree walk that dominated the projection
tick); write_contract keeps write-time normalization. Legacy
stem-shaped leaf ids heal through the new idempotent
heal_contract_leaf_ids sweep (per-root doc-id fast path, loud logs,
error containment) invoked via the heal-leaf-ids CLI subcommand or
directly at startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKStH52GkNm7UZ37eijbLh
The projector re-projected the whole world unconditionally every
--interval second (1.0s in production) even when nothing changed
(py-spy: _tick_sync 11.1s of a 15s quiet-world sample). Waking is now
change-driven: a debounced watchfiles (inotify) watcher over the
projection's actual input surfaces feeds a ChangePacer -- settle 0.1s
after the last write of a burst, max-delay = interval from the first
(a busy world keeps the former cadence), never two projections closer
than interval (the flag keeps meaning the fast-path cadence floor) --
and a slow configurable heartbeat (--heartbeat, default 15s) bounds
/api/state staleness and time-derived field resolution for everything
a watcher cannot see (memory-root TTL surfaces, landing state,
git-derived worktree status, stale/overdue flips).

- new serving/change_watcher.py: derived watch roots (tasks/, observer
  lifecycles/workspace/drift, providers status/setup, temp progress and
  tool-report dirs, worktrees/*/*/provider-runtime only -- never the
  checkouts), input-event filter (self-outputs, *.tmp, workspace river,
  supervisor heartbeat), 30s watch-set refresh, reconcile tick after
  re-establish, loud fixed-interval degrade on any failure (fail-open)
- projector: pacer-or-legacy-sleep pacemaker; tick body, prime, SSE
  diff/broadcast, ETag revision, and cancellation semantics unchanged;
  sim replay and injected-now() tests keep the exact legacy pacing;
  projection_count/last_wake_reason instrumentation
- create_app: heartbeat + watch_changes seams (watcher live-only);
  --heartbeat plumbed through the CLI, daemon spawn/ensure, and the
  --reload dev factory
- watchfiles>=1.1,<2 added as an explicit dependency (decision logged;
  no watch library existed in the tree)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKStH52GkNm7UZ37eijbLh
…dening)

Adversarial review confirmed a permanent-staleness hole: chmod 000 on a
contract changes neither mtime_ns nor size, so the (mtime_ns, size) cache kept
serving the old good parse FOREVER, where the pre-cache readers degraded to
skip-every-tick (PermissionError) -- and unlike the same-stat rewrite window,
it never self-healed. Same trigger class: a rewrite with (mtime_ns, size)
pinned via os.utime. ctime_ns changes on chmod/utime/rename/writes and is
untouched for genuinely unchanged files, so adding it to _ParseCacheEntry and
the _cached_contract comparison costs zero extra parses on the unchanged path.

Tests: permission flip is skipped (not served stale) on the next build and
self-heals when readability returns; a same-size utime-pinned rewrite is now
detected and re-parsed. Both step past the kernel's coarse-clock granule
before mutating (ctime cannot be set explicitly), and the chmod assertion is
skipped when running as root. Focused suites 260 passed; full mcp suite 2098
passed / 3 skipped; ruff + pyright clean on touched files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKStH52GkNm7UZ37eijbLh
Two one-line hardenings adopted from the adversarial review (verdict
INTEGRATE, no blocking findings):

- F1: exclude workspace/operator-inbox.lock from the watch filter. The
  inbox store opens it "a+b" on every access (including each tick's
  read_agent_pickups), so its boot-time creation emitted exactly one
  spurious change-tick per daemon start.
- F3: move the projection_input_roots derivation inside the watcher
  run loop's retry guard. A transient stat/glob failure now follows the
  same loud degrade-and-retry path as a watch failure instead of
  escaping run() and killing the task permanently (which was fail-open
  but lost the periodic self-heal).

Tests: filter unit extended with the lock file; new recovery test
proves a derivation that raises once degrades loudly and re-establishes
on the next retry cycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKStH52GkNm7UZ37eijbLh
The watch derivation globbed worktrees/*/*/provider-runtime and watched it
recursively, descending into each task's container data (Postgres/grepai) —
unreadable to the daemon user (permission-denied crashed the whole watch) and
high-churn (every WAL write would re-project). Remove that root entirely:
worktree provider-state.json changes are infrequent and heartbeat-covered,
and central provider status is already watched via logs/providers. Add
ignore_permission_denied=True to awatch as defence-in-depth.
Bump the three repository-owned version locations to 3.0.0rc6. This RC carries
the 260712_projection-tick-scaling work:
- walk-free load_contract + explicit heal-leaf-ids migration seam (PTS-L1)
- one shared contract snapshot per tick with a stat-keyed parse cache (PTS-L2)
- change-driven projection with idle heartbeat; no worktree/container watching (PTS-L3)

Observer tick read cost ~750ms -> ~9ms; idle re-projections ~20/20s -> ~3/20s.
@Foxfire1st
Foxfire1st merged commit b120efb into main Jul 13, 2026
4 checks passed
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.

1 participant