Self-recall (scheduled callback) system — allows the agent to schedule future headless work that runs without user interaction.
A recall is a delayed or recurring task that the agent schedules for itself. When the trigger time arrives, the agent wakes up, reads its self-instruction, and continues working using other tools. The user can watch the execution live via WebSocket streaming.
Use cases:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ schedule_recall │────▶│ PostgreSQL │────▶│ recall loop │
│ manage_recall │ │ session_recalls │ │ (poll + fire) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Agent.run_stream │
│ (headless) │
└─────────────────┘
│
▼
┌─────────────────┐
│ WebSocket │
│ streaming to UI │
└─────────────────┘
Table: session_recalls
| Column | Type | Description |
|---|---|---|
id |
TEXT PK |
UUID |
session_id |
TEXT FK |
References sessions(id) ON DELETE CASCADE |
call_type |
TEXT |
once, recurring, immediate |
trigger_at |
TIMESTAMPTZ |
When to fire (UTC) |
interval_seconds |
INTEGER |
Repeat interval (required for recurring) |
internal_comment |
TEXT |
Human-readable note |
additional_context_message |
TEXT |
Self-instruction injected as system message |
status |
TEXT |
pending, fired, cancelled |
created_at |
TIMESTAMPTZ |
Creation time |
updated_at |
TIMESTAMPTZ |
Last mutation |
Constraints:
session_id WHERE status = 'pending').immediate recalls fire ASAP — trigger_at is set to now().schedule_recallParameters:
| Param | Type | Required | Description |
|---|---|---|---|
call_type |
string |
yes | once, recurring, immediate |
when |
string |
for once/recurring | Trigger time. Relative formats preferred: 30m, 2h 15m, in 3 hours, tomorrow at 09:00. ISO 8601 only if timezone is known. |
timezone_offset |
string |
optional | ±HH:MM — required for absolute times if user's timezone is known |
interval_seconds |
integer |
for recurring | Repeat interval in seconds |
internal_comment |
string |
optional | Human-readable note |
additional_context_message |
string |
yes | Self-instruction — write it as a todo for yourself |
Rules for additional_context_message:
"Tell the user that 2 hours have passed""Read /tmp/build.log with filesystem. If errors, read last 50 lines and report."manage_recallParameters:
| Param | Type | Required | Description |
|---|---|---|---|
action |
string |
yes | cancel, skip, list |
session_id |
string |
optional | Target session (defaults to current) |
Actions:
cancel — deletes the pending recall. Standard pattern before scheduling a new one.skip — advances a recurring recall by interval_seconds. Uses GREATEST(trigger_at, now) to avoid scheduling in the past.list — shows all recalls (pending/fired/cancelled) for the session.navi/core/scheduler.py::recall_scheduler_loop() — background task that:
trigger_at <= now()_fire_recall()_fire_recall behavior_runs), reschedule +60 scancelledcurrent_user_id, current_user_role, current_user_info so tools work correctly_busy_sessions (dict of session_id → asyncio.Event)stream_start — clients begin showing the streaming messageagent.run_stream(is_recall=True) — stream all events to EventBus + WebSocket + replay bufferfired (once) or reschedule (recurring), publish RecallUpdateMaxIterationsReached: treated as success, not failurecancelled (once) or reschedule (recurring)_busy_sessions and _runs, send session_syncPOST /sessions/{id}/stop signals the recall's stop_event (same mechanism as stopping a normal run).
During a headless recall run, the client receives the same events as a normal chat:
stream_start thinking_delta tool_started tool_call stream_delta stream_end session_sync
Plus out-of-run recall_update events whenever recall state changes.
chatStore.recall is set. Displays trigger time + Cancel/Skip buttons.is_recall=true styled differently (light text, clock badge).recall_update WebSocket events.See api.md for full schemas:
GET /sessions/{id}/recall — get pending recallDELETE /sessions/{id}/recall — cancelPOST /sessions/{id}/recall/skip — skip next recurring occurrence| File | Role |
|---|---|
navi/core/scheduler.py |
RecallScheduler, DDL, loop, _fire_recall |
navi/tools/schedule_recall.py |
ScheduleRecallTool |
navi/tools/manage_recall.py |
ManageRecallTool |
navi/tools/_internal/time_parser.py |
parse_when() — natural language time parser |
navi/api/routes/sessions.py |
REST endpoints for recall CRUD |
navi/api/websocket.py |
_busy_sessions, _notify_session, _on_recall_update |
navi/core/events.py |
RecallUpdate event class |
navi/llm/base.py |
Message.is_recall flag |