79 lines
4.2 KiB
Markdown
79 lines
4.2 KiB
Markdown
# 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 pickup** — `tasks-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
|