Skip to content

Dependency analyzer cannot be cancelled, bounded, or observed; one wedged job blocks all future runs #43

Description

@vnovick

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-101Enqueue 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:28FetchIssues 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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions