Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/lingtai/tools/daemon/ANATOMY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ related_files:
- src/lingtai/mcp_servers/daemon_common/server.py
- tests/test_daemon.py
- tests/test_daemon_run_dir.py
- tests/test_daemon_checkpoint.py
- tests/test_daemon_codex_usage.py
- tests/test_codex_standalone_compaction.py
- tests/test_daemon_detached_supervisor.py
Expand Down Expand Up @@ -59,7 +60,7 @@ contract lives in `CONTRACT.md`. MCP-capable backends also get the built-in
`daemon_common` MCP (`src/lingtai/mcp_servers/daemon_common/server.py:1-151`);
its `finish` tool writes `daemon_completion.json`, and only a validated
`finish(status="done")` allows terminal `done`. Parent MCP tools are not
auto-inherited. The daemon-eligible `email` intrinsic is available only when explicitly requested in a task's `tools` list, and daemon tool
auto-inherited. The daemon-eligible `email` intrinsic is available only when explicitly requested in a task's `tools` list, and each LingTai-backend run receives a private `checkpoint` tool only after its exact `DaemonRunDir` exists (`daemon/__init__.py:2529-2607`); `checkpoint` is not part of `_daemon_intrinsic_surface()` or any external CLI backend surface. Daemon tool
calls still pass through the kernel `ToolExecutor`/`ToolCallGuard` path before
any handler runs. Each `daemon.emanate` batch gets a stable `group_id` shared by
every run in that batch, while each daemon still keeps its own `run_id`. Each
Expand Down Expand Up @@ -92,12 +93,12 @@ polling.

## Components

- `daemon/__init__.py` — public capability surface. `get_description`, `get_schema`, and `setup`; the core class is `DaemonManager`, which manages the full emanation lifecycle and parent-stop cleanup. Key internals: `_ToolCollector` (`daemon/__init__.py:743`) intercepts `add_tool` calls during preset-driven capability setup to build a sandboxed tool surface without mutating the parent's registry. `EMANATION_BLACKLIST` (`daemon/__init__.py:65`) prevents recursion by blocking `daemon`, `avatar`, `psyche`, `skills`, and `knowledge`; `_parent_host_tool_floor()` (`daemon/__init__.py:76`) derives the NARROW set of always-on host tools a preset emanation may borrow from the parent — exactly `shell` + the file group (read/write/edit/glob/grep), derived from `CORE_DEFAULTS − EMANATION_BLACKLIST − {mcp}`; optional/provider parent tools (`vision`, `web_search`) are deliberately excluded so a preset that omits/fails a provider cap does not silently fall back to the parent; `_reconcile_terminal_notifications()` (`daemon/__init__.py:1116-1151`) retries terminal run dirs whose published receipt is missing, after stale-parent reaping; `_write_kimicode_mcp_config()` (`daemon/__init__.py:309-337`) writes the run-private Kimi `mcp.json` for stdio and HTTP registrations; `_daemon_intrinsic_surface()` (`daemon/__init__.py:1207`) is the narrow daemon intrinsic bridge for explicitly requested `email` plus the automatic LingTai `compact` schema; `_task_mcp_registrations()` (`daemon/__init__.py:1296`) validates full per-task MCP registrations and renders prompt-safe YAML; `_daemon_common_mcp_registration()` / `_read_daemon_completion()` (`daemon/__init__.py:1390` / `daemon/__init__.py:1460`) add and validate the built-in completion MCP; `_connect_task_mcp_registrations()` (`daemon/__init__.py:1550`) starts task-scoped MCP clients for the LingTai backend; `_daemon_provider_defaults()` (`daemon/__init__.py:1695`) preserves the parent/preset provider defaults for every provider and adds the per-run `daemon.json` cache anchor only for Codex; `_build_tool_surface()` (`daemon/__init__.py:1786`) automatically includes `compact`, includes only task-scoped MCP tools rather than auto-inheriting parent MCP tools, and — for preset-driven emanations — preserves the parent's narrow host tool floor (`_parent_host_tool_floor()`: shell + file primitives) that saved presets omit from `manifest.capabilities`, so requested host tools valid in the parent set are not rejected as unknown (resolution is preset-first, then intrinsics/MCP, then parent host fill-in); `_run_emanation()` (`daemon/__init__.py:2074`) is the LingTai-backend worker loop that builds a fresh daemon-scoped service for every no-preset run instead of reusing the parent service, sends LingTai `prompt` (or its exact default) as the first ordinary user message, performs sole-call provider-independent `compact` resets while retaining the exact call/result pair, and enforces `finish(done)` before `mark_done`.
- `daemon/__init__.py` — public capability surface. `get_description`, `get_schema`, and `setup`; the core class is `DaemonManager`, which manages the full emanation lifecycle and parent-stop cleanup. Key internals: `_ToolCollector` (`daemon/__init__.py:743`) intercepts `add_tool` calls during preset-driven capability setup to build a sandboxed tool surface without mutating the parent's registry. `EMANATION_BLACKLIST` (`daemon/__init__.py:65`) prevents recursion by blocking `daemon`, `avatar`, `psyche`, `skills`, and `knowledge`; `_parent_host_tool_floor()` (`daemon/__init__.py:76`) derives the NARROW set of always-on host tools a preset emanation may borrow from the parent — exactly `shell` + the file group (read/write/edit/glob/grep), derived from `CORE_DEFAULTS − EMANATION_BLACKLIST − {mcp}`; optional/provider parent tools (`vision`, `web_search`) are deliberately excluded so a preset that omits/fails a provider cap does not silently fall back to the parent; `_reconcile_terminal_notifications()` (`daemon/__init__.py:1116-1151`) retries terminal run dirs whose published receipt is missing, after stale-parent reaping; `_write_kimicode_mcp_config()` (`daemon/__init__.py:309-337`) writes the run-private Kimi `mcp.json` for stdio and HTTP registrations; `_daemon_intrinsic_surface()` (`daemon/__init__.py:1207`) is the narrow daemon intrinsic bridge for explicitly requested `email` plus the automatic LingTai `compact` schema; `_task_mcp_registrations()` (`daemon/__init__.py:1296`) validates full per-task MCP registrations and renders prompt-safe YAML; `_daemon_common_mcp_registration()` / `_read_daemon_completion()` (`daemon/__init__.py:1390` / `daemon/__init__.py:1460`) add and validate the built-in completion MCP; `_connect_task_mcp_registrations()` (`daemon/__init__.py:1550`) starts task-scoped MCP clients for the LingTai backend; `_daemon_provider_defaults()` (`daemon/__init__.py:1695`) preserves the parent/preset provider defaults for every provider and adds the per-run `daemon.json` cache anchor only for Codex; `_build_tool_surface()` (`daemon/__init__.py:1786`) automatically includes `compact`, includes only task-scoped MCP tools rather than auto-inheriting parent MCP tools, and — for preset-driven emanations — preserves the parent's narrow host tool floor (`_parent_host_tool_floor()`: shell + file primitives) that saved presets omit from `manifest.capabilities`, so requested host tools valid in the parent set are not rejected as unknown (resolution is preset-first, then intrinsics/MCP, then parent host fill-in); `_run_emanation()` (`daemon/__init__.py:2446`) is the LingTai-backend worker loop that builds a fresh daemon-scoped service for every no-preset run instead of reusing the parent service, binds the run-scoped internal `checkpoint` tool after `run_dir` is known (`daemon/__init__.py:2529-2607`), sends LingTai `prompt` (or its exact default) as the first ordinary user message, performs sole-call provider-independent `compact` resets while retaining the exact call/result pair, reconstructs compact sessions with the same run-scoped tools, and enforces `finish(done)` before `mark_done`.

- `daemon/claude_interactive.py` — interactive Claude Code daemon backend. **Hidden / not user-selectable:** `claude` / `claude-interactive` were removed from the `get_schema()` `backend` enum and description (`daemon/__init__.py:889-904`); the code path stays only so older callers and stored daemon entries with `backend="claude"` still resolve through `_normalize_backend`/dispatch. New work uses print-mode `claude-p`. This retained PTY bridge is POSIX-only;
Windows headless composition never selects it, and native interactive support
remains deferred until ConPTY has its own accepted adapter. `ClaudeInteractiveBridge` (`daemon/claude_interactive.py:103`) accepts only the persistent Port injected by runtime composition; it never constructs a private adapter and rejects missing injection before managed-workspace, harness, or spawn work. It runs normal interactive `claude` under a PTY from a LingTai-managed workspace, writes the managed system prompt (`daemon/claude_interactive.py:80-96`), prepares empty or explicit-source detached worktrees (`daemon/claude_interactive.py:250-309`), answers terminal probes, injects `SessionStart`/`Stop` hooks via inline `--settings`, relays hook payloads through a FIFO, auto-selects workspace trust only inside the verified managed root (`daemon/claude_interactive.py:535-559`), and parses Claude transcript JSONL into daemon progress/result state.
- `daemon/run_dir.py` — per-emanation filesystem run directory. `DaemonRunDir` owns every filesystem effect for one run: folder layout, `daemon.json` atomic writes, batch `group_id` metadata (`DaemonRunDir.new_group_id()`), JSONL appends, CLI progress events, heartbeat touches, `result.txt`, versioned `daemon.json` data (`data_version`), visible `call_parameters`, terminal state markers, and backend resume-id persistence (`DaemonRunDir.set_session_id(key, value, *, overwrite)` — empty/duplicate values are no-ops returning `False`; `overwrite=False` keeps the first id for OpenCode-family backends; write failures propagate). The `DaemonManager` calls into a `DaemonRunDir` at every lifecycle hook without itself touching the filesystem.
- `daemon/run_dir.py` — per-emanation filesystem run directory. `DaemonRunDir` owns every filesystem effect for one run: folder layout, `daemon.json` atomic writes, batch `group_id` metadata (`DaemonRunDir.new_group_id()`), JSONL appends, CLI progress events, heartbeat touches, `result.txt`, versioned `daemon.json` data (`data_version`), visible `call_parameters`, terminal state markers, bounded semantic checkpoint constants (`daemon/run_dir.py:52-61`), `record_checkpoint()` (`daemon/run_dir.py:622-663`) for atomic `latest_checkpoint` sequence persistence plus best-effort checkpoint evidence/heartbeat writes, and backend resume-id persistence (`DaemonRunDir.set_session_id(key, value, *, overwrite)` — empty/duplicate values are no-ops returning `False`; `overwrite=False` keeps the first id for OpenCode-family backends; write failures propagate). The `DaemonManager` calls into a `DaemonRunDir` at every lifecycle hook without itself touching the filesystem.
- `adapters/posix/daemon_execution_child_entrypoint.py` — fresh-interpreter execution boundary launched by the supervisor before its watcher. It registers exact execution PID/PGID/start identity and composes the existing production host; it does not duplicate backend parsers. `daemon_resume_owner_entrypoint.py` provides the bounded detached owner for one terminal supported-CLI resume generation. Durable `resume-claims/` records enforce one writer, and `followups/` plus `daemon.json` expose follow-up truth to `daemon(check)`.
- `adapters/posix/process_identity.py` — shared POSIX process-incarnation helper. Linux identities combine boot ID and `/proc/<pid>/stat` start ticks; Darwin/BSD identities use bounded `ps` start time plus PPID. It returns `None` when observation is unavailable, and all ownership-sensitive signal paths refuse unknown or mismatched identities.
- `daemon/runtime.py` — stateless daemon backend runtime primitives shared by the LingTai in-process loop and transitional CLI runners. Port-owned Codex, Cursor, OpenCode-family, Qwen, and Kimi runners use the daemon-local process Port for stderr draining and stdout iteration; only ask workers use a local deadline, while initial streams remain watchdog-owned. The historical private names remain for unmigrated paths and compatibility tests.
Expand Down Expand Up @@ -139,7 +140,7 @@ The `daemon` tool exposes five actions:
| `emanate` | Spawn one or more subagents with specified task + tools + optional preset |
| `list` | List running/completed/failed emanations with status and elapsed time |
| `ask` | Send a follow-up message to a running emanation |
| `check` | Read-only progress tail: `daemon.json` state + last N events from `events.jsonl` + a compact `artifacts` block (the run's artifact manifest — relative path/size/mtime/role per important file, plus run-level state/result_path/error_path). On in-memory registry miss (e.g. after refresh/molt) falls back to the durable `daemons/*/` run dirs, resolving by full `run_id` (exact) or short `handle` (most-recent, with ambiguity flagged) |
| `check` | Read-only progress tail: `daemon.json` state + last N events from `events.jsonl` + a compact `artifacts` block (the run's artifact manifest — relative path/size/mtime/role per important file, plus run-level state/result_path/error_path) + `checkpoint` when `daemon.json.latest_checkpoint` exists (`daemon/__init__.py:7205-7207`). On in-memory registry miss (e.g. after refresh/molt) falls back to the durable `daemons/*/` run dirs, resolving by full `run_id` (exact) or short `handle` (most-recent, with ambiguity flagged) |
| `list` | Progressive-disclosure index: active registry + historical run dirs; lazily rebuilds missing/invalid/stale-version `daemon.json` and returns prompt/result previews with search filters |
| `reclaim` | Cancel all running emanations, shut down CLI process groups/thread pools through the same runtime-shutdown helper used by agent stop, reset ID counter |

Expand All @@ -153,7 +154,7 @@ daemon/__init__.py
├── _build_tool_surface() — auto-includes `compact`, filters other requested tools against blacklist, expands groups, and merges preset/MCP/email surfaces; preset emanations also keep the NARROW parent host floor (`_parent_host_tool_floor()`: shell + file primitives only) so saved presets that omit core caps don't make requested host tools unknown — optional/provider parent tools (vision/web_search) are NOT borrowed and stay unknown unless the preset supplies them
├── _instantiate_preset_capabilities() — sets up preset tool surface in a sandbox
├── _build_emanation_prompt() — composes the kernel prompt, complete task system instruction, selected guidance, and compact schema
├── _run_emanation() — LingTai worker loop: sends `prompt`/default first, runs ToolExecutor/ToolCallGuard, and performs same-run sole-call compact resets while retaining the exact call/result pair
├── _run_emanation() — LingTai worker loop: adds the run-scoped `checkpoint` schema/handler, sends `prompt`/default first, runs ToolExecutor/ToolCallGuard, and performs same-run sole-call compact resets while retaining the exact call/result pair
├── _run_claude_interactive_emanation() — `claude` / `claude-interactive` backend; delegates to `run_claude_interactive()` (`daemon/claude_interactive.py:771`) to create the managed workspace, drive the interactive Claude TUI through PTY + hooks + transcript parsing, and persist managed-workspace state.
├── _run_claude_code_emanation() — `claude-p` / compatibility `claude-code` backend; parses `--output-format stream-json --verbose` print-mode events in real time so `claude_session_id`, per-turn text, and tool_use/tool_result land in DaemonRunDir during the run (vs. post-hoc). Claude Code's own `usage` fields are deliberately NOT forwarded to append_tokens (external billing path; semantics don't match the kernel's adapter accounting); `_normalize_claude_usage` requires the primary `input_tokens` and `output_tokens` fields, defaults the optional cache-read/cache-creation fields to zero, and rejects bool/negative/malformed consumed values (matching Codex/Cursor). The first valid terminal `result` event's usage is buffered — not persisted — until after cancellation/timeout, exit code, `is_error`, and `_require_done_completion` (the daemon_common `finish()` MCP contract — itself a terminal acceptance gate: when the run loaded `daemon_common`, a missing/bad `finish()` call fails the run here, before any usage is persisted or `mark_done` runs) are all classified/passed, then persisted exactly once UI-only to `daemon.json.cli_tokens` via `run_dir.record_cli_tokens` (`cached = cache_read + cache_creation` input tokens). Cancellation/timeout observed before the final acceptance check, a later failure classification, or a missing/bad completion sentinel arriving after the terminal line therefore cannot leave false accepted usage; cancellation published after that acceptance point retains the existing best-effort terminal semantics rather than introducing cross-backend atomic arbitration in this path. `_run_ask_claude_code_stream` (the `daemon(ask)` resume follow-up) applies the same buffer-then-classify ordering against its deadline/exit-code/`is_error` gates — resume has no `_require_done_completion` gate — so the TUI `/daemons` view can show accepted usage without touching either token ledger.
├── _run_codex_emanation() — codex backend; parses `codex exec --json` JSONL events (thread.started → codex_session_id, item.completed → agent_message text, turn.completed → terminal). Symmetric with the claude-code backend. A valid terminal `usage` object is normalized to disjoint input (`input_tokens - cached_input_tokens`, clamped at zero), cached input, and output in UI-only `daemon.json.cli_tokens`; the raw object is retained in a `cli_usage` event, with no token-ledger row and at most one terminal account per stream. Codex receives native config overrides for `daemon_common` plus parent-provided stdio MCP registrations, and `finish(done)` is required before success when daemon_common is loaded.
Expand Down Expand Up @@ -191,6 +192,7 @@ daemon/run_dir.py
├── build_manifest() / write_manifest() — `build_manifest(run_path)` is a classmethod pure read over a run dir: lists path/size/mtime/inferred-role for well-known artifacts (priority order) + extra work-product files (sorted, `.tmp` skipped), capped at `_MANIFEST_MAX_ENTRIES` (records `artifacts_total`/`truncated`), plus run-level `state`/`result_path`/`error_path` (the last set only for failed/timeout/cancelled runs with a result.txt). Used both by `write_manifest()` (instance, called from every terminal marker after daemon.json/result.txt are final, best-effort via `_safe`, persists `artifacts.json`) and by `_artifacts_summary`'s fallback for old/running runs that have no `artifacts.json`. `MANIFEST_VERSION` mirrors `data_version`
├── record_user_send() — appends user-role entry to chat_history.jsonl
├── bump_turn() — marks end of LLM round (daemon.json + chat_history + heartbeat)
├── record_checkpoint() — validates a bounded semantic checkpoint, assigns a monotonic run-local sequence under `_state_transaction`, atomically writes authoritative `daemon.json.latest_checkpoint`, then best-effort appends evidence/touches heartbeat
├── set_current_tool() — marks tool dispatch starting (daemon.json + events + heartbeat)
├── clear_current_tool() — marks tool dispatch finished
├── record_cli_output() — records CLI backend stdout/stderr as cli_output events
Expand Down
Loading