Repository navigation
webui: stop appending each skill card into its own checkbox #269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # Investigation — zig build test intermittently hangs forever in the bounded-chat abort test | ||
|
|
||
| ## TL;DR | ||
|
|
||
| - **Question:** llm.client's 'bounded chat aborts a provider that never sends a response' (client.zig:1624) intermittently never returns: the test binary sits at ~0% CPU indefinitely. Seen twice in four otherwise-green runs of one tree on 2026-08-18 (arm64 macOS), once for 3 hours. Suspect: the 2026-08-17 request watchdog sometimes fails to unblock the parked read. Recovery: kill the test binary (match by cwd) and rerun. | ||
| - **Finding:** Investigating on 2026-08-18. | ||
| - **Resolution:** Investigating on 2026-08-18. | ||
|
|
||
| ## Status | ||
|
|
||
| Investigating on 2026-08-18. | ||
|
|
||
| ## Trigger and scope | ||
|
|
||
| ## Evidence | ||
|
|
||
| ## Hypotheses and tests | ||
|
|
||
| ## Finding | ||
|
|
||
| ## Resolution or handoff | ||
|
|
||
| ## References | ||
|
|
||
| - Related bug: none yet | ||
| ## Evidence | ||
|
Comment on lines
+13
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Align the report body with its section structure.
🧰 Tools🪛 markdownlint-cli2 (0.23.2)[warning] 26-26: Multiple headings with the same content (MD024, no-duplicate-heading) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| - 2026-08-18, run A: gate (zig build, zig build tools, zig build test) on 20fe5628 plus a webui-only diff. 'zig build test' stalled; the test binary showed 0.2% CPU after 30 minutes. 'sample <pid>' put the main thread in llm.client.test.bounded chat aborts a provider that never sends a response (client.zig:1624). Two other builds were running concurrently at the time. | ||
| - 2026-08-18, run B: same command on a64bd7e2 plus a webui-only diff, no concurrent builds. Same test, same frame, ~0.1% CPU, still parked after 2 h 58 m. This rules out build concurrency as the trigger. | ||
| - Two runs of the same tree(s) earlier the same day completed green (1623/1634 and 1625/1636, 11 skipped), so the hang is intermittent, roughly 2 in 4 today. | ||
| - Three orphaned test binaries from another session's worktree, ~4.4 days old, were found parked in the sibling test cli.test.a provider that never answers costs the sweep its budget, not the OS connect timeout (+1556), all at 0% CPU. So both never-answering-provider tests can wedge, and a wedged one never ages out on its own. | ||
| - The suite under test is the request watchdog shipped 2026-08-17 (agent.request_timeout_ms / stream_idle_timeout_ms; docs/reports/bugs/2026-08-17-agent-llm-call-has-no-deadline.md): the watchdog arms on a worker thread and Abort shutdown(2) is what unblocks the read. A hang here means that shutdown sometimes does not unblock the parked read, or the watchdog thread never fires. | ||
|
|
||
| ## Recovery | ||
|
|
||
| - Identify the wedged binary by cwd, never by argv (the build runner's argv is relative): iterate pids from 'ps', match 'lsof -p <pid> -d cwd' against the worktree, confirm with 'sample <pid>' that the main thread is in one of the two never-answering-provider tests, then kill it and the parent 'zig build test'. A rerun of the same tree passes. | ||
| - Reproduction is not on demand: the same tree passed twice and hung twice on the same day. Nothing here bisects to a change in the tree under test; the diffs in both hanging runs were webui-only files the test never touches. | ||
| ## Control run (2026-08-19) | ||
|
|
||
| A pristine detached worktree of a64bd7e2 (origin/main, no diff at all) wedged the same way: 'zig build test' parked in llm.client.test.bounded chat aborts a provider that never sends a response at ~0% CPU, confirmed by 'sample' after the 10-minute threshold. That makes the score on a64bd7e2 four hangs in four runs (three on a webui-only diff, one on pristine main), against three completed green runs earlier the same day on 15c9138c/20fe5628 bases. The hang therefore lives on current main independent of any working diff, and became much more frequent (possibly deterministic) somewhere in 20fe5628..a64bd7e2 — af6703e2 ('toolhost registry, cli updates') and 317c257b/cecb96f8 are the commits in that window. Not bisected further; recovery remains kill-by-cwd and rerun, but on a64bd7e2 reruns no longer converge. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| // Drives the shipped loadSkills over the DOM stub. The forEach callback used | ||
| // to shadow the #skills container with the row's checkbox, so every card was | ||
| // appended into its own checkbox (a hierarchy cycle the DOM refuses) and the | ||
| // panel rendered "Could not load skills." whenever at least one skill existed. | ||
| import assert from "node:assert/strict"; | ||
| import { readFileSync } from "node:fs"; | ||
| import { dirname, join } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import test from "node:test"; | ||
| import "../lib/dom-stub.mjs"; | ||
|
|
||
| const here = dirname(fileURLToPath(import.meta.url)); | ||
| const src = readFileSync(join(here, "tools.js"), "utf8"); | ||
|
|
||
| // The real function text, run against the stub document and a stubbed fetch: | ||
| // tools.js imports ui.js, whose vendor import node cannot resolve, so the | ||
| // module is not importable here and the shipped source is extracted instead | ||
| // (the harden.test.mjs pattern). | ||
| function shippedLoadSkills(elements, fetchImpl) { | ||
| const m = /(function loadSkills\(\) \{[\s\S]*?\n\})/.exec(src); | ||
| assert.ok(m, "loadSkills missing from tools.js"); | ||
| const doc = Object.create(globalThis.document); | ||
| doc.getElementById = function (id) { return elements[id] || null; }; | ||
| const factory = new Function( | ||
| "document", "fetch", "_readJson", "utilFmtBytes", "showLoadError", | ||
| "return (" + m[1] + ");" | ||
| ); | ||
| return factory( | ||
| doc, | ||
| fetchImpl, | ||
| function (x) { return x; }, | ||
| function () { return "1 KB"; }, | ||
| function () { elements.loadErrorShown = true; } | ||
| ); | ||
| } | ||
|
|
||
| test("a non-empty skills list renders its cards into #skills", async function () { | ||
| const d = globalThis.document; | ||
| const els = { skills: d.createElement("div"), "skills-status": d.createElement("p") }; | ||
| const loadSkills = shippedLoadSkills(els, function () { | ||
| return Promise.resolve({ skills: [ | ||
| { name: "review.md", bytes: 1024, enabled: true, description: "how to review" }, | ||
| { name: "ship.md", bytes: 2048, enabled: false } | ||
| ] }); | ||
| }); | ||
| await loadSkills(); | ||
| assert.equal(els.loadErrorShown, undefined, "the happy path must not fall into the load-error branch"); | ||
| const cards = els.skills.childNodes.filter(function (n) { return n.className === "skill-card"; }); | ||
| assert.equal(cards.length, 2); | ||
| cards.forEach(function (card) { | ||
| assert.equal(card.parentNode, els.skills); | ||
| // The checkbox sits inside the card, never the other way around. | ||
| const check = card.childNodes[0]; | ||
| assert.equal(check.tagName, "INPUT"); | ||
| assert.equal(check.childNodes.length, 0); | ||
| }); | ||
| assert.equal(els["skills-status"].textContent, "2 skills."); | ||
| }); | ||
|
|
||
| test("an empty skills list still renders its empty state", async function () { | ||
| const d = globalThis.document; | ||
| const els = { skills: d.createElement("div"), "skills-status": d.createElement("p") }; | ||
| const loadSkills = shippedLoadSkills(els, function () { | ||
| return Promise.resolve({ skills: [] }); | ||
| }); | ||
| await loadSkills(); | ||
| assert.equal(els.loadErrorShown, undefined); | ||
| assert.match(els.skills.textContent, /No skills on file/); | ||
| assert.equal(els["skills-status"].textContent, "No skills."); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Refresh the TL;DR after the August 19, 2026 control run.
Line 5 records two hangs in four runs on August 18, 2026. Line 40 adds a later result: four hangs in four runs on
a64bd7e2, and reruns no longer converge. The TL;DR is now stale. Mark it as an August 18 snapshot or update it with the August 19 result.🤖 Prompt for AI Agents