Summary
The dependency analyzer (POST /api/v1/deps/analyze) cannot be stopped, cannot time out on any useful horizon, reports nothing while it runs, and builds an unbounded prompt. A single wedged job blocks every future analysis run for up to an hour, and the only recovery is a daemon restart.
Observed on v0.2.0 (6d1c67c).
Root causes
1. No cancel route. internal/server/server.go:1272-1273 registers only POST /deps/analyze and GET /deps/analyze/{jobId}. There is no DELETE.
2. One wedged job blocks all future runs. internal/depsanalysis/job.go:97-101 — Enqueue returns the in-flight job's ID whenever current.Status == JobRunning, and current is only cleared in execute's success/failure path. Combined with (1) and (3), a hung job holds the slot for the full turn timeout.
3. No job-level timeout. Enqueue hands execute the daemon context and keeps no per-job handle. The only bound is agent.turn_timeout_ms, whose default is 3,600,000 ms = 1 hour (internal/config/config.go:429). The dashboard gives up polling after 10 minutes (web/src/queries/deps.ts:43), so client and server disagree by 50 minutes about whether the job is alive.
4. Zero observability. cmd/itervox/deps_analyzer_service.go:163-164 passes LogDir: "" and leaves onProgress nil. Nothing is written to disk and nothing reports progress — the button is a black box by construction, which is most of what "it's stuck" actually means to an operator.
5. Unbounded prompt. internal/depsanalysis/tracker_pass.go:28 — FetchIssues returns every issue across active + terminal + backlog states in one slice, and buildAnalyzerPrompt marshals all of it plus the entire tracker-edge list into a single turn. AnalyzerIssue is already compact (it drops comments, branches, timestamps) but Description is uncapped. On a few-hundred-issue backlog this is where the pass actually dies.
6. IssuesScanned is never written. Declared at internal/depsanalysis/job.go:31, read at cmd/itervox/deps_analyzer_service.go:213 and internal/server/server.go:812 — never assigned anywhere. The status row and the completion toast always report 0.
7. No panic recovery. internal/depsanalysis/job.go:116 is a bare go m.execute(ctx, job) with no recover(). A panic in the runner takes down the daemon, not just the job.
8. Analysis is manual-only. Nothing schedules a pass, so the inferred-edge sidecar goes stale as soon as new issues land.
Secondary
Nothing validates that analyzer-emitted source/target identifiers correspond to real issues, so hallucinated edges land in .itervox/dependencies.json silently.
Proposed fix
Split into two pieces, because the first depends on nothing and addresses the reported symptom directly:
4a — unstick it. Per-job context.CancelFunc + DELETE /api/v1/deps/analyze/{jobId} + a distinct cancelled status; a deps_analyzer_timeout_ms config (default 10 min, matching the dashboard's existing poll deadline); chunk the issue set with deps_analyzer_chunk_size (default 75) and scope the tracker-edge list per chunk; wire a real LogDir and progress callback; write IssuesScanned; add recover().
Cancellation is genuinely effective here rather than cosmetic: RunTurn threads its context into exec.CommandContext (internal/agent/claude.go:154), so cancelling the job context kills the agent subprocess.
Accepted cost of chunking: an edge between two issues in different chunks is invisible to the analyzer. Sorting by state then identifier clusters related work to reduce boundary misses, and the run logs the chunk count so the limitation is visible rather than silent.
4b — scheduled/incremental passes. Depends on a confidence/staleness model for inferred edges, so it is sequenced after that work.
Status
Spec and implementation plan are written; 4a is partially implemented (config fields, job lifecycle with cancel/timeout/panic-recovery, and the chunking helpers are done and reviewed). Remaining: wire chunking into the service, the cancel endpoint, and the frontend mutation.
Filing this so the analysis and the file:line evidence are captured in the tracker rather than only in a local plan.
Summary
The dependency analyzer (
POST /api/v1/deps/analyze) cannot be stopped, cannot time out on any useful horizon, reports nothing while it runs, and builds an unbounded prompt. A single wedged job blocks every future analysis run for up to an hour, and the only recovery is a daemon restart.Observed on
v0.2.0(6d1c67c).Root causes
1. No cancel route.
internal/server/server.go:1272-1273registers onlyPOST /deps/analyzeandGET /deps/analyze/{jobId}. There is noDELETE.2. One wedged job blocks all future runs.
internal/depsanalysis/job.go:97-101—Enqueuereturns the in-flight job's ID whenevercurrent.Status == JobRunning, andcurrentis only cleared inexecute's success/failure path. Combined with (1) and (3), a hung job holds the slot for the full turn timeout.3. No job-level timeout.
Enqueuehandsexecutethe daemon context and keeps no per-job handle. The only bound isagent.turn_timeout_ms, whose default is 3,600,000 ms = 1 hour (internal/config/config.go:429). The dashboard gives up polling after 10 minutes (web/src/queries/deps.ts:43), so client and server disagree by 50 minutes about whether the job is alive.4. Zero observability.
cmd/itervox/deps_analyzer_service.go:163-164passesLogDir: ""and leavesonProgressnil. Nothing is written to disk and nothing reports progress — the button is a black box by construction, which is most of what "it's stuck" actually means to an operator.5. Unbounded prompt.
internal/depsanalysis/tracker_pass.go:28—FetchIssuesreturns every issue across active + terminal + backlog states in one slice, andbuildAnalyzerPromptmarshals all of it plus the entire tracker-edge list into a single turn.AnalyzerIssueis already compact (it drops comments, branches, timestamps) butDescriptionis uncapped. On a few-hundred-issue backlog this is where the pass actually dies.6.
IssuesScannedis never written. Declared atinternal/depsanalysis/job.go:31, read atcmd/itervox/deps_analyzer_service.go:213andinternal/server/server.go:812— never assigned anywhere. The status row and the completion toast always report 0.7. No panic recovery.
internal/depsanalysis/job.go:116is a barego m.execute(ctx, job)with norecover(). A panic in the runner takes down the daemon, not just the job.8. Analysis is manual-only. Nothing schedules a pass, so the inferred-edge sidecar goes stale as soon as new issues land.
Secondary
Nothing validates that analyzer-emitted
source/targetidentifiers correspond to real issues, so hallucinated edges land in.itervox/dependencies.jsonsilently.Proposed fix
Split into two pieces, because the first depends on nothing and addresses the reported symptom directly:
4a — unstick it. Per-job
context.CancelFunc+DELETE /api/v1/deps/analyze/{jobId}+ a distinctcancelledstatus; adeps_analyzer_timeout_msconfig (default 10 min, matching the dashboard's existing poll deadline); chunk the issue set withdeps_analyzer_chunk_size(default 75) and scope the tracker-edge list per chunk; wire a realLogDirand progress callback; writeIssuesScanned; addrecover().Cancellation is genuinely effective here rather than cosmetic:
RunTurnthreads its context intoexec.CommandContext(internal/agent/claude.go:154), so cancelling the job context kills the agent subprocess.Accepted cost of chunking: an edge between two issues in different chunks is invisible to the analyzer. Sorting by state then identifier clusters related work to reduce boundary misses, and the run logs the chunk count so the limitation is visible rather than silent.
4b — scheduled/incremental passes. Depends on a confidence/staleness model for inferred edges, so it is sequenced after that work.
Status
Spec and implementation plan are written; 4a is partially implemented (config fields, job lifecycle with cancel/timeout/panic-recovery, and the chunking helpers are done and reviewed). Remaining: wire chunking into the service, the cancel endpoint, and the frontend mutation.
Filing this so the analysis and the file:line evidence are captured in the tracker rather than only in a local plan.