Commit 76b90e8
authored
fix: background reliability, TUI/agent loop, and CodeRabbit hardening (#13)
* fix(todo): parse JSON-encoded string passed as todos list
LLMs occasionally serialize the todos array as a JSON string instead
of a proper JSON array, causing Pydantic validation to fail with
"Input should be a valid list". Add a before-validator that transparently
parses the string via json.loads when detected.
* fix: harden background task sync and reliability across subsystems
Validated subset of the TUI/background-agent reliability scan. Changes:
- background: serialise the runtime read-modify-write in every _mark_task_*
and in recover() under a cross-process per-task lock (store._runtime_lock
+ _write_runtime_unlocked) so a worker heartbeat landing mid-sequence is no
longer lost; add a SIGTERM->SIGKILL escalation fallback in
_best_effort_kill that only fires if the task is still running; cap a bash
task's output.log at config.background.max_output_bytes (default 50 MiB)
so a chatty task cannot exhaust disk.
- pythinker-host: skip the process-group kill once the child has exited and
been reaped, since the OS may have recycled its pid/pgid.
- web fetch: follow redirects manually and re-validate every hop against the
SSRF guard, closing a public->link-local (metadata endpoint) redirect
bypass.
- grep fallback: bound the pure-Python search with a wall-clock deadline
mirroring the ripgrep timeout and report partial results.
- browser launch: route OAuth/feedback URL opens through a detached
open_url_in_browser() so browser chatter cannot corrupt the TUI or consume
key presses meant for it.
- cli: restore the terminal to a sane state on SIGTERM/SIGQUIT and via
atexit.
- live view: use the theme "warning" token instead of a hardcoded accent.
- mcp/toolset: annotate the fastmcp OAuth provider as Any for pyright.
Tests added/updated across the background, tools, ui, and host suites.
* fix(background): crash-consistent agent-task status (H2) + prune aged terminal tasks (M9)
H2: route every terminal agent-task update through one
BackgroundTaskManager.finalize_agent_task() that writes the authoritative
TaskRuntime first and the derived subagent record last, replacing the eight
ad-hoc (update_instance, _mark_task_*) pairs in BackgroundAgentRunner whose
ordering was inconsistent (run() wrote the record first, _run_core the task).
recover() now reconciles a subagent record still stuck at running_background to
the status implied by the authoritative TaskRuntime — for terminal tasks too —
closing the crash/kill window that left TaskRuntime and AgentInstanceRecord
divergent. A resumed running_foreground or already-terminal record is never
clobbered.
M9: prune terminal background-task directories older than
config.background.task_retention_days (default 7) opportunistically during
reconcile(); never removes non-terminal tasks or tasks whose worker is alive.
Tests: crash/kill reconciliation, foreground no-clobber, finalize end-state,
reconcile pruning, and cleanup_old_tasks unit coverage.
* fix(background): don't reconcile a live agent's record off a stale terminal task
The H2 recover() reconciliation could corrupt a currently-running agent. When an
agent_id is reused — a background resume mints a new task_id while the prior run's
task stays terminal in the store — recover() saw the old terminal task alongside a
running_background record and reset the live agent's record to the old task's
terminal status. AgentInstanceRecord.last_task_id is not maintained, so gate the
reconcile on the set of agent_ids owned by live in-process tasks and skip those.
Regression test: old terminal task + live resumed task sharing one agent_id; the
live agent's running_background record must survive recover().
* fix(tui): stop rendering the todo list twice during an in-flight turn
When a turn is in flight the pinned status tail already renders the todo
list under its verb spinner. The background-task status line was reading
the same `_latest_todos` and appending the rows again, so the list showed
twice while the agent worked. Restrict the duplicated rows to the
between-turns case (show_verb=True) where the background line is the only
surface; suppress them when the pinned tail is active.
* fix(background): steer away from blocking on one task when siblings run
Blocking on a single task with TaskOutput(block=true) waits only for that
task and freezes the turn until the slowest sibling finishes, so
completion notifications for the others land with no listener. Add that
guidance to the TaskOutput tool description, the Agent tool's next_step
hints, and the idle-completion system-reminder (which now reports how many
background tasks are still running). Steers the model to return control
and rely on automatic re-wake instead.
* fix(soul): nudge once when a turn ends on a bare statement of intent
Models sometimes end a message with a transitional preamble ("Let me
synthesize the findings into a unified report.") but attach no tool call
and produce no result. The loop treats any tool-call-free message as the
final answer, so the turn ends before the promised work is done. Detect
that shape conservatively and inject a one-shot system-reminder asking the
model to deliver the result or make the tool call. Capped at once per turn
so a stubborn model can still finish.
* fix: address CodeRabbit review findings across subsystems
- background/manager: re-read TaskControl inside the recovery lock so a
kill landing between list_views() and lock acquisition is honored as
killed, not mislabeled lost; derive the subagent record from the runtime
that actually won the terminal race instead of the requested outcome.
- cli: guard SIGQUIT registration behind hasattr — it is POSIX-only and
was crashing CLI startup on Windows.
- grep_local: make _iter_python_search_files lazy so the per-file timeout
check fires during discovery instead of after the whole tree is walked.
- web/fetch: drop the misleading await on the synchronous response.release().
- ui/shell/slash: add missing return type annotation.
- tests: add a fail-closed validation test for SetTodoList params.
* ci: satisfy spell-check and host formatting gates
- typos: accept the verb stems (prepar/generat/provid/updat/examin/continu)
used as word-prefix alternatives in the unfinished-intent detection regex.
- ruff format packages/pythinker-host/tests/test_local_host.py, which was
left unformatted and failed `make check-pythinker-host` across the matrix.
* ci: fix Windows host pyright and main-package formatting gates
- test_local_host: route POSIX-only os.killpg / signal.SIGKILL through Any
holders so the strict host pyright gate passes on the win32 platform stubs
(the test is already skipped at runtime on Windows). Verified with
`pyright --pythonplatform Windows`.
- ruff format tests/ui_and_conv/test_shell_feedback_slash.py, which failed
the main-package `ruff format --check` gate.
* test: refresh default-config snapshot for new background fields
The default-config dump snapshot was stale: earlier commits added
`task_retention_days` and `max_output_bytes` to BackgroundConfig without
updating tests/core/test_config.py, failing test-pythinker-code across the
matrix. Add both fields in dump order.
* fix: log idle-reminder task-count failures; test grep fallback via public API
Address two CodeRabbit review findings on PR #13:
- ui/shell: replace contextlib.suppress(Exception) around the idle-reminder
active-task count with a try/except that logs at debug (active_running still
falls back to 0), so background-task introspection failures are no longer
invisible.
- tests/tools/test_grep: exercise the Python fallback's wall-clock bound through
the public Grep() API with a forced ripgrep-unavailable path instead of
calling _python_grep directly, and drop the now-unused import.1 parent 4253c53 commit 76b90e8
38 files changed
Lines changed: 1545 additions & 206 deletions
File tree
- packages/pythinker-host
- src/pythinker_host
- tests
- src/pythinker_code
- auth
- background
- cli
- soul
- tools
- agent
- background
- file
- todo
- web
- ui/shell
- visualize
- utils
- tests
- background
- core
- tools
- ui_and_conv
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
71 | 76 | | |
72 | 77 | | |
73 | 78 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| 9 | + | |
8 | 10 | | |
9 | 11 | | |
10 | 12 | | |
| |||
196 | 198 | | |
197 | 199 | | |
198 | 200 | | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
178 | 178 | | |
179 | 179 | | |
180 | 180 | | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
181 | 190 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
6 | 5 | | |
7 | 6 | | |
8 | 7 | | |
| |||
150 | 149 | | |
151 | 150 | | |
152 | 151 | | |
153 | | - | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
154 | 155 | | |
155 | 156 | | |
156 | 157 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
14 | 13 | | |
15 | 14 | | |
16 | 15 | | |
| |||
660 | 659 | | |
661 | 660 | | |
662 | 661 | | |
663 | | - | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
664 | 665 | | |
665 | 666 | | |
666 | 667 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
11 | 10 | | |
12 | 11 | | |
13 | 12 | | |
| |||
329 | 328 | | |
330 | 329 | | |
331 | 330 | | |
332 | | - | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
333 | 334 | | |
334 | 335 | | |
335 | 336 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
99 | 99 | | |
100 | 100 | | |
101 | 101 | | |
102 | | - | |
103 | | - | |
104 | | - | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
105 | 107 | | |
106 | 108 | | |
107 | 109 | | |
108 | 110 | | |
109 | 111 | | |
110 | 112 | | |
111 | 113 | | |
112 | | - | |
113 | | - | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
114 | 117 | | |
115 | 118 | | |
116 | | - | |
117 | | - | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
118 | 122 | | |
119 | 123 | | |
120 | 124 | | |
121 | 125 | | |
122 | 126 | | |
123 | 127 | | |
124 | | - | |
125 | | - | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
126 | 131 | | |
127 | 132 | | |
128 | 133 | | |
129 | | - | |
130 | | - | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
131 | 137 | | |
132 | 138 | | |
133 | 139 | | |
| |||
197 | 203 | | |
198 | 204 | | |
199 | 205 | | |
200 | | - | |
201 | | - | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
202 | 209 | | |
203 | 210 | | |
204 | 211 | | |
205 | 212 | | |
206 | 213 | | |
207 | | - | |
208 | | - | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
209 | 219 | | |
210 | | - | |
211 | 220 | | |
212 | 221 | | |
213 | 222 | | |
214 | | - | |
215 | | - | |
| 223 | + | |
216 | 224 | | |
217 | 225 | | |
218 | 226 | | |
| |||
0 commit comments