Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions TODOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,57 @@ Start by checking whether `advisor` is missing from `tool_overrides` (if `True`/
**Priority:** P2
**Depends on:** None

## TUI (Ink)

### 8 pre-existing vitest failures across five unrelated subsystems

**What:** `vitest run` in `ui-tui/` fails 8 tests spanning five subsystems that have nothing to do with each other:
- `src/__tests__/createGatewayEventHandler.test.ts` — "anchors inline_diff as its own segment where the edit happened"
- `src/__tests__/createGatewayEventHandler.test.ts` — "keeps verbose result text on inline_diff tool completions"
- `src/__tests__/cursorDriftRegression.test.ts` — "agrees with wrap-ansi at every typing-prefix of the user-reported message"
- `src/__tests__/statusRule.test.ts` — "collapses the context bar to a token count on narrow terminals"
- `src/__tests__/statusRule.test.ts` — "shows every segment on a wide terminal"
- `src/__tests__/useConfigSync.test.ts` — "falls back to kaomoji default when missing or invalid"
- `src/__tests__/useConfigSync.test.ts` — "defaults to kaomoji for missing/unknown values"
- `src/__tests__/virtualHeights.test.ts` — "uses compound user prompt width when estimating user message wrapping"

**Why:** A standing red baseline trains everyone to ignore vitest output, which is exactly how a known-8 becomes a known-12 and a real regression lands unnoticed. The Python suite is at a 0-failure baseline; the TS suite should be too. `cursorDriftRegression` and `virtualHeights` in particular guard real rendering correctness — composer cursor placement and scroll-height estimation — so a silent regression there is user-visible.

**Context:** Discovered pre-existing on 2026-07-30 while shipping the AskUserQuestion picker (branch `feat/ask-user-question-picker`) — verified they fail identically on `main` with that branch's changes stashed, so this predates and is unrelated to that PR. `createGatewayEventHandler.test.ts` is the only one whose source file that branch touched, and it fails the same way without the change.

Likely independent root causes given the spread; treat as five small investigations, not one. `useConfigSync` (2 failures, both about the kaomoji default) and `statusRule` (2 failures) each look like a single cause with two symptoms.

**Effort:** M
**Priority:** P0
**Depends on:** None

### Worker housekeeping stalls for the whole of a long AskUserQuestion dialog

**What:** `_run_worker` (`src/server/agent_server.py`) fires `_deliver_task_notifications()` and `_fire_due_scheduled()` only from its `except _queue.Empty` idle branch. While `ask_user` is parked waiting on the user, the worker is inside `_run_turn`, so neither runs. A user who walks away from a question dialog silences background-agent completion notifications and every scheduled-task/cron tick (`/loop`, `Cron*`) for up to `ask_user_timeout_s` (30 min).

**Why:** Those ticks are documented as "checks every second". 30 minutes is 6x the previous worst case, which was the 300s permission timeout — so this change widened an existing stall rather than creating it, but it widened it a lot. A `/loop` that silently skips half an hour looks like a broken loop, not a busy one.

**Context:** Found 2026-07-31 by the performance specialist during `/ship` of the AskUserQuestion picker (branch `feat/ask-user-question-picker`). Verified: the synchronous `pending.event.wait()` itself is CORRECT and does not block the stdio pump — sync tools dispatch via `asyncio.to_thread` (`tool_execution.py:590`), so the wait lands on a pool thread, and the stdio pump is a separate loop on the main thread. The stall is specifically the worker's idle-branch housekeeping, not the event loop.

Two candidate fixes: wait in short slices (`while not pending.event.wait(1.0) and elapsed < timeout`) pumping housekeeping each slice — but that puts worker concerns inside a session handler; or move the scheduler tick onto its own daemon thread so turn duration stops gating it. The second is cleaner and fixes it for long turns generally, not just dialogs.

**Effort:** M
**Priority:** P2
**Depends on:** None

### Ctrl+C on a prompt overlay leaves the status line stale

**What:** `cancelOverlayFromCtrlC` (`ui-tui/src/app/useInputHandlers.ts`) patches the overlay and the turn outcome but not `status`, while the `actions.answer*` paths also patch `status: 'running…'`. So dismissing an approval, a plan approval, or a question dialog with Ctrl+C leaves the footer showing the waiting-state text (`waiting on your answers` / the approval status) while the turn resumes.

**Why:** Cosmetic, but the footer is the one place that says whether the agent is waiting on you. Saying "waiting on your answers" when it is not is exactly the kind of small lie that trains people to stop reading it.

**Context:** Found 2026-07-31 during `/ship` of the AskUserQuestion picker. PRE-EXISTING pattern, not introduced by that branch — the `approval` and `planApproval` Ctrl+C branches have the identical omission, and the new `questions` branch faithfully copies them. Fix all three together, or route them through their `actions.answer*` callbacks once the `useMainApp` declaration-order constraint noted in the code comment is resolved (the callbacks are declared after `useInputHandlers` is called, which is why the branches use `gateway.rpc` directly).

**Effort:** S
**Priority:** P3
**Depends on:** None

## Completed


</content>
19 changes: 10 additions & 9 deletions src/entrypoints/headless.py
Original file line number Diff line number Diff line change
Expand Up @@ -1007,25 +1007,26 @@ def handler(request: PermissionAskRequest) -> PermissionAskReply:
return handler


_NON_INTERACTIVE_ANSWER = (
"No interactive user is available (running headless/non-interactive). "
"Proceed autonomously with your best judgment and reasonable default "
"assumptions; do not ask again."
)


def _noop_ask_user(questions): # type: ignore[override]
# Non-interactive mode: there is no user to answer. Returning bare
# empty strings left the model with no signal about WHY the answer was
# empty — observed live (terminal-bench raman-fitting) to make it flail,
# re-asking / retrying instead of committing to an approach. Return an
# explicit "proceed autonomously" answer so the model moves on
# decisively. (The interactive TUI still shows the real dialog; only the
# decisively. (The interactive TUI shows the real dialog; only the
# headless surface — which cannot collect input — substitutes this.)
#
# NOT ``None``: that is the DECLINE signal (the user dismissed a dialog),
# which is a different thing from "there was never a dialog".
#
# Local import, deliberately: hoisting this to module scope pulls the tool
# package into headless's import graph at load time, which perturbed
# test_sigterm_triggers_drain under full-suite load. Not worth a style win.
from src.tool_system.tools.ask_user_question import NON_INTERACTIVE_ANSWER
answers: dict = {}
for q in questions or []:
if isinstance(q, dict) and isinstance(q.get("question"), str):
answers[q["question"]] = _NON_INTERACTIVE_ANSWER
answers[q["question"]] = NON_INTERACTIVE_ANSWER
return answers


Expand Down
2 changes: 1 addition & 1 deletion src/reference_data/ts_tool_properties.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"max_result_size_chars": 20000
},
"tool_overrides": {
"AskUserQuestion": {"is_read_only": true, "is_concurrency_safe": true},
"AskUserQuestion": {"is_read_only": true, "is_concurrency_safe": false, "_note": "DELIBERATE DIVERGENCE: TS AskUserQuestionTool.tsx isConcurrencySafe()=true; this port returns false. Upstream serializes interactive dialogs through REPL's toolUseConfirmQueue and renders only the head. This port has no such queue and the TUI's PromptZone is an exclusive if-chain, so a second simultaneous question dialog would be invisible while still blocking its tool. See src/tool_system/tools/ask_user_question.py."},
"Bash": {"is_read_only": false, "is_concurrency_safe": false, "is_destructive": true},
"Brief": {"is_read_only": true, "is_concurrency_safe": true},
"ClipboardRead": {"is_read_only": true, "is_concurrency_safe": true},
Expand Down
Loading
Loading