Home / Projects / Orchestrata Ultimate / Decisions / ADR index / ADR-0097
ADR-0097: The resident AST daemon publishes the snapshot file from its warm cache — the post-edit hooks stop re-serializing the tree on every write, and the duplicate home-directory refresh hook is retired
| field | value |
|---|---|
| Status | Accepted — Accepted (ruling, Mohamed, 2026-10-13, verbatim: "SHOULD WE REPLACE IT NOW TO SAVE HOOK TIME, NOTE I THINK IT WAS DONE IN ORCHERSYTAYA ULTIMATE ARCHITECTURE PROJECT" → recommendation presented → "mint the adr and brief… |
| Date | 2026-10-13 |
| Register | factory (docs/adr, 4-digit ids) |
| Source | docs/adr/0097-the-resident-ast-daemon-publishes-the-snapshot-the-post-edit-hooks-stop-re-serializing-and-the-duplicate-hook-is-retired.md — permalink at f5dffa8ab (unchanged on master since that commit) |
| Wiki register entry | ADR-0097 (wiki status: Accepted) |
The record
Status: Accepted (ruling, Mohamed, 2026-10-13, verbatim: "SHOULD WE REPLACE IT NOW TO SAVE HOOK TIME, NOTE I THINK IT WAS DONE IN ORCHERSYTAYA ULTIMATE ARCHITECTURE PROJECT" → recommendation presented → "mint the adr and brief the builder") · Date: 2026-10-13 · Extends the AST-daemon work (P1 location-independent, P2 install-tools, both merged) · Bounded hook change; not the codetools loader of codetools ADR-0022, which stays sequenced behind the gate wave
Context
Two refresh paths fire on every Write/Edit in an orchestrata checkout:
- The plugin's post-edit hook
plugins/orchestrata/project-hooks/ast-incremental-refresh.sh(deployed under~/.claude/plugins/cache/orchestrata/orchestrata/<ver>/), dispatched by_dispatch.sh, which detachesscripts/serialize_codebase_ast.py serialize … --incrementalto rewriteplans/ast-ultimate.jsonand logs to.cache/ast-refresh.log. claude/hooks/ast_sync_hook.py(deployed to~/.claude/hooks/, also shipped asplugins/orchestrata/hooks/ast_sync_hook.py), which detaches the same serialize plus a Prolog KB reindex.
Both are already incremental in the narrow sense (only files whose content
hash changed are re-parsed; every other module's dict is reused), yet the
refresh hook's own header records a warm re-serialize of 64 s, and on
2026-10-13 at 15:02 one edit in the adr-sync-fix worktree had two
serializers plus the module-context hook running concurrently for over two
minutes. The hook explicitly detaches so the edit is not blocked, which means
the cost is paid in CPU and in snapshot staleness windows rather than in
latency, and it is paid twice.
What already exists: scripts/ast_daemon.py, deployed as
/opt/orchestrata-tools/bin/ast-daemon and run per checkout by
orchestrata-ast-daemon@<path>.service (started by the
start-ast-daemon.sh hook; running today for the primary checkout, the
boot-errors worktree and, since 15:06, adr-sync-fix). It holds the parsed
tree warm, watches the sources with watchfiles (inotify, debounced),
invalidates only changed files in its cache, and serves every toolkit query
over a Unix socket (query_ast_daemon in serialize_codebase_ast.py, with
a local fallback). Its header states that plans/ast-ultimate.json is "a
CLI-only snapshot" the daemon does not write. The P1/P2 worktrees that built
it are merged (0 commits ahead of master); the architecture branch carries no
further AST work. So the warm, incremental engine Mohamed remembers is built
and running; the gap is that the on-disk snapshot the freshness pin,
code_intel, gates.py and the Prolog KB read is still produced by a
separate re-serialize on every edit.
Decision (Mohamed's ruling, restated as the executable rule)
- The daemon publishes the snapshot.
ast_daemon.pygains apublishcommand (socket request) that writes the snapshot artifact for its repo (plans/ast-<name>.json, same schema andfile_metablock the serializer writes, atomically via temp file +rename(2)) from its warm cache, and a watcher-driven publish: after the debounced invalidation of changed.pyfiles, the daemon republishes the snapshot (debounce configurable, default the existing--watch-debounce-ms). Publishing reuses the serializer's own writer so the artifact is byte-compatible with a CLI serialize of the same tree. - The post-edit hook asks the daemon first.
ast-incremental-refresh.shchecks for a live daemon socket for the repo (<repo>/.claude/run/ast-daemon.sock); if present it sendspublish(or, when the watcher already republished after this edit, does nothing) and exits; only when no daemon is live does it fall back to today's detached serialize. The freshness pin (ast-freshness-pin.sh) keeps reading the file and keeps its STALE verdict logic unchanged. - The duplicate hook is retired.
claude/hooks/ast_sync_hook.pystops serializing; its remaining job (the Prolog KB reindex) moves behind the same daemon-publish event (a reindex runs only when the snapshot content hash changed), or is folded intogates.py's existingast-sync + reaugmentordering, which stays the one explicit full refresh. One edit triggers at most one snapshot write and at most one reindex. - Measured acceptance. On a worktree with a live daemon, the post-edit
wall from
Editreturn to a fresh snapshot (file_metahash of the edited file updated) is measured and recorded; target is seconds, against the 64 s warm re-serialize and the >120 s double run observed. The freshness pin reports fresh after the edit.gates.pyfull refresh still works and still reaugments runtime calls (the bareast-synczeroing hazard noted in the pin hook must not regress). - Scope. Hooks and the daemon only; no change to the snapshot schema,
to
serialize_codebase_ast.py's CLI, tocode_intel, or to codetools. Deployed throughdeploy/install-claude-config.shanddeploy/install-tools.shlike any hook/tool change.
Alternatives rejected
- Wait for codetools ADR-0022's loader. Correct end state, but sequenced behind the gate wave by Mohamed's standing order; every builder edit until then pays one to two minutes of hook CPU.
- Shorten the hook by skipping the refresh. Leaves the snapshot stale; the freshness pin would then block structural answers on every edit.
- Run the serialize synchronously but faster. The 64 s is parse plus write of a large tree; the daemon already holds the parse.
Consequences
- Builder brief
plans/briefs/2026-10-13-ast-daemon-publishes-snapshot.md(DESIGN_WAIVER: ADR-0097). Files:scripts/ast_daemon.py,plugins/orchestrata/project-hooks/ast-incremental-refresh.sh,.claude/hooks/ast-incremental-refresh.sh(same content, repo-local copy),claude/hooks/ast_sync_hook.py,plugins/orchestrata/hooks/ast_sync_hook.py, tests besidetests/scripts/test_ast_daemon.py,tests/claude/test_start_ast_daemon_hook.py. - After deploy, every edit in a daemon-served checkout costs one socket call; checkouts without a daemon keep today's behaviour.
- The daemon becomes the single writer of the snapshot in served checkouts;
gates.pyremains the explicit full refresh.
Provenance
Mohamed, 2026-10-13, verbatim in Status. Measurements: hook header
(ast-incremental-refresh.sh lines 84–87, "a warm re-serialize is 64s");
process list at 15:02 (ast-sync ×2, serialize_codebase_ast.py serialize,
module_context.py, 108–118 s elapsed on one edit); daemon source
scripts/ast_daemon.py (_watch_loop, _invalidate_file, header note on
plans/ast-ultimate.json); unit orchestrata-ast-daemon@.service
(ExecStart=/opt/orchestrata-tools/bin/ast-daemon --repo-root /%I --socket /%I/.claude/run/ast-daemon.sock); worktrees wt-AST-DAEMON-P1/P2 at 0
commits ahead of origin/master. Recorded by the coordinating session; the
code change is delegated under the brief.
Incident evidence added 2026-10-13 15:20, after acceptance (append-only;
decision unchanged, scope of rules 2–3 made explicit): one builder's edits
in the adr-sync-fix worktree accumulated 32 concurrent
serialize_codebase_ast.py serialize processes, 16 ast-sync and 12
orphaned module_context.py hook processes (reparented to the user
systemd manager after their 45 s hook timeout), driving the load average to
45 on 16 threads; the coordinator terminated the hook byproducts and
load fell to 29 within a minute. Causes: the plugin refresh hook's
single-writer flock is bypassed by ast_sync_hook.py, which spawns its
serializer unlocked; and _dispatch.sh does not kill a timed-out hook's
process group. Both are inside the hooks this ADR already names, so the
builder brief gains: (a) at most one serializer per repo at any time, with
the daemon publish single-writer and the fallback taking the same lock,
proven by a burst test; (b) every dispatched hook runs in its own process
group that is killed on timeout, proven by a slow-dummy-hook test.
Mohamed, same time: "I THINK WE FIXED THE ORPHAN PROBLEM EITHER HERE OR
USING KIMI IN THE ORCHESTRATA ULTIMATE ARCH PROJECT" — checked: the fixes
he recalls are reap-orphan-waiters.sh (agent polling loops, deliberately
conservative after it killed a live agent on 2026-10-11) and the
architecture branch's "incremental loads no longer leave orphan subtrees"
(graph), neither of which covers hook child processes.
Related ADRs
- Also mentioned: ADR-0022
Rulings cited
None.
Specs cited
None (or no spec page exists for the ones named).