Files
hqt/docs/superpowers/specs/2026-06-11-delayed-prompt-injection-design.md
T

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, 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:

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:

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.