7.4 KiB
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, or90s. - 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:30or● 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:
idprimary key.session_idforeign key tosessions.id, unique.prompttext, required.due_atdatetime, required.statusstring, one ofpending,sending,sent, orfailed.errortext, nullable.created_atdatetime.sent_atdatetime, 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:
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:
- Call
dispatch_due_prompts(now). - Surface any sent/failed dispatch results as notifications.
- 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:
- Load the target session row.
- Change the prompt status to
sendingand commit before interacting with tmux, so overlapping poll workers cannot inject the same prompt twice. - If the session row is gone, mark the prompt
failed. - If the tmux window is alive, inject directly.
- If the window is dead or missing, resume the session without switching tmux focus, then inject.
- On successful text + Enter injection, mark the row
sentand setsent_at. - On any failure, mark the row
failedand 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
ServiceErrorand 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_promptstable and uniquesession_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:
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.