docs: design delayed prompt injection
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
# Delayed prompt injection
|
||||
|
||||
**Date:** 2026-06-11
|
||||
**Status:** Approved
|
||||
|
||||
## Goal
|
||||
|
||||
Add a TUI-only feature that schedules one delayed prompt for a specific hqt
|
||||
session and injects it when the selected time arrives. The primary use case is
|
||||
recovering a harness that stopped because it ran out of budget: the user can
|
||||
schedule a restart/continue prompt and let hqt deliver it later.
|
||||
|
||||
Scheduled prompts fire only while the hqt TUI is running. If hqt is closed at
|
||||
the due time, the prompt remains pending and fires on the next TUI poll after
|
||||
hqt starts again.
|
||||
|
||||
## User Behavior
|
||||
|
||||
The session list gets a new keybinding, `p`, labeled "Prompt Later". Pressing
|
||||
it for the selected session opens a modal that collects:
|
||||
|
||||
- Prompt text.
|
||||
- A relative delay such as `30m`, `2h`, `1h30m`, or `90s`.
|
||||
- A confirmation action.
|
||||
|
||||
Each session supports exactly one active delayed prompt. Scheduling a new prompt
|
||||
for that session replaces the existing pending, sent, or failed prompt. A
|
||||
companion cancel action, `P` / `Shift+P`, removes the selected session's active
|
||||
prompt.
|
||||
|
||||
Visualization is intentionally compact:
|
||||
|
||||
- The session row shows the next pending trigger, for example
|
||||
`◉ waiting @ 14:30` or `● idle @ 12m`.
|
||||
- After scheduling, canceling, sending, or failing a prompt, the TUI shows a
|
||||
short notification such as `Prompt scheduled for hqt-4 at 14:30`.
|
||||
|
||||
The row indicator is shown only for pending prompts. Sent prompts disappear from
|
||||
the active indicator. Failed prompts are not retried automatically; they can be
|
||||
replaced or canceled from the selected session.
|
||||
|
||||
## Architecture
|
||||
|
||||
Use a persisted database table and drive dispatch from the existing TUI poller.
|
||||
This matches the current service architecture and keeps scheduling state visible
|
||||
across TUI restarts without introducing a background daemon.
|
||||
|
||||
### Data Model
|
||||
|
||||
Add a `ScheduledPrompt` model and migration:
|
||||
|
||||
- `id` primary key.
|
||||
- `session_id` foreign key to `sessions.id`, unique.
|
||||
- `prompt` text, required.
|
||||
- `due_at` datetime, required.
|
||||
- `status` string, one of `pending`, `sending`, `sent`, or `failed`.
|
||||
- `error` text, nullable.
|
||||
- `created_at` datetime.
|
||||
- `sent_at` datetime, nullable.
|
||||
|
||||
The unique `session_id` constraint enforces one active prompt record per
|
||||
session. Scheduling a prompt replaces any existing prompt row for that session,
|
||||
including failed or sent rows.
|
||||
|
||||
### Service Layer
|
||||
|
||||
`SessionService` owns delayed prompt behavior because it already owns session
|
||||
lookup, resume/fallback behavior, status polling, and tmux interaction.
|
||||
|
||||
Add service methods along these lines:
|
||||
|
||||
```python
|
||||
def schedule_prompt(self, session_id: int, prompt: str, due_at: datetime) -> ScheduledPrompt:
|
||||
...
|
||||
|
||||
def cancel_scheduled_prompt(self, session_id: int) -> None:
|
||||
...
|
||||
|
||||
def get_scheduled_prompt(self, session_id: int) -> ScheduledPrompt | None:
|
||||
...
|
||||
|
||||
async def dispatch_due_prompts(self, now: datetime) -> list[DispatchResult]:
|
||||
...
|
||||
```
|
||||
|
||||
Extend `SessionInfo` with an optional `scheduled_prompt` field so `SessionList`
|
||||
can render the compact row indicator from the same object it already receives
|
||||
for each row.
|
||||
|
||||
### Tmux Layer
|
||||
|
||||
`TmuxRunner` already has literal `send_text()` and `send_enter()` methods. Add
|
||||
thin `TmuxManager` delegates and make both send paths return success/failure
|
||||
instead of fire-and-forget so the service can record failed injection.
|
||||
|
||||
Prompt injection sends the prompt body as literal text, then sends Enter as a
|
||||
separate tmux key event. Prompt content such as `-flag`, quotes, semicolons, or
|
||||
the word `Enter` must be treated as text, not as tmux commands or key names.
|
||||
|
||||
### TUI Runtime
|
||||
|
||||
The existing 3-second TUI poll loop becomes the scheduler runtime:
|
||||
|
||||
1. Call `dispatch_due_prompts(now)`.
|
||||
2. Surface any sent/failed dispatch results as notifications.
|
||||
3. Refresh session rows and tmux labels using the existing status flow.
|
||||
|
||||
No prompt fires while the TUI process is not running. Overdue prompts fire on
|
||||
the first poll after startup.
|
||||
|
||||
## Dispatch Semantics
|
||||
|
||||
When a pending prompt is due:
|
||||
|
||||
1. Load the target session row.
|
||||
2. Change the prompt status to `sending` and commit before interacting with
|
||||
tmux, so overlapping poll workers cannot inject the same prompt twice.
|
||||
3. If the session row is gone, mark the prompt `failed`.
|
||||
4. If the tmux window is alive, inject directly.
|
||||
5. If the window is dead or missing, resume the session without switching tmux
|
||||
focus, then inject.
|
||||
6. On successful text + Enter injection, mark the row `sent` and set `sent_at`.
|
||||
7. On any failure, mark the row `failed` and store an error message.
|
||||
|
||||
The current `attach_session()` method resumes and then selects the tmux window.
|
||||
Factor an internal helper, `_ensure_session_alive(db, sess) -> bool`, so due
|
||||
prompt dispatch can reuse the resume/fallback ladder without changing the
|
||||
user's active tmux window.
|
||||
|
||||
Failed prompts must not retry automatically on every poll. This avoids sending
|
||||
duplicate prompts or repeatedly restarting a harness after a transient failure.
|
||||
The user can replace the failed prompt by scheduling a new one, or clear it with
|
||||
the cancel action.
|
||||
|
||||
## TUI Details
|
||||
|
||||
Add a new modal screen, `SchedulePromptScreen`, under `src/hqt/tui/screens/`.
|
||||
It should follow existing modal conventions from the project:
|
||||
|
||||
- Plain Textual form controls.
|
||||
- Compact dialog styling using existing CSS patterns.
|
||||
- Inline validation for empty prompt or invalid delay.
|
||||
- OK and Cancel actions.
|
||||
|
||||
Delay parsing is relative-only for the first version. Accepted units are
|
||||
seconds, minutes, and hours. Combined values such as `1h30m` are accepted.
|
||||
Absolute timestamps, recurring prompts, and multiple queued prompts are out of
|
||||
scope.
|
||||
|
||||
`SessionList` row rendering includes pending prompt information when present.
|
||||
Show `@ <countdown>` while the due time is less than 60 minutes away, rounded
|
||||
up to whole minutes, for example `@ 12m`. Show local 24-hour time for prompts
|
||||
that are at least 60 minutes away, for example `@ 14:30`.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Scheduling with no selected session shows a warning notification.
|
||||
- Empty prompt text or invalid delay keeps the modal open with an inline error.
|
||||
- Scheduling for a deleted session raises `ServiceError` and shows a TUI error.
|
||||
- Deleted target session at dispatch time marks the prompt failed.
|
||||
- Resume/fallback failure marks the prompt failed with the existing tmux/spawn
|
||||
error when available.
|
||||
- Text or Enter injection failure marks the prompt failed.
|
||||
- Database errors are allowed to surface through existing service error handling.
|
||||
|
||||
## Testing
|
||||
|
||||
Add focused tests at the same layers the current code uses:
|
||||
|
||||
- Model/migration test for the `scheduled_prompts` table and unique
|
||||
`session_id`.
|
||||
- Service tests for schedule, replace, cancel, and retrieving pending prompt
|
||||
metadata in session rows.
|
||||
- Service dispatch tests for alive-window injection, dead-window auto-resume
|
||||
without attach/select, missing session failure, injection failure, and failed
|
||||
prompts not retrying on the next poll.
|
||||
- Tmux tests for manager send delegates and literal send behavior returning
|
||||
success/failure.
|
||||
- TUI tests for the keybindings, modal validation, scheduling action, cancel
|
||||
action, row indicator, and poll-time dispatch notifications.
|
||||
|
||||
After implementation changes, the project checks must pass:
|
||||
|
||||
```bash
|
||||
uv run ruff format src tests
|
||||
uv run ruff check src tests
|
||||
uv run ty check
|
||||
```
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Firing delayed prompts while hqt is closed.
|
||||
- CLI scheduling.
|
||||
- Multiple queued prompts per session.
|
||||
- Absolute date/time scheduling.
|
||||
- Recurring prompts.
|
||||
- Prompt history or a full queue-management screen.
|
||||
- Harness-specific prompt templates.
|
||||
Reference in New Issue
Block a user