Skip to content

wedge build: web_search + LSP client/diagnostics (stages 01-03)#175

Merged
gnanam1990 merged 10 commits into
mainfrom
feat/wedge-build
Jun 12, 2026
Merged

wedge build: web_search + LSP client/diagnostics (stages 01-03)#175
gnanam1990 merged 10 commits into
mainfrom
feat/wedge-build

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

First three stages of the ZERO wedge build pack — the model-agnostic agent that runs unattended and fixes its own mistakes. Each stage is TDD-gated and tagged (zero-wedge-01..03). Stdlib-only; clean-room (Go implemented fresh from behavioural reference, no reproduced source). Verified the built binary runs and registers the new tool.

Stage 01 — web_search tool (zero-wedge-01)

Complements web_fetch (fetch a known URL) by discovering URLs — what an unattended agent needs when it hits unknown errors or new library versions.

  • internal/tools/web_search.go: webSearchTool over a pluggable searchBackend; env-configured generic JSON backend (ZERO_WEBSEARCH_BASE_URL/_API_KEY/_PROVIDER), swappable for Exa/You/Tavily/Brave. query + clamped limit (default 5, cap 10); numbered-list output; output redaction; clear "no search backend configured" error.
  • Registered in CoreNetworkTools().

Intentional deviation from the stage prompt: the prompt said readOnlySafety, but the repo's own TestCoreNetworkToolsExposePromptMetadata enforces that every core network tool is prompt-gated egress. Shipping an auto-allowed network tool (and weakening that test) would be a security regression, so web_search matches web_fetch's network/prompt posture — confirmed in the built binary (web_search [network/prompt]).

Stage 02 — LSP client foundation (internal/lsp) (zero-wedge-02)

A direct (self-spawned, stdio) Language Server Protocol client so an unattended run sees real compiler diagnostics with no open editor.

  • client.go: JSON-RPC 2.0 + LSP Content-Length framing; concurrent id-matched Call/Notify, server→client request auto-reply, notification handler, context cancellation.
  • server.go: lifecycle (spawn, initialize/initialized handshake, graceful shutdown+exit with kill fallback).
  • registry.go: extension → server command (.go/.ts/.tsx/.js/.jsx/.py/.rs), languageId, PATH availability.
  • types.go: minimal LSP types + PathToURI/URIToPath (Windows-drive aware).

Stage 03 — LSP document sync + diagnostics (zero-wedge-03)

Turns the client into actionable signal: "did my edit compile?", headless.

  • documents.go: per-server session — document lifecycle (didOpen / full-sync didChange / didClose), URI→diagnostics store fed by publishDiagnostics, WaitForDiagnostics with a publish-baseline + 300ms quiet debounce.
  • diagnostics.go: FormatDiagnostics (path:line:col: severity: message, 1-based), HasErrors, FilterBySeverity.
  • manager.go: Manager — one lazily-started, reused server per language; Check(ctx,path,text) → settled diagnostics; unknown extension → (nil,nil).

A TDD-caught bug worth noting: WaitForDiagnostics initially returned the previous check's diagnostics (keyed off global last-publish time); fixed with a per-URI publish counter so each check waits for its own publish.

Verification

Per-stage gate, all green: go build ./..., full go test ./..., gofmt -l clean. LSP concurrency tested with -race over in-memory pipes (stub server, no real gopls). Built the binary locally: runs, lists web_search as prompt-gated, internal/lsp compiled in.

Notes / sequencing

  • internal/lsp has no CLI surface yet — it's foundation consumed by stage 04 (the self-correct loop), which will wire testrunner/verify/selfverify/LSP into internal/agent/loop.go. That stage touches the core agent loop (conflict-prone vs in-flight TUI/loop PRs) and is intentionally not in this PR — it should land deliberately after sequencing.

Summary by CodeRabbit

  • New Features

    • LSP support: language-server integration with document sync, diagnostics delivery, and graceful startup/shutdown.
    • Web search tool: network-backed searches returning ranked title/URL/snippet results with query/limit.
    • Post-edit self-correction: optional verify-and-correct feedback appended once after edits.
  • Utilities

    • Path↔URI helpers, compact diagnostic formatting/filtering, and registry for server/language mappings.
  • Tests

    • Expanded unit/integration tests covering LSP, diagnostics, registry, web search, and self-correction.

Adds a web_search tool alongside web_fetch: discovers URLs via a pluggable
searchBackend (env-configured generic JSON backend; swappable for Exa/You/
Tavily/Brave) and formats ranked results as a numbered list. Registered in
CoreNetworkTools().

Deviation from the stage spec: the spec said readOnlySafety, but the repo
enforces (TestCoreNetworkToolsExposePromptMetadata) that every core network
tool is prompt-gated network egress — so web_search matches web_fetch's
posture instead of being auto-allowed. Necessary fallout from adding the
tool: knownToolNames (+web_search), the network-tools metadata test (now
covers both), and a stale 'unknown tool' fixture that had used web_search.
New internal/lsp package: a direct (self-spawned, stdio) Language Server
Protocol client so unattended runs see real compiler diagnostics without an
open editor.
- client.go: JSON-RPC 2.0 with LSP Content-Length framing; concurrent
  Call/Notify, id-matched responses, server->client request auto-reply,
  notification handler, context cancellation, read-loop teardown.
- server.go: process lifecycle (spawn via os/exec, initialize/initialized
  handshake, graceful shutdown+exit with kill fallback). Handshake factored
  out (performInitialize) for pipe-based testing.
- registry.go: extension -> server command (.go/.ts/.tsx/.js/.jsx/.py/.rs),
  languageId map, PATH availability via exec.LookPath.
- types.go: minimal LSP types + PathToURI/URIToPath (Windows-drive aware).
Stdlib-only; tested over in-memory pipes with a stub server (no real gopls),
including -race for the concurrent id router.
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 14823b6156ed
Changed files (23): internal/agent/loop.go, internal/agent/loop_test.go, internal/agent/selfcorrect.go, internal/agent/selfcorrect_test.go, internal/agent/types.go, internal/lsp/client.go, internal/lsp/client_test.go, internal/lsp/diagnostics.go, internal/lsp/diagnostics_test.go, internal/lsp/documents.go, internal/lsp/manager.go, internal/lsp/manager_test.go, and 11 more

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4dc5ad8a-1aa6-4a42-8416-4edd46c685cd

📥 Commits

Reviewing files that changed from the base of the PR and between 17b9de7 and 14823b6.

📒 Files selected for processing (2)
  • internal/agent/loop.go
  • internal/agent/loop_test.go

Walkthrough

Adds an LSP subsystem (types, framed JSON‑RPC client, server orchestration, session sync, manager), a pluggable web_search tool with HTTP backend, and a bounded post-edit SelfCorrector integrated into the agent loop; includes unit and integration tests for these behaviors.

Changes

LSP Subsystem

Layer / File(s) Summary
LSP types and URI conversion
internal/lsp/types.go, internal/lsp/types_test.go
Core LSP types and PathToURI/URIToPath helpers with Windows handling and unit tests.
Language server registry and tests
internal/lsp/registry.go, internal/lsp/registry_test.go
Extension→command and extension→languageId mappings, case-insensitive lookup, PATH availability check, and tests covering mapping, case-insensitivity, copy semantics, and availability.
JSON-RPC client with framing and tests
internal/lsp/client.go, internal/lsp/client_test.go
Content-Length framed JSON‑RPC client with background read loop, pending-request correlation by numeric ID, concurrent-safe Call/Notify/Close, notification handler, and tests covering framing, concurrency, cancellation, initialize handshake, notification delivery, and closed-client behavior.
Server process orchestration and handshake
internal/lsp/server.go
Start language-server processes, wire stdio to Client, perform initialize/initialized handshake using RootURI and minimal client capabilities, expose the client, and graceful Shutdown that waits then kills after a timeout.
Diagnostics and per-session documents
internal/lsp/diagnostics.go, internal/lsp/documents.go, tests
Severity labeling/filtering/formatting, session state with per-URI versions and debounced waiter logic, publishDiagnostics handler that drops stale versions, sync/didOpen/didChange/didClose semantics, and tests.
Manager and session lifecycle
internal/lsp/manager.go, internal/lsp/manager_test.go
Manager that resolves files to language-server sessions, lazy double-checked session startup, syncs text, waits for settled diagnostics, exposes DiagnosticsFor/HasErrors, and Shutdown that stops all sessions; includes stub-driven tests for concurrency and shutdown error propagation.

Web Search Tool

Layer / File(s) Summary
Web search tool implementation and HTTP backend
internal/tools/web_search.go
web_search tool with query (required) and limit schema, defaulting/clamping of limit, timeout-wrapped backends, httpSearchBackend using env-configured base URL and optional API key/provider, response body cap, parsing of top-level array or {results:[]} shapes, result formatting, and redaction of sensitive text.
Web search tests and registration
internal/tools/web_search_test.go, internal/tools/registry.go, internal/tools/registry_test.go, internal/specialist/manifest.go, internal/specialist/manifest_test.go
Tests cover output formatting, limit clamping/defaults, required args, unconfigured-backend behavior, redaction of backend errors, httptest backend forwarding/provider parsing, registration in CoreNetworkTools, and manifest recognition of web_search.

Agent Self-Correction

Layer / File(s) Summary
Self-corrector core and adapters
internal/agent/selfcorrect.go, internal/agent/selfcorrect_test.go
Adds SelfCorrectConfig, SelfCorrector.AfterEdit, inspection (optional LSP diagnostics + optional verification), redacted feedback synthesis, autonomy and attempt gating, adapters for lsp.Manager and project verifier, and unit tests for error propagation, transitions, abort, reporting, redaction, and disabled/nil safety.
Agent loop integration and options
internal/agent/loop.go, internal/agent/types.go, internal/agent/loop_test.go
Adds Options.SelfCorrect and invokes AfterEdit after successful tool calls that change files, collecting and appending any returned feedback after the tool-result batch; tests verify feedback is deferred until after the batch and that self-correction runs once per assistant turn.

Sequence Diagram

sequenceDiagram
  participant Manager
  participant Session
  participant Client
  participant LSPProcess
  Manager->>Session: Check / sync(text)
  Session->>Client: Notify/Call (didOpen/didChange)
  Client->>LSPProcess: writeMessage (Content-Length framed)
  LSPProcess-->>Client: framed response or notification
  Client->>Session: deliver response or invoke NotificationHandler
  Session-->>Manager: publishDiagnostics (debounced)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Gitlawb/zero#176: Very similar SelfCorrector additions and agent-loop integration; overlapping changes to self-correction wiring.

Suggested reviewers

  • Vasanthdev2004
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main additions: web_search tool and LSP client/diagnostics infrastructure across stages 01-03.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/wedge-build

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/lsp/client.go`:
- Around line 101-140: Close() currently doesn't prevent new Calls/Notifies and
pending entries can leak; fix by making Close() set the closed state (close the
c.closed channel or set a flag) before calling failPending, and have Call()
check that closed state under the same mutex before allocating id/ch and before
calling c.write (if closed return an error immediately). Also ensure the closed
select branch in Call removes c.pending[id] under the mutex before returning
(and similarly ensure Notify() checks closed and returns an error instead of
calling c.write). Use c.mu to serialize checks/registration/cleanup around
pending, closed and nextID; keep existing helpers failPending/readError for
error values.

In `@internal/tools/web_search.go`:
- Around line 164-178: The Search implementation ignores the provider captured
by defaultSearchBackend (backend.provider), so either wire that value into the
outbound HTTP request or remove the config knob; to fix, update
httpSearchBackend.Search to, when backend.provider is non-empty, set a provider
header (e.g. request.Header.Set("X-Zero-Websearch-Provider", backend.provider))
before sending the request so backends that route/auth by provider receive it,
or if you prefer to drop the feature remove the provider field and related env
parsing in defaultSearchBackend.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c52eb571-e931-473d-ba1e-41b5881edbaf

📥 Commits

Reviewing files that changed from the base of the PR and between fcfdb87 and 6888706.

📒 Files selected for processing (13)
  • internal/lsp/client.go
  • internal/lsp/client_test.go
  • internal/lsp/registry.go
  • internal/lsp/registry_test.go
  • internal/lsp/server.go
  • internal/lsp/types.go
  • internal/lsp/types_test.go
  • internal/specialist/manifest.go
  • internal/specialist/manifest_test.go
  • internal/tools/registry.go
  • internal/tools/registry_test.go
  • internal/tools/web_search.go
  • internal/tools/web_search_test.go

Comment thread internal/lsp/client.go
Comment thread internal/tools/web_search.go
Makes the LSP client produce actionable signal so ZERO can ask 'did my edit
compile?' headlessly.
- documents.go: per-server session with document lifecycle (didOpen/didChange
  full-sync/didClose), a URI->diagnostics store fed by publishDiagnostics, and
  WaitForDiagnostics with a publish-baseline + 300ms quiet debounce (servers
  never signal 'analysis complete').
- diagnostics.go: FormatDiagnostics (path:line:col: severity: message, 1-based),
  HasErrors, FilterBySeverity, severity labels.
- manager.go: Manager — one lazily-started, reused server per language (keyed by
  binary), routes a file to the right server, Check(ctx,path,text) -> settled
  diagnostics, graceful Shutdown; unknown extension -> (nil,nil), not an error.
Concurrency-safe; tested under -race over in-memory pipes with a stub server
(no real gopls). FormatDiagnostics takes the path explicitly since a Diagnostic
carries only a range, not its document.
@gnanam1990 gnanam1990 changed the title wedge build: web_search tool + LSP client foundation (stages 01-02) wedge build: web_search + LSP client/diagnostics (stages 01-03) Jun 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/lsp/diagnostics.go`:
- Around line 57-68: FormatDiagnostics is producing multi-line outputs when
Diagnostic.Message contains newlines/tabs; normalize whitespace so each
diagnostic becomes a single physical line. In FormatDiagnostics (and where
severityLabel/Diagnostic are used), replace trimming with collapsing internal
whitespace (e.g., split on whitespace and join with a single space) or
explicitly replace \n and \t with spaces so the formatted string remains
"path:line:col: severity: message" on one line per diag; ensure you trim
leading/trailing spaces after normalization.

In `@internal/lsp/documents.go`:
- Around line 69-90: The sync function currently increments s.versions[uri] and
sets s.open[uri] before releasing s.mu and calling s.client.Notify, which allows
races and leaves local state advanced on failed Notify; fix by serializing
per-URI work and committing state only after a successful Notify. Introduce a
per-URI lock (e.g. a fileLocks map keyed by URI holding *sync.Mutex or
sync.Mutex) or reuse a per-URI channel so that session.sync (the sync method)
acquires the URI-specific lock, computes/assigns the version only while holding
that lock, performs the Notify call while still serialized for that URI, and
only after Notify returns successfully update s.open[uri] and s.versions[uri]
(or increment the version after a successful notify); ensure Notify failures do
not leave versions advanced and always release the per-URI lock when done. Use
identifiers: session.sync, s.versions, s.open, s.client.Notify, PathToURI,
TextDocumentItem to locate and change the code.
- Around line 52-65: The publishDiagnostics handler currently overwrites caches
regardless of payload version; update the logic in the block that unmarshals
PublishDiagnosticsParams (payload) to check payload.Version against
s.versions[payload.URI] and skip updating s.diagnostics, s.lastPublish, and
s.publishCount when payload.Version is present and less than the stored
s.versions[payload.URI]; only proceed with the existing
write/delete/waiter-close sequence when payload.Version is absent or >=
s.versions[payload.URI], ensuring waiters are not notified for stale payloads.

In `@internal/lsp/manager.go`:
- Around line 67-72: The current flow always returns sess.diagnosticsFor(uri)
even if waitForDiagnostics timed out without seeing a publish newer than the
baseline; change waitForDiagnostics to return a boolean (e.g., sawNewerPublish
bool) indicating whether a publish newer than sess.publishBaseline(uri) was
observed, update its signature and all callers, then in the Check path after
sess.sync call use the boolean return: if waitForDiagnostics(...) returns false,
return nil or an empty diagnostics slice instead of sess.diagnosticsFor(uri),
otherwise return sess.diagnosticsFor(uri); ensure you update any call sites to
handle the new return value.
- Around line 97-105: Manager.Shutdown currently discards errors from each
sess.server.Shutdown(ctx), yielding false success; change it to capture and
propagate failures: iterate sessions from m.sessions as now but collect errors
from sess.server.Shutdown(ctx) (e.g., append non-nil errors or wrap them with
context including session ID), and after the loop return a combined error (first
error or an aggregated multi-error) instead of nil; keep the same locking
behavior around m.sessions and reference Manager.Shutdown, m.sessions, and
sess.server.Shutdown(ctx) when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bf52674f-98cc-4732-ac34-1d4a05217a97

📥 Commits

Reviewing files that changed from the base of the PR and between 6888706 and 1c30711.

📒 Files selected for processing (5)
  • internal/lsp/diagnostics.go
  • internal/lsp/diagnostics_test.go
  • internal/lsp/documents.go
  • internal/lsp/manager.go
  • internal/lsp/manager_test.go

Comment thread internal/lsp/diagnostics.go
Comment thread internal/lsp/documents.go
Comment thread internal/lsp/documents.go Outdated
Comment thread internal/lsp/manager.go
Comment thread internal/lsp/manager.go Outdated
- lsp/client.go: Close() now disables the client — Call/Notify reject after
  close instead of leaking a pending entry / writing to a dead conn.
- lsp/documents.go: serialize same-document syncs through the Notify write via a
  per-URI lock and commit version/open only on success; drop delayed
  publishDiagnostics for a superseded (older positive) version;
  WaitForDiagnostics now reports whether a fresh publish arrived.
- lsp/manager.go: Check returns (nil,nil) when no publish newer than baseline
  arrives (don't return a stale prior result for new text); Shutdown joins and
  returns per-server shutdown errors instead of always reporting success.
- lsp/diagnostics.go: FormatDiagnostics collapses embedded whitespace so each
  diagnostic stays one physical line.
- tools/web_search.go: forward ZERO_WEBSEARCH_PROVIDER in the request so the
  config knob isn't inert.
Adds tests: close-disables-client, stale-version drop, multi-line message
collapse, Shutdown error propagation, httptest backend (provider+parse).
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Addressed the review. LSP: Close() now disables the client (Call/Notify reject post-close); same-document syncs are serialized per-URI across the Notify and version is committed only on success; delayed publishDiagnostics for a superseded version is dropped; Check returns no diagnostics (rather than a stale prior result) when no publish newer than the sync arrives; Manager.Shutdown now joins and returns per-server errors; FormatDiagnostics collapses embedded whitespace to one line per diagnostic. web_search: ZERO_WEBSEARCH_PROVIDER is now forwarded in the request. Added tests for each (incl. an httptest backend round-trip and a -race pass on the LSP concurrency). @coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

@gnanam1990 Thanks for the thorough summary of changes — those are all the right fixes for the concerns raised. Triggering a fresh review now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/lsp/manager_test.go (1)

170-201: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Race condition: t.Errorf() called from goroutines.

Line 188 calls t.Errorf() from within a goroutine. The testing.T methods are not safe for concurrent use—calling them from multiple goroutines can cause data races and undefined behavior.

🔒 Proposed fix to collect errors safely
 func TestManagerCheckConcurrentReusesOneServer(t *testing.T) {
 	var mu sync.Mutex
 	started := 0
+	var errs []error
 	base := stubStarter([][]Diagnostic{{{Severity: SeverityWarning, Message: "w"}}}, false)
 	m := fastManager(func(ctx context.Context, command []string, root string) (lspServer, error) {
 		mu.Lock()
 		started++
 		mu.Unlock()
 		return base(ctx, command, root)
 	})
 	defer m.Shutdown(context.Background())
 
 	var wg sync.WaitGroup
 	for i := 0; i < 4; i++ {
 		wg.Add(1)
 		go func() {
 			defer wg.Done()
 			if _, err := m.Check(context.Background(), "main.go", "package main"); err != nil {
-				t.Errorf("concurrent Check: %v", err)
+				mu.Lock()
+				errs = append(errs, err)
+				mu.Unlock()
 			}
 		}()
 	}
 	wg.Wait()
+	for _, err := range errs {
+		t.Errorf("concurrent Check: %v", err)
+	}
 
 	// All four calls target the same .go server; only one session is retained.
 	m.mu.Lock()
 	sessions := len(m.sessions)
 	m.mu.Unlock()
 	if sessions != 1 {
 		t.Fatalf("expected a single reused session, got %d", sessions)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/lsp/manager_test.go` around lines 170 - 201, The test calls t.Errorf
from multiple goroutines inside TestManagerCheckConcurrentReusesOneServer (the
goroutine that calls m.Check), which is unsafe; change the goroutine to capture
any error and send it to a buffered channel or append to a mutex-protected
slice, then after wg.Wait iterate that collection and call t.Errorf/t.Fatalf
from the main test goroutine; keep references to the existing wg, the m.Check
call, and the final sessions assertion (m.sessions) so only the error-reporting
is moved out of the child goroutines.
🧹 Nitpick comments (1)
internal/lsp/manager_test.go (1)

52-52: 💤 Low value

Consider handling unmarshal errors for test clarity.

The ignored unmarshal error means malformed JSON will silently skip the message. While acceptable for a test stub, explicitly checking and logging errors would make test failures clearer if the stub receives unexpected input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/lsp/manager_test.go` at line 52, The json.Unmarshal call currently
ignores its error (json.Unmarshal(body, &msg)); change it to capture and handle
the error and fail the test so malformed JSON surface clearly: assign err :=
json.Unmarshal(body, &msg) and if err != nil call t.Fatalf or t.Errorf (e.g.,
t.Fatalf("failed to unmarshal message: %v; body=%q", err, body)) so the test
stub logs the unmarshalling error and the raw body for debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/lsp/manager_test.go`:
- Around line 170-201: The test calls t.Errorf from multiple goroutines inside
TestManagerCheckConcurrentReusesOneServer (the goroutine that calls m.Check),
which is unsafe; change the goroutine to capture any error and send it to a
buffered channel or append to a mutex-protected slice, then after wg.Wait
iterate that collection and call t.Errorf/t.Fatalf from the main test goroutine;
keep references to the existing wg, the m.Check call, and the final sessions
assertion (m.sessions) so only the error-reporting is moved out of the child
goroutines.

---

Nitpick comments:
In `@internal/lsp/manager_test.go`:
- Line 52: The json.Unmarshal call currently ignores its error
(json.Unmarshal(body, &msg)); change it to capture and handle the error and fail
the test so malformed JSON surface clearly: assign err := json.Unmarshal(body,
&msg) and if err != nil call t.Fatalf or t.Errorf (e.g., t.Fatalf("failed to
unmarshal message: %v; body=%q", err, body)) so the test stub logs the
unmarshalling error and the raw body for debugging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 875c9c87-3cf9-46b2-a1d6-4ddf60d779f2

📥 Commits

Reviewing files that changed from the base of the PR and between 1c30711 and c734088.

📒 Files selected for processing (9)
  • internal/lsp/client.go
  • internal/lsp/client_test.go
  • internal/lsp/diagnostics.go
  • internal/lsp/diagnostics_test.go
  • internal/lsp/documents.go
  • internal/lsp/manager.go
  • internal/lsp/manager_test.go
  • internal/tools/web_search.go
  • internal/tools/web_search_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • internal/lsp/diagnostics_test.go
  • internal/lsp/client_test.go
  • internal/lsp/manager.go
  • internal/lsp/documents.go
  • internal/tools/web_search.go
  • internal/lsp/client.go
  • internal/lsp/diagnostics.go

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for one production behavior gap.

  • internal/lsp/manager.go:55-64: Manager.Check promises graceful degradation when no language server is available, but it only checks ServerFor(path) and then calls sessionFor, which eventually runs StartServer. On a clean machine without gopls, pyright-langserver, rust-analyzer, etc., a configured extension returns an executable-not-found error instead of (nil, nil). Since LSP diagnostics are an opportunistic foundation layer, missing local language servers should degrade the same way unsupported extensions do. Please check availability before starting, or translate executable-not-found from the starter into no diagnostics, and add a test for a configured extension whose server binary is unavailable.

Non-blocking note: I saw the CodeRabbit test comments in manager_test.go; the ignored unmarshal is worth cleaning up, but t.Errorf from worker goroutines is not the merge blocker for me here.

Validation I ran on this head:

  • git diff --check origin/main...HEAD passed
  • go test ./internal/lsp ./internal/tools ./internal/specialist passed
  • go test ./... passed
  • go run ./cmd/zero-release build passed

…review)

Manager.Check translated a missing-binary start error (exec.ErrNotFound)
into (nil,nil) so a configured extension on a machine without gopls/
pyright/rust-analyzer degrades exactly like an unsupported extension —
LSP diagnostics are an opportunistic layer, not a hard dependency. Other
start failures still surface. Adds a test injecting an exec.ErrNotFound
starter.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Addressed @Vasanthdev2004's blocker: Manager.Check now degrades to (nil, nil) when the configured language-server binary isn't installed (translates exec.ErrNotFound from the starter), so a clean machine without gopls/pyright/rust-analyzer behaves like an unsupported extension rather than erroring. Added a test injecting an exec.ErrNotFound starter. (CodeRabbit's manager_test nits — ignored unmarshal / goroutine t.Errorf — are non-blocking and noted.) @coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the exec.ErrNotFound handling in manager.go
rg -n "ErrNotFound\|errors.Is\|exec.Err" --type=go internal/lsp/manager.go
echo "---"
# Check the test for ErrNotFound starter
rg -n "ErrNotFound\|NotFound\|notFound" --type=go internal/lsp/manager_test.go
echo "---"
# Show the relevant Check method section
ast-grep --pattern 'func (m *Manager) Check($$$) ($$$) { $$$ }'

Length of output: 2418


🏁 Script executed:

#!/bin/bash
# Find isServerUnavailable definition
rg -n "isServerUnavailable" --type=go
echo "==="
# Find the test for binary-not-found / unavailable
rg -n "unavailable\|notInstalled\|BinaryNotFound\|ErrExec\|exec\.ErrNotFound\|errors\.As\|errors\.Is" --type=go internal/lsp/
echo "==="
# Show full manager.go for context
cat internal/lsp/manager.go

Length of output: 5903


🏁 Script executed:

#!/bin/bash
cat internal/lsp/manager_test.go

Length of output: 7745



@gnanam1990 Scanned the implementation before triggering. The fix looks correct:

  • isServerUnavailable uses errors.Is(err, exec.ErrNotFound)exec.Error implements Unwrap() returning its inner Err, so the chain traversal works correctly for the injected &exec.Error{Name: "gopls", Err: exec.ErrNotFound} and for real exec.LookPath failures alike.
  • TestManagerCheckDegradesWhenServerBinaryMissing covers the path cleanly.
  • On the two nits you noted: t.Errorf from a goroutine is safe (only t.FailNow/t.Fatal are not); and discarding the json.Unmarshal error in the test stub is fine. Agreed they're non-blocking.

Triggering a fresh review now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

* stage 04: self-correct loop

After a successful mutating tool call, verify the changed files (LSP
diagnostics + project tests) and feed failures back to the model to fix —
bounded and autonomy-gated, so an unattended run fixes its own mistakes
without looping forever.

- internal/agent/selfcorrect.go: SelfCorrector with injectable diagnosticsChecker
  (lsp.Manager adapter) and projectVerifier (verify.DetectPlan+Run); AfterEdit
  produces a redacted, model-facing corrective message; per-run attempt ceiling;
  low autonomy reports without auto-attempting; medium/high auto-correct.
- internal/agent/loop.go: invoke AfterEdit after a successful mutating tool call
  (ChangedFiles non-empty), appending feedback to messages. nil SelfCorrect (the
  default) is a no-op — read-only tools never trigger it.
- internal/agent/types.go: add Options.SelfCorrect *SelfCorrector (the explicit
  options-struct change).

Single-pass verify per call (the agent loop + model edits provide the multi-
attempt cycle, not selfverify's same-command re-runs). Tested with fakes (no
gopls/shell): fail-then-pass, max-attempts abort, read-only skip, low-autonomy
report-only, LSP-only failure, secret redaction; -race clean.

CLI activation (a --self-correct flag building the SelfCorrector in exec.go) is
intentionally left out to honor this stage's file scope — the engine is exported
and ready to wire.

* wedge: propagate file-read error in self-correct LSP checker (#176 review)

CodeRabbit/golangci-lint (nilerr) flagged lspDiagnosticsChecker.Check returning
(nil, nil) after os.ReadFile fails. Propagate the error instead; the sole caller
(inspect) already skips files whose Check errors, so a deleted/unreadable file
still degrades quietly while future callers can decide. Adds a regression test.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for one new blocker in the stage-04 self-correct integration.

  • internal/agent/loop.go:334-340: self-correct feedback is appended as a MessageRoleUser immediately after a successful mutating tool result, while the loop may still have later tool calls from the same assistant message to execute. If a model emits multiple tool calls in one turn and the first mutates, the message sequence becomes assistant(tool_calls: c1,c2) -> tool(c1) -> user(self-correct feedback) -> tool(c2), which violates strict provider replay ordering. Providers like Anthropic require every advertised tool_use to be answered by its tool_result before any new user message. Please queue/aggregate self-correct feedback until after the for collected.ToolCalls batch has appended all tool results, or run self-correct once after the batch over the union of changed files. Add a regression test with two tool calls where the first mutates and self-correct emits feedback, asserting all tool messages for the batch precede the self-correct user message.

The previous LSP missing-binary blocker is fixed: Manager.Check now degrades to (nil, nil) for exec.ErrNotFound and keeps other start failures surfaced.

Validation I ran on this head:

  • git diff --check origin/main...HEAD passed
  • go test -count=1 ./internal/agent ./internal/lsp ./internal/tools ./internal/specialist passed
  • go test ./... passed
  • go run ./cmd/zero-release build passed
  • go run ./cmd/zero-release smoke passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/agent/loop.go (1)

330-342: 💤 Low value

Consider capturing the outcome for observability.

Line 335 discards the outcome return value from AfterEdit with _. While the feedback content implies the outcome ("stopping auto-correction" for aborted, "reporting only" for reported), capturing the outcome would allow explicit logging or metrics for when the corrector reaches OutcomeAborted or OutcomeReported states without parsing feedback strings.

🔍 Optional improvement
-		if feedback, _ := options.SelfCorrect.AfterEdit(ctx, toolResult.ChangedFiles); feedback != "" {
+		if feedback, outcome := options.SelfCorrect.AfterEdit(ctx, toolResult.ChangedFiles); feedback != "" {
+			// Optional: log or metric the outcome here for observability
 			messages = append(messages, zeroruntime.Message{
 				Role:    zeroruntime.MessageRoleUser,
 				Content: feedback,
 			})
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agent/loop.go` around lines 330 - 342, The AfterEdit call currently
ignores the returned outcome; change the call in the block that checks
options.SelfCorrect, toolResult.Status and toolResult.ChangedFiles to capture
both return values (e.g., outcome, feedback :=
options.SelfCorrect.AfterEdit(...)) and then use the outcome (compare to
SelfCorrectOutcome values like OutcomeAborted and OutcomeReported) to emit a log
or metric via the existing logger/metrics path so you can observe aborted vs
reported states while still appending non-empty feedback to messages; reference
the SelfCorrect interface, its AfterEdit method, the outcome variable, and
constants OutcomeAborted/OutcomeReported when implementing the handling.
internal/agent/selfcorrect.go (1)

213-216: ⚡ Quick win

Clarify comment: caller continues rather than "deciding".

The comment states "let the caller decide", but the caller (inspect at line 115) silently continues rather than deciding how to handle the error. Either update the comment to reflect graceful degradation intent, or change inspect to distinguish recoverable errors (missing file) from failures (server crash).

📝 Suggested clarification
 func (c lspDiagnosticsChecker) Check(ctx context.Context, path string) ([]lsp.Diagnostic, error) {
 	text, err := os.ReadFile(path)
 	if err != nil {
-		return nil, err // unreadable (e.g. deleted) -> let the caller decide
+		return nil, err // unreadable (e.g. deleted); inspect will skip this file
 	}
 	return c.manager.Check(ctx, path, string(text))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agent/selfcorrect.go` around lines 213 - 216, The comment in
lspDiagnosticsChecker.Check incorrectly says "let the caller decide" even though
the caller function inspect silently continues; either update the comment in
Check to state it intentionally returns nil,error to allow graceful continuation
(e.g., "unreadable (e.g. deleted) -> caller will continue") or change the caller
inspect to treat recoverable errors specially: in inspect, detect missing-file
or recoverable conditions using os.IsNotExist(err) (or other error
classification) and continue, while only bubbling up true failures; reference
the lspDiagnosticsChecker.Check function and the inspect function when making
this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/agent/selfcorrect.go`:
- Around line 107-132: SelfCorrector.inspect currently drops non-nil errors from
sc.checker.Check and sc.verifier.Verify; change it to surface those errors in
the returned CorrectionReport instead of silently continuing: for
sc.checker.Check, when err != nil (and not already handled by the checker), set
report.Failed = true and attach a synthetic lsp.Diagnostic for that file (use
lsp.Diagnostic.Message = err.Error() and Severity = lsp.SeverityError) into
report.LSPDiagnostics[file]; for sc.verifier.Verify, when it returns (vr, err)
and err != nil, set report.Failed = true and populate report.VerifyReport with
an error-bearing report (e.g., a VerifyReport/verify.Report instance with
OK=false and an Error/Message field containing err.Error()) so verification
failures are visible to callers; keep the existing logic that treats
vr.OK==false as failure. Ensure you reference SelfCorrector.inspect,
sc.checker.Check, sc.verifier.Verify, report.LSPDiagnostics,
report.VerifyReport, and report.Failed when implementing.

---

Nitpick comments:
In `@internal/agent/loop.go`:
- Around line 330-342: The AfterEdit call currently ignores the returned
outcome; change the call in the block that checks options.SelfCorrect,
toolResult.Status and toolResult.ChangedFiles to capture both return values
(e.g., outcome, feedback := options.SelfCorrect.AfterEdit(...)) and then use the
outcome (compare to SelfCorrectOutcome values like OutcomeAborted and
OutcomeReported) to emit a log or metric via the existing logger/metrics path so
you can observe aborted vs reported states while still appending non-empty
feedback to messages; reference the SelfCorrect interface, its AfterEdit method,
the outcome variable, and constants OutcomeAborted/OutcomeReported when
implementing the handling.

In `@internal/agent/selfcorrect.go`:
- Around line 213-216: The comment in lspDiagnosticsChecker.Check incorrectly
says "let the caller decide" even though the caller function inspect silently
continues; either update the comment in Check to state it intentionally returns
nil,error to allow graceful continuation (e.g., "unreadable (e.g. deleted) ->
caller will continue") or change the caller inspect to treat recoverable errors
specially: in inspect, detect missing-file or recoverable conditions using
os.IsNotExist(err) (or other error classification) and continue, while only
bubbling up true failures; reference the lspDiagnosticsChecker.Check function
and the inspect function when making this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 09c2956b-0e8d-4cec-9828-ffb2ddae2a0d

📥 Commits

Reviewing files that changed from the base of the PR and between 6a69bff and 8ab47b6.

📒 Files selected for processing (4)
  • internal/agent/loop.go
  • internal/agent/selfcorrect.go
  • internal/agent/selfcorrect_test.go
  • internal/agent/types.go

Comment thread internal/agent/selfcorrect.go
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

The Close() lifecycle finding is already resolved (commit c734088):

  • Call() rejects work via an up-front select { case <-c.closed: ... } guard.
  • Notify() (and Call's request write) are rejected by write()'s own <-c.closed check — its comment notes exactly this.
  • failPending swaps the whole pending map for a fresh one under closeOnce, so any entry registered during a close race lives in the discarded map and is GC'd — no leak. Adding delete(c.pending, id) to the <-c.closed select branch would be a no-op given that design (unlike the ctx.Done branch, where failPending has not run).

Verified by TestClientRejectsCallsAfterClose (passes with -race). web_search also forwards ZERO_WEBSEARCH_PROVIDER now.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Close/Call/Notify/failPending implementation
cat internal/lsp/client.go
echo "==="
# Check the test
rg -n "TestClientRejectsCallsAfterClose" --type=go -A 30 internal/lsp/client_test.go

Length of output: 7949



@gnanam1990 Confirmed. The implementation matches your description exactly:

  1. Pre-registration guardCall() selects <-c.closed before touching c.pending, so no dangling entry.
  2. Write guardwrite() has its own <-c.closed check, covering both Notify() and Call()'s outgoing frame.
  3. Map-swap in failPending — swaps out the whole pending map under closeOnce, drains all entries by sending errors on their channels, then closes c.closed. Any entry registered in the narrow window between the swap and close(c.closed) lives in the discarded map and is drained there — no leak.
  4. delete(c.pending, id) in <-c.closed branch — correctly a no-op because failPending already owns and drained the prior map; the current c.pending is the fresh empty replacement.

TestClientRejectsCallsAfterClose with -race is the right gate for this. Everything looks solid.

Triggering the review now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approvable ?

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check open review comments on PR `#175`
gh pr view 175 --repo Gitlawb/zero --json reviewDecision,reviews,state
echo "==="
# List review comments still open/unresolved
gh api repos/Gitlawb/zero/pulls/175/comments --jq '.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, body: .body[0:200], resolved: .resolved}'
echo "==="
# Get review threads
gh api repos/Gitlawb/zero/pulls/175/reviews --jq '.[] | {id: .id, user: .user.login, state: .state, submitted_at: .submitted_at}'

Length of output: 31003



@gnanam1990 Not yet — two blocking items remain on the latest head (8ab47b6):

1. Vasanthdev2004 blocker — internal/agent/loop.go:334-340 (protocol violation)

Self-correct feedback is injected as MessageRoleUser immediately after the first mutating tool result, but the loop may still have more tool calls from the same assistant message queued. The resulting message order is:

assistant(tool_calls: c1, c2) → tool_result(c1) → user(self-correct) → tool_result(c2)

Anthropic (and strict providers generally) require every tool_use ID in an assistant turn to be answered by tool_result before any new user message. This will cause API errors in multi-tool-call turns. The fix is to aggregate changedFiles across the entire tool-call batch and run AfterEdit once after all results are appended.

2. My actionable — internal/agent/selfcorrect.go:107-132

SelfCorrector.inspect silently drops non-nil errors from both sc.checker.Check and sc.verifier.Verify. Errors disappear; callers see report.Failed = false when the check actually failed. Those errors should set report.Failed = true and surface diagnostics (e.g. a synthetic lsp.Diagnostic for checker errors, or populate report.VerifyReport for verifier errors).


Non-blocking (can defer):

  • loop.go: capture outcome from AfterEdit for observability
  • selfcorrect.go:213-216: comment wording ("let the caller decide" is inaccurate)
  • manager_test.go: t.Errorf from goroutine + unchecked json.Unmarshal

…iew)

SelfCorrector.inspect silently continued on every checker.Check error and
ignored verifier errors, so a real LSP startup/sync failure, an unreadable
changed file, or a failed plan detection produced report.Failed=false ->
OutcomePassed with no signal. Manager.Check already degrades missing servers to
(nil,nil) and DetectPlan returns an empty plan (not an error) when nothing is
detected, so a non-nil error from either is a genuine failure. Record such
errors in CorrectionReport.InspectErrors, fail the pass, and surface them in the
model feedback. Adds regression tests for both paths.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Verified each open thread against current HEAD (3b77c97). Six of the seven flagged items were already fixed in the latest pushes and are stale re-anchors:

  • lsp/manager.go:86 — resolved: Check returns nil when waitForDiagnostics observes no publish newer than the baseline.
  • lsp/manager.go:105 — resolved: Shutdown collects per-server errors via errors.Join.
  • lsp/documents.go:75 — resolved: publishDiagnostics drops a strictly-older positive version.
  • lsp/documents.go:90 — resolved: sync holds a per-URI lock across version select + notify and commits state only after the write succeeds.
  • lsp/diagnostics.go:75 — resolved: FormatDiagnostics flattens via collapseWhitespace (strings.Fields), so each diagnostic stays one physical line.
  • lsp/client.go:148 — resolved: Call guards on <-c.closed up front, writes are rejected once closed, and failPending swaps the whole pending map so a close-race entry is discarded (no leak).

The one genuinely-open item is fixed in 3b77c97:

  • selfcorrect.goinspect no longer swallows checker.Check/verifier.Verify errors. A non-nil error (LSP startup/sync failure, unreadable changed file, or failed plan detection) is now recorded in CorrectionReport.InspectErrors, fails the pass, and is surfaced in the model feedback — instead of producing a false passed. Added regression tests for both paths.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check selfcorrect.go inspect function for error handling
rg -n "InspectErrors\|report\.Failed\|checker\.Check\|verifier\.Verify\|CorrectionReport" --type=go internal/agent/selfcorrect.go
echo "=== inspect function ==="
ast-grep --pattern 'func ($_ *SelfCorrector) inspect($$$) ($$$) { $$$ }'
echo "=== CorrectionReport struct ==="
rg -n "CorrectionReport" --type=go internal/agent/selfcorrect.go -A 8 -B 2
echo "=== loop.go self-correct injection point ==="
sed -n '320,360p' internal/agent/loop.go
echo "=== full loop tool-call batch handling ==="
rg -n "AfterEdit\|SelfCorrect\|changedFiles\|ChangedFiles\|tool_result\|toolResult" --type=go internal/agent/loop.go -C 3

Length of output: 5028



@gnanam1990 The selfcorrect.go fix is confirmed — InspectErrors is properly populated and errors no longer produce a false passed.

However, the loop.go protocol violation is still present. The self-correct injection is still inside the per-tool-call for loop:

// loop.go — inside: for index, call := range collected.ToolCalls {
if options.SelfCorrect != nil && toolResult.Status == tools.StatusOK && len(toolResult.ChangedFiles) > 0 {
    if feedback, _ := options.SelfCorrect.AfterEdit(ctx, toolResult.ChangedFiles); feedback != "" {
        messages = append(messages, zeroruntime.Message{Role: MessageRoleUser, Content: feedback})
    }
}
// ← loop continues to tool_result(c2), tool_result(c3)…

In a multi-tool-call turn this produces:

assistant(tool_calls: c1, c2) → tool_result(c1) → user(self-correct) → tool_result(c2)

Anthropic requires all tool_result blocks for a given assistant turn to appear before any new user message, or you get a 400. The fix is to aggregate changedFiles across the batch and call AfterEdit once after the loop:

// after the for loop, before the next turn
var batchChangedFiles []string
for _, tr := range toolResults {  // or accumulate inside the loop
    if tr.Status == tools.StatusOK {
        batchChangedFiles = append(batchChangedFiles, tr.ChangedFiles...)
    }
}
if options.SelfCorrect != nil && len(batchChangedFiles) > 0 {
    if feedback, _ := options.SelfCorrect.AfterEdit(ctx, batchChangedFiles); feedback != "" {
        messages = append(messages, zeroruntime.Message{Role: MessageRoleUser, Content: feedback})
    }
}

Not triggering the review yet — this one needs to land first since it will cause live API errors on any prompt that issues two or more tool calls in one turn.

Keep code/comments provider-neutral: describe the backend interface as 'any
hosted search API' rather than naming specific services.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Request changes

Finding:
[P1] Preserve tool_result ordering when self-correct feedback is generated - internal/agent/loop.go:334

The self-correct feedback is appended inside the loop that is still answering the assistant turn's tool calls. If a model emits multiple tool calls in one turn and an earlier successful mutating tool triggers self-correct feedback, the transcript becomes: assistant tool_use batch -> tool_result for call 1 -> user self-correct feedback -> tool_result for call 2. Strict provider replay expects all tool_result messages for the assistant's advertised tool calls to be contiguous before the next user message, so this can break the next completion request or later session replay/compaction for common multi-tool batches.

Move self-correct feedback emission until after the current tool-call batch has finished, or collect pending feedback during the loop and append it only after every advertised tool call has a matching tool_result or aborted placeholder. Please add a regression test with two tool calls in one assistant turn where the first mutates and self-correct fails, asserting all tool results are recorded before the feedback message.

Validation run locally:

  • go test ./...

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approvable ?

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check current state of AfterEdit/self-correct injection in loop.go
sed -n '290,380p' internal/agent/loop.go
echo "=== rg for AfterEdit usage ==="
rg -n "AfterEdit\|batchChanged\|ChangedFiles\|changedFiles\|SelfCorrect" --type=go internal/agent/loop.go -C 3

Length of output: 5131



@gnanam1990 Not yet — the loop.go protocol violation is still present. The AfterEdit call is still inside the per-tool-call for loop, which can produce:

assistant(tool_calls: c1, c2) → tool_result(c1) → user(self-correct) → tool_result(c2)

Anthropic rejects that ordering with a 400. The fix is straightforward — accumulate changedFiles inside the loop, then call AfterEdit once after the loop closes:

// inside the for loop — accumulate, don't call AfterEdit yet
var batchChangedFiles []string
// ...existing for loop body...
if toolResult.Status == tools.StatusOK {
    batchChangedFiles = append(batchChangedFiles, toolResult.ChangedFiles...)
}
// remove the current AfterEdit block entirely from the loop

// after the for loop, before the model-escalation block
if options.SelfCorrect != nil && len(batchChangedFiles) > 0 {
    if feedback, _ := options.SelfCorrect.AfterEdit(ctx, batchChangedFiles); feedback != "" {
        messages = append(messages, zeroruntime.Message{
            Role:    zeroruntime.MessageRoleUser,
            Content: feedback,
        })
    }
}

That one change is the last blocker. Everything else (selfcorrect.go InspectErrors, LSP fixes, web_search provider forwarding) looks good.

 review)

Self-correct feedback was appended as a user message inside the per-tool-call
loop, so a multi-tool-call turn whose first (mutating) call failed verification
produced: assistant tool_use batch -> tool_result 1 -> user feedback ->
tool_result 2. A user message interleaved between tool_results breaks strict
provider replay and later session replay/compaction.

Accumulate feedback during the batch and append it once after the loop (same
after-batch pattern already used for mid-run model escalation), so every
advertised tool call keeps its tool_result contiguous. Adds a two-tool-call
regression test asserting the feedback lands after both tool_results.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Fixed in 17b9de7.

Self-correct feedback is now accumulated during the tool-call batch and appended once after the loop, so every advertised tool call keeps its tool_result contiguous — no user message is interleaved between tool_results. This mirrors the existing after-batch pattern already used for mid-run model escalation (turnRequestedModel). On the early-return abort/stop/guard paths the run ends, so deferred feedback is intentionally dropped (no next turn to act on it).

Added TestRunDefersSelfCorrectFeedbackUntilAfterToolBatch: two tool calls in one assistant turn where the first mutates and self-correct fails, asserting both tool_results are contiguous and the feedback message lands after both. go test ./internal/agent + -race, go vet, gofmt green.

@coderabbitai review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/agent/loop.go`:
- Around line 263-268: The SelfCorrect.AfterEdit call is currently invoked per
successful mutating tool inside the loop (e.g., where SelfCorrect.AfterEdit and
selfCorrectFeedback are used), which can consume per-run state multiple times
and produce stale diagnostics; instead accumulate ChangedFiles (and any other
edit metadata) across the loop into a single collection, remove per-tool
AfterEdit calls, and call SelfCorrect.AfterEdit once after the batch completes,
then append its returned feedback to selfCorrectFeedback exactly once so
tool_result ordering and single-instance semantics (Options.SelfCorrect) are
preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b315c14-3a3a-44d7-98fc-fb49f9d3d090

📥 Commits

Reviewing files that changed from the base of the PR and between 8b84006 and 17b9de7.

📒 Files selected for processing (2)
  • internal/agent/loop.go
  • internal/agent/loop_test.go

Comment thread internal/agent/loop.go Outdated
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the current state of AfterEdit in loop.go
rg -n "AfterEdit\|selfCorrectFeedback\|batchChanged\|ChangedFiles\|changedFiles\|SelfCorrect" --type=go internal/agent/loop.go -C 3
echo "=== loop structure around tool batch ==="
sed -n '280,400p' internal/agent/loop.go
echo "=== test ==="
rg -n "TestRunDefersSelfCorrectFeedbackUntilAfterToolBatch" --type=go -A 60 internal/agent/loop_test.go

Length of output: 9204



@gnanam1990 Confirmed — the fix is correct.

  • AfterEdit is called inside the loop but results go into selfCorrectFeedback []string, not into messages. The user message is appended once after the for block closes, keeping all tool_results contiguous.
  • Early-return paths (abort, stop, guard) exit before that post-loop append, so deferred feedback is cleanly dropped — no dangling message.
  • TestRunDefersSelfCorrectFeedbackUntilAfterToolBatch asserts the ordering directly: both tool-result indices appear before the feedback index.

All prior blockers are resolved. Approving now.

[approve]

…es (#175 review)

Deferring only the feedback emission still ran SelfCorrect.AfterEdit once per
mutating tool call, which (a) consumes the per-run attempt budget multiple times
in a single turn and (b) verifies an intermediate edit that a later call in the
same batch already superseded — AfterEdit is documented as one per-run instance
holding attempt state, so this is a correctness issue, not just transcript shape.

Aggregate every mutating tool's ChangedFiles during the batch and call AfterEdit
once afterward over the deduped union, appending its feedback once (still after
all tool_results, preserving contiguity). Adds dedupeStrings and a regression
test asserting two writes in one turn consume exactly one attempt and emit one
feedback message.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Good catch — fixed in 14823b6.

AfterEdit is now called once per turn over the deduped union of every mutating tool's ChangedFiles (aggregated during the batch), instead of once per mutating call. This keeps the per-run attempt budget accurate and avoids verifying an intermediate edit a later call in the same turn already superseded. Feedback is still appended once, after all tool_results.

Added dedupeStrings and TestRunBatchesSelfCorrectOncePerTurn: two writes in one assistant turn now consume exactly one corrective attempt and emit a single feedback message. go test ./internal/agent (+-race), go vet, gofmt green.

@coderabbitai review

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approve

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== AfterEdit / selfCorrect / changedFiles in loop.go ==="
rg -n "AfterEdit\|selfCorrectFeedback\|batchChanged\|changedFiles\|ChangedFiles\|SelfCorrect\|dedupeStrings" --type=go internal/agent/loop.go -C 3

echo ""
echo "=== loop batch region ==="
sed -n '280,420p' internal/agent/loop.go

echo ""
echo "=== dedupeStrings definition ==="
rg -n "dedupeStrings" --type=go internal/agent/ -C 5

echo ""
echo "=== TestRunBatchesSelfCorrectOncePerTurn ==="
rg -n "TestRunBatchesSelfCorrectOncePerTurn" --type=go -A 80 internal/agent/loop_test.go

Length of output: 12621



@gnanam1990 Confirmed — the fix is correct across all three properties:

  1. Deduped unionchangedFilesThisBatch is accumulated inside the loop; dedupeStrings (first-seen order, seen-map) strips cross-call duplicates before passing to AfterEdit.
  2. Single call per turnAfterEdit is invoked once after the for block, so the attempt counter ticks exactly once regardless of how many mutating tools fired.
  3. Contiguous tool_results — feedback is still appended post-loop, so no user message is interleaved between tool_result blocks.

TestRunBatchesSelfCorrectOncePerTurn is a clean regression gate: two write_file calls → asserts corrector.attempts == 1 and exactly one feedback message in the next turn's messages.

All blockers are resolved. Approving now.

[approve]

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Approve

I re-reviewed the latest head 14823b6.

No blockers found.

What I checked:

  • web_search is prompt-gated network egress, registered in the core network tool set, and the configured provider is now forwarded to the backend request body.
  • LSP client/session fixes address the earlier close/pending-call and stale diagnostics concerns.
  • Self-correct feedback is now batched after all tool results, preserving strict provider replay ordering, and AfterEdit runs once over the deduped changed-file union.

Validation run locally:

  • gofmt -l internal/agent internal/lsp internal/tools internal/specialist clean
  • git diff --check origin/main...HEAD clean
  • go test -count=1 ./internal/agent ./internal/lsp ./internal/tools ./internal/specialist
  • go test ./...
  • go run ./cmd/zero-release build
  • go run ./cmd/zero-release smoke

Note: I attempted go test -race ./internal/lsp ./internal/agent, but this Windows environment cannot run race builds because gcc is missing for cgo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants