Files
nanobot-runtime/skills/detach/architecture.md
2026-06-10 06:39:52 +02:00

4.2 KiB

Detach skill — architecture

Directory layout

~/.nanobot/workspace/tasks/
  inbox/    — tasks waiting to be picked up (written atomically from new/)
  running/  — task currently executing
  done/     — completed tasks (success)
  failed/   — completed tasks (exception or timeout)
  archive/  — tasks moved out of the active view; no longer shown by list
  new/      — atomic write staging: skill writes here, then renames into inbox/

Task lifecycle

capture (skill) → inbox/ → running/ → done/ or failed/ → archive/
  1. Capture — the skill calls create-task.py, which writes the task file into new/ and atomically renames it into inbox/. This rename is the trigger for the daemon. The filename timestamp uses microsecond precision (%Y-%m-%d_%H_%M_%S_%f, e.g. 2026-06-07_15_00_00_123456-slug.md), making filename collisions impossible even for simultaneous calls with the same slug. tasks_common.py parsers accept both the old second-precision format (T-joined, e.g. 2026-06-07T150000) and the new underscore format for backward compatibility with existing task files. When --model <token> is given, the script fuzzy-resolves it to an exact preset against config.json at capture time (fail-fast in chat) and stores it in the model: frontmatter field.
  2. Daemon pickuptasks-daemon.py is started by a systemd .path unit whenever inbox/ is non-empty. It processes all files in one pass (Type=oneshot). Concurrency is handled by systemd: the service won't start again while the previous run is still live; the level-triggered .path unit re-triggers it after the run if inbox is still non-empty.
  3. Execution — for each file in inbox/: move to running/, read frontmatter, call Nanobot.run(goal, session_key="detach:<stem>") with a 45-minute timeout in an isolated session. If the frontmatter carries model: <preset>, the daemon switches to it via bot._loop.set_model_preset(preset) before running (the same switch the /model chat command performs); otherwise the task runs on agents.defaults.modelPreset.
  4. Completion — daemon appends ## Result and a trailing metadata block (completed, duration_seconds, status) to the file, then moves it to done/ (success) or failed/ (exception or timeout).
  5. Notification — daemon sends a Telegram message to chat_id from the frontmatter (or falls back to the first allowFrom ID for non-Telegram channels).
  6. Archive — user explicitly calls the archive subaction; archive-tasks.py moves selected files from done/ to archive/.

File format

Each task is a single Markdown file:

---
created: <ISO 8601>
channel: telegram | websocket | ...
chat_id: "<id>"
slug: <kebab-case>
model: <preset>          # optional; omitted → agent default
---

# Goal

<self-contained goal text>

# Constraints

- No user interaction (isolated session, no clarification questions — work with what you have).
- <optional extra constraints>

# Result

<appended by daemon after completion>

---
completed: <ISO 8601>
duration_seconds: <int>
status: done | failed

The daemon appends the # Result section and the trailing --- block; everything before that is written by create-task.py at capture time.

Scripts

Script Role
create-task.py Capture: writes task file, ensures queue dirs, atomically moves to inbox/; logs CREATE to detach.log
tasks-daemon.py Long-running one-shot systemd service; executes tasks, notifies via Telegram; logs lifecycle events to detach.log
list-tasks.py List running/, done/, failed/ (capped at 10 newest each) as a flat bullet list, one task per bullet (slug · time · age + indented goal)
read-task.py Format and print a completed task's result
archive-tasks.py Move tasks from done/ to archive/ (by slug or all); logs ARCHIVE to detach.log
tasks_common.py Shared stdlib helpers: paths, parsers, formatters, model-preset resolution, shared log()~/.nanobot/workspace/log/detach.log

Systemd units

Two user-level units under ~/.config/systemd/user/:

  • tasks-daemon.service — Type=oneshot, runs tasks-daemon.py
  • tasks-daemon.path — level-triggered, watches inbox/, starts the service when non-empty