AGENTS.md@src/core · git:20261004.e7e6481 · 2026-10-04 · sha256 738d4d58d1d2998c
AGENTS.md@src/core git:20261004.e7e6481A
Immutable. This exact content never changes and is served at /api/v1/blob/738d4d58d1d2998c.
# Core Layer — Quick Reference **Full implementation details: `.claude/skills/walnut-core-internals/SKILL.md`** (task model, phase lifecycle, session monitoring, message queue, JSONL patterns, compaction). Load that skill before non-trivial work in `src/core/`. **Decision records live in [`docs/decision/`](../../docs/decision/).** Read the relevant record before rebuilding or "fixing" anything it says was deliberately removed. In particular, read the [self-report](../../docs/decision/summarizer-self-report.md) and [session-end](../../docs/decision/no-session-end-gist.md) decisions before changing turn-complete task updates or session lifecycle hooks. **Search/indexing:** the engine is `src/lib/hybrid-search/` (self-contained library: tokenizer, FTS5 schema, two keyword lanes, vector rescore, embed worker) with `src/core/search/` as the walnut adapter (serializers, event-bus wiring, backfill). Read [`src/lib/hybrid-search/README.md`](../lib/hybrid-search/README.md) before changing interactive search, the tokenizer, embedding models, or the indexing path. The library must keep ZERO walnut imports (`tests/lib/hybrid-search-boundary.test.ts` enforces it). **Task and session status:** read [Task/session status decisions](../../docs/decision/task-session-status.md) before changing phase migrations, cache invalidation, turn settlement, FIFO markers, or snapshot guards. It maps failure modes to code and regression tests. **Inbox Triage (`src/core/triage/`)** starts a NEW task and a NEW session for every run on purpose, and its memory lives in notes rather than in a context window. Read [Inbox Triage](../../docs/plan/inbox-triage.md) before touching the buffer, the envelope, the letter budget or the run bookkeeping: the load-bearing parts are the at-least-once handover (a claim is acknowledged only once the run's TASK exists) and the fact that the State.md check is SOFT and never retries a run that already sent letters. **MCP servers plugins run (`src/core/mcp-servers/`)**: Walnut as an MCP CLIENT (the opposite of `src/mcp/`, Walnut's own server). One process per registered server, started on first use, kept alive, closed when idle. Calls go through the SDK client's low-level `request`, never `callTool`: after a `listTools` the SDK throws on a structured result that breaks its outputSchema, which would turn a message that really posted into a failure. Sessions reach a server only through the `/api/mcp` exposure policy (read-only tools by default); see "MCP servers" in [plugin-development.md](../../docs/reference/plugin-development.md). ## Invariants you must not break (even without reading the skill) - **Daemon-uniform file access (THE one rule):** every read of a Claude Code session-data file (transcripts, subagent jsonl, workflow manifests, plan files, stream copies) goes through `DaemonFileReader(host ?? '__local__')` — local AND remote, no raw `fs`/`ssh` reads. Sanctioned helpers live in `session-file-reader.ts` / `session-history.ts` / `session-changes.ts`. Documented exceptions (partial-read fast paths pending a daemon `fs.readRange`): `team-reader.ts`, `subagent-poller.ts`, `_areTeammatesStillActive()`. - **The server never signals a CLI pid.** A pid in a session record proves nothing about which daemon spawned the process (2026-09-26: an ephemeral test server over a copied production store killed the user's live CLIs). Ending a CLI always goes to the owning daemon's `stop`: `stopThroughOwner` / `sweepOrphansThroughOwner` / `stopAcpThroughOwner` in `sessions/owner-stop.ts`. Every `stop` with reason user, maintenance or idle spreads `stopProvenance(conn, initiator)` (`sessions/stop-provenance.ts`: `home`, `initiator`, and `strict` from an ephemeral server) so the daemon can refuse another Walnut's session. The orphan `stop` sends `home` alone, with `expectPid`: the daemon checks it against its own registry, `.pgid` file and journal (`orphanStopRefusal`). `acpStop` carries only the runtime id, and an ephemeral server sends it to its local daemon only. Never add a `process.kill` / `safeKillProcessGroup` on a pid read from the sessions store (`tests/core/signal-call-ratchet.test.ts` fails on any new signal site). - **Daemon socket writes:** every daemon→client WS write in `daemon-standalone.ts` MUST use `safeSend` (Bun silently drops sends under backpressure). Plain-Node `daemon-source.ts` doesn't need it — see the PARITY NOTE there. - **Canonical JSONL** (`~/.claude/projects/<cwd>/<sid>.jsonl`) is owned by Claude Code — Walnut never writes to it. Walnut's own copy is the streams-dir jsonl. - Task `phase` is the source of truth; `status` is derived (`applyPhase()` mutates both). - Child tasks (`parent_task_id`) are the canonical subtask model; embedded `task.subtasks` is legacy — do not extend.