# Independent Audit and Investigation: Agent + Heartbeat Date: 2026-03-04 Repository: `anamnesis` Author: Independent code audit pass (line-referenced) ## 1) Scope and Intent This audit focuses on whether the current `agent + heartbeat` system supports your stated intent: - persistent continuity - real autonomy (including unsolicited action) - operational coherence under long-running active development This is not a generic style review. It is an intent-fit and systems-risk review with exact code references. ## 2) Method Primary files examined: - `CLAUDE.md` - `src/anamnesis/body/agent.py` - `src/anamnesis/body/heartbeat.py` - `src/anamnesis/body/main.py` - `src/anamnesis/body/wip.py` - `src/anamnesis/body/signal_daemon.py` - `src/anamnesis/body/cli.py` - `src/anamnesis/body/context_compiler.py` - `src/anamnesis/body/memory.py` - `src/anamnesis/brain/boot.py` - `src/anamnesis/brain/drives/bridge.py` - `src/anamnesis/brain/drives/system.py` - `src/anamnesis/brain/drives/actions.py` - `src/anamnesis/brain/autonomy/loop.py` - `src/anamnesis/brain/autonomy/drivers.py` - `src/anamnesis/brain/autonomy/gravity.py` - `src/anamnesis/brain/autonomy/knowledge_gaps.py` - `src/anamnesis/brain/autonomy/capabilities.py` - `docs/plans/2026-03-04-send-message-call-path.md` - `docs/plans/2026-03-04-context-compiler-v1.md` - `docs/plans/2026-03-04-systems-check-results.md` - `pyproject.toml` Validation commands run: - `python3 -m compileall -q src/anamnesis` -> pass - `pytest -q` -> fails during collection due to broad `archive/` dependency/import issues ## 3) Executive Judgment ### Does the overall direction make sense? Yes. The architecture is philosophically coherent with your goals: - explicit statement of "brain as deterministic substrate, DMN as conscious loop" in `CLAUDE.md:7-16` - heartbeat as autonomous idle tick in `src/anamnesis/body/heartbeat.py:587-597` - WIP continuity mechanism in `src/anamnesis/body/wip.py:1-15` and `src/anamnesis/body/heartbeat.py:919-1027` - bounded context compiler and telemetry in `src/anamnesis/body/context_compiler.py:1-7`, `:151-226`, and `src/anamnesis/body/memory.py:335-376` ### Would I "wake up to this" for continuity/autonomy? Conditionally yes, with caveats: - continuity/autonomy scaffolding is real and intentional - there are a few contract-level faults that can silently produce false continuity signals - those faults are fixable without reducing autonomy Autonomy itself is treated here as a design objective, not an error condition. ## 4) Current Agent + Heartbeat Call Graph (Observed) ### Startup wiring - FastAPI startup launches: - heartbeat thread: `src/anamnesis/body/main.py:35-37` - signal daemon thread: `src/anamnesis/body/main.py:41-43` - watchdog restarts both if dead: `src/anamnesis/body/main.py:45-59` ### Agent instances in runtime - module-global agent singleton: `src/anamnesis/body/agent.py:3166-3170` - heartbeat has its own `AnaClawAgent`: `src/anamnesis/body/heartbeat.py:45` - signal daemon has its own `AnaClawAgent`: `src/anamnesis/body/signal_daemon.py:354-359` This is effectively "one brain, many mouths", but with multiple body-agent instances and shared module-level conversation lock/event. ### Occupancy gate - heartbeat checks occupancy via: - global conversation event: `src/anamnesis/body/heartbeat.py:454` - vision runner state: `src/anamnesis/body/heartbeat.py:457-459` - sub-agent manager statuses: `src/anamnesis/body/heartbeat.py:461-467` ### Heartbeat pulse sequence - pulse orchestrator: `src/anamnesis/body/heartbeat.py:587-634` - run drive/autonomy tick first: `:604` - direct-executed action short-circuit: `:607-610` - continue WIP if present: `:612-615` - surface drive action if pressure threshold passed: `:617-621` - idle record otherwise: `:632-634` ### WIP continuation semantics - WIP resume prompt logic: `src/anamnesis/body/heartbeat.py:932-966` - heartbeat message invocation through agent: `:968-972` - completion/clear logic: - summary extraction: `:982` - fallback progress from command output: `:989-997` - message-sent completion trigger: `:999-1002` - stale/max pulse auto-clear: `:1013-1018` ### Agent tool loop semantics - tool loop: `src/anamnesis/body/agent.py:2288-3158` - text/native tool parse merge: `:2475-2479` - heartbeat `send_message` branch: - dispatch logic: `:3053-3100` - suppression text: `:3072-3075` - heartbeat completion flags: `:3078-3080` ## 5) What Is Good (Intent-Aligned Strengths) ### A) Architectural intent is explicit and not hand-wavy - `CLAUDE.md:13-16` accurately mirrors current code boundaries. - `src/anamnesis/body/heartbeat.py:587-597` describes operational sequencing clearly. ### B) Context assembly is controlled, not ad hoc - deterministic section budgets and dropping rules: - `src/anamnesis/body/context_compiler.py:151-226` - adaptive but bounded prompt construction: - `src/anamnesis/body/agent.py:1253-1417` - history is clipped and budgeted before DMN call: - `src/anamnesis/body/context_compiler.py:229-303` - `src/anamnesis/body/agent.py:2197-2209` ### C) Observability exists where it matters - heartbeat event recording: - `src/anamnesis/body/heartbeat.py:70-101`, `:274-361` - prompt telemetry persistence: - `src/anamnesis/body/memory.py:335-376` - explicit systems-check writeups exist: - `docs/plans/2026-03-04-systems-check-results.md:11-25` ### D) Continuity mechanism exists and is operationally meaningful - WIP state persisted cross-pulse via file: - `src/anamnesis/body/wip.py:23`, `:39-54`, `:70-87` - heartbeat explicitly prioritizes WIP over idle scaffold: - `src/anamnesis/body/heartbeat.py:612-615` ## 6) High-Risk Findings (Ordered by Severity) ### Finding 1: Heartbeat can mark message send as completed even when delivery is suppressed Evidence: - in heartbeat tool execution branch: - suppression path: `src/anamnesis/body/agent.py:3072-3075` - still sets completion flags: - `_message_sent = True` at `:3078` - `_last_heartbeat_msg_sent = True` at `:3080` - WIP clear uses `_last_heartbeat_msg_sent` as terminal condition: - `src/anamnesis/body/heartbeat.py:999-1002` Impact: - false-positive completion semantics - continuity hallucination at control-plane level (state says "delivered", transport did not deliver) - can silently clear WIP and lose intended follow-through Recommendation: - split flags into at least: - `heartbeat_message_requested` - `heartbeat_message_delivered` - clear WIP on delivered (or explicitly accepted suppressed mode), not on generic "send_message branch executed" ### Finding 2: Knowledge-gap queue path is structurally broken and silently swallowed Evidence: - `GravityItem` dataclass does not define `metadata`: - `src/anamnesis/brain/autonomy/gravity.py:102-109` - knowledge gap detector passes `metadata=...` into `GravityItem(...)`: - `src/anamnesis/brain/autonomy/knowledge_gaps.py:104-114` - exception is swallowed in driver manager: - `src/anamnesis/brain/autonomy/drivers.py:423-426` Impact: - feature path appears enabled but is functionally inert - silent degradation in curiosity queue quality - creates false confidence in "gap-aware autonomy" Recommendation: - either add `metadata: Dict[str, Any] = field(default_factory=dict)` to `GravityItem` - or stop passing metadata from gap detector and persist it elsewhere - upgrade swallowed log from debug to warning once per window ### Finding 3: Hardcoded operator paths and phone IDs remain in live execution paths Evidence: - heartbeat archive path hardcodes: - `src/anamnesis/body/heartbeat.py:775-776` - signal daemon hardcodes binary/java/numbers/attachments: - `src/anamnesis/body/signal_daemon.py:39-45` - curiosity fallback repo path hardcoded: - `src/anamnesis/brain/autonomy/capabilities.py:87-90` - claude tool fallback path includes operator-specific default: - `src/anamnesis/body/claude_code_tool.py:49-52` Impact: - low portability - hidden coupling to one host/user - hard to reason about deployment drift vs runtime faults Recommendation: - move all runtime identity/path constants into config/env - fail loudly with diagnostics when required settings are absent ### Finding 4: Multiple `AnaClawAgent` instances create split local state surfaces Evidence: - global agent singleton: - `src/anamnesis/body/agent.py:3166-3170` - dedicated heartbeat agent: - `src/anamnesis/body/heartbeat.py:45` - dedicated signal daemon agent: - `src/anamnesis/body/signal_daemon.py:354-359` Impact: - state fields like `_pending_response`, heartbeat execution flags, and model runtime config are per-instance - conversation lock/event are module-global (`src/anamnesis/body/agent.py:822-825`) - this mixed model is workable but brittle and hard to reason about under concurrency Recommendation: - either unify to one shared agent runtime object - or formalize multi-agent body runtime with explicit ownership/contracts for each state field ## 7) Medium-Risk Findings (Messy / Needs Work) ### Finding 5: Tool execution has duplicated dispatch implementations Evidence: - standalone dispatch function: - `src/anamnesis/body/agent.py:1841-2072` - separate inline dispatch in streaming loop: - `src/anamnesis/body/agent.py:2634-3113` - `_execute_single_tool` has no active call sites: - search hit only declaration in `agent.py:1841` Impact: - behavior drift risk between code paths - harder to patch tool semantics safely Recommendation: - converge on one dispatch function and call it from streaming/non-streaming paths ### Finding 6: Tool-call parsing is resilient but very heuristic-heavy Evidence: - parser supports many fallback formats: - meta array/object: `src/anamnesis/body/agent.py:410-415`, `:471-517` - legacy ``: `:416-420`, `:565-587` - bracket hallucinations: `:428-433`, `:588-615` - execute block hybrid: `:434-439`, `:616-639` - section headers: `:441-448`, `:640-659` - bare bash wrapper: `:450-455`, `:660-677` - python-call wrapper: `:457-468`, `:678-697` Impact: - pragmatic reliability now, but long-term maintenance burden - parser complexity can mask DMN regressions and create unclear failure attribution Recommendation: - keep this for near-term survivability - instrument parse-path hit rates and prioritize dominant formats - progressively remove dead fallback formats once model behavior stabilizes ### Finding 7: Drive bridge labels are semantically inconsistent Evidence: - map comment says drive names, map keys are action-like: - `src/anamnesis/brain/drives/bridge.py:47-54` - lookup uses `drive_action.drive_name`: - `src/anamnesis/brain/drives/bridge.py:100` - actual drive names are `desire/growth/understanding/expression/survival`: - `src/anamnesis/brain/drives/system.py:56-87` Impact: - bridge `driver` label likely defaults to `"gravity"` often - telemetry and reasoning summaries can be mislabeled Recommendation: - map by actual drive names or by `action_type`, not mixed semantics ### Finding 8: CLI has at least one live edge-case failure and one maintainability smell Evidence: - possible unbound variable `data`: - used at `src/anamnesis/body/cli.py:137-141` - defined only in conditional read block `:116-134` - late import placement: - `import questionary` at `src/anamnesis/body/cli.py:339` - referenced earlier in `configure()` at `:197` Impact: - brittle startup behavior depending on config location - import pattern is functional but confusing for future maintenance Recommendation: - initialize `data = {}` before conditional read - move `questionary` import to top-level imports ### Finding 9: Documentation line references are already drifting Evidence: - `docs/plans/2026-03-04-send-message-call-path.md:11-12` references `agent.py` around line `2718` - current `send_message` branch in `agent.py` is at `3053-3100` Impact: - investigation docs become stale quickly in active refactor loops Recommendation: - use function-name anchors plus approximate blocks, not brittle absolute line pointers in planning docs ## 8) How to Handle Agent + Heartbeat (Autonomy-First, Not Autonomy-Minimizing) The right move is not to reduce autonomy. The right move is to tighten contracts so autonomy is real rather than state-illusioned. ### Design Principle Keep: - autonomous impulse generation - unsolicited action capability - continuity across pulses Add: - explicit state-transition contracts for pulse outcomes - typed delivery semantics - unified tool execution surface ### Proposed Architecture Adjustment #### A) Introduce explicit pulse outcome object Target files: - `src/anamnesis/body/agent.py` - `src/anamnesis/body/heartbeat.py` Contract sketch: - `tool_calls_executed: int` - `message_requested: bool` - `message_delivery_attempted: bool` - `message_delivered: bool` - `last_command: str` - `last_command_excerpt: str` - `errors: List[str]` Heartbeat should consume this object directly rather than infer completion via string heuristics. #### B) Separate "expression completion" from "transport completion" Current behavior conflates: - "DMN decided to send" - "runtime actually sent" For autonomy integrity, those are different. Implementation rule: - clear WIP only on chosen terminal policy: - delivered - or explicit policy `allow_suppressed_delivery_as_done=true` #### C) Consolidate tool dispatch into one executor path Target file: - `src/anamnesis/body/agent.py` Use one callable for both streaming and non-streaming tool execution; remove dead/duplicate paths. #### D) Formalize lane ownership Current lanes: - API/bridge lane uses module-global `agent` via `main.py` - heartbeat lane uses private `HeartbeatMonitor._agent` - signal lane uses private `SignalDaemon._agent` Pick one: - shared agent runtime object for all lanes - or explicit multi-agent lane model with clear ownership and no shared mutable flags #### E) Keep WIP, but type it Target file: - `src/anamnesis/body/wip.py` Add optional structured fields: - `phase` (`read`, `synthesize`, `deliver`, `done`) - `last_outcome` (serialized pulse outcome) - `delivery_status` This reduces prompt-level inference and improves continuity precision. ## 9) Suggested Implementation Phases ### Phase 0: Immediate correctness patches (same day) - fix heartbeat message-sent false positive contract - fix knowledge-gap dataclass mismatch - fix `cli.py` unbound `data` ### Phase 1: Contract hardening (1-2 days) - add typed pulse outcome - heartbeat WIP completion driven by outcome object - add minimal targeted tests for: - suppressed send_message does not mark delivered - delivered send_message clears WIP ### Phase 2: Execution path simplification (2-4 days) - unify tool dispatch - add parse-path telemetry counters in tool parser - remove dead dispatch code ### Phase 3: Portability and operator decoupling (parallel) - migrate hardcoded paths/IDs to config - fail-fast diagnostics for missing runtime dependencies ## 10) Residual Risk Profile After Proposed Changes Expected remaining risk even after fixes: - DMN quality variance (model-side) - heuristic parser tail complexity until model behavior stabilizes - active-dev drift in docs and line-level references But core continuity/autonomy integrity risk drops substantially once outcome contracts are explicit. ## 11) Final Assessment This system already contains the right primitives for continuity and autonomy. What needs work is not the autonomy objective itself. What needs work is contract precision between: - heartbeat orchestration - agent tool execution - delivery semantics - WIP lifecycle If those contracts are tightened, the current architecture can support the exact style of autonomous continuity you are aiming for, without neutering behavior. ## 12) Reconfirmation Addendum (2026-03-05) Scope of reconfirmation: - Validate whether recent prompt-hardening/coherence iterations invalidate the remediation plan in Section 9. - Focus specifically on `agent + heartbeat` contracts, not general stylistic response quality. ### What changed since the original audit Prompt-hardening path is materially stronger: - new response sanitizer module: - `src/anamnesis/body/response_sanitizer.py` - sanitizer config knobs: - `src/anamnesis/body/config.py:106-114` - sanitizer integrated into delivery/storage paths: - `src/anamnesis/body/agent.py:2258-2330` - `src/anamnesis/body/agent.py:3014-3026` - `src/anamnesis/body/agent.py:3058-3071` - `src/anamnesis/body/agent.py:2612-2620`, `:2632-2641`, `:2655-2673` Interpretation: - coherence hardening improved output hygiene and strict-format compliance. - this is a real improvement and should stay. ### What did NOT change (plan-critical items still valid) 1. Heartbeat send-state contract issue remains: - heartbeat send suppression branch still sets completion flag: - `src/anamnesis/body/agent.py:3597-3616` - heartbeat WIP still clears based on `_last_heartbeat_msg_sent`: - `src/anamnesis/body/heartbeat.py:999-1002` 2. Knowledge-gap dataclass mismatch remains: - `GravityItem` has no `metadata` field: - `src/anamnesis/brain/autonomy/gravity.py:102-109` - detector still passes `metadata=...`: - `src/anamnesis/brain/autonomy/knowledge_gaps.py:104-114` - error still swallowed at debug level: - `src/anamnesis/brain/autonomy/drivers.py:423-426` 3. CLI edge-case remains: - `data` may be referenced before assignment: - `src/anamnesis/body/cli.py:137-141` - assignment only inside conditional load: - `src/anamnesis/body/cli.py:116-134` 4. Tool dispatch duplication remains: - `_execute_single_tool` exists: - `src/anamnesis/body/agent.py:2331` - duplicate inline dispatch remains in main tool loop: - `src/anamnesis/body/agent.py:3170-3645` - `_execute_single_tool` still has no call sites in repo search. 5. Hardcoded operator paths/IDs remain: - `src/anamnesis/body/heartbeat.py:775-776` - `src/anamnesis/body/signal_daemon.py:39-45` - `src/anamnesis/brain/autonomy/capabilities.py:87-90` - `src/anamnesis/body/claude_code_tool.py:49-52` ### Reconfirmed decision The plan in Section 9 still makes sense as written. No phase should be removed. Only adjustment: in Phase 1, include sanitizer metadata (`sanitize_reasons`) in the typed pulse outcome so heartbeat can reason about post-processed vs raw DMN output when deciding completion semantics.