4.2 KiB
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/
- Capture — the skill calls
create-task.py, which writes the task file intonew/and atomically renames it intoinbox/. 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.pyparsers 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 againstconfig.jsonat capture time (fail-fast in chat) and stores it in themodel:frontmatter field. - Daemon pickup —
tasks-daemon.pyis started by a systemd.pathunit wheneverinbox/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.pathunit re-triggers it after the run if inbox is still non-empty. - Execution — for each file in
inbox/: move torunning/, read frontmatter, callNanobot.run(goal, session_key="detach:<stem>")with a 45-minute timeout in an isolated session. If the frontmatter carriesmodel: <preset>, the daemon switches to it viabot._loop.set_model_preset(preset)before running (the same switch the/modelchat command performs); otherwise the task runs onagents.defaults.modelPreset. - Completion — daemon appends
## Resultand a trailing metadata block (completed,duration_seconds,status) to the file, then moves it todone/(success) orfailed/(exception or timeout). - Notification — daemon sends a Telegram message to
chat_idfrom the frontmatter (or falls back to the firstallowFromID for non-Telegram channels). - Archive — user explicitly calls the
archivesubaction;archive-tasks.pymoves selected files fromdone/toarchive/.
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, runstasks-daemon.pytasks-daemon.path— level-triggered, watchesinbox/, starts the service when non-empty