Skip to content

fix: unify tool-success mapping and honor telemetryFunctionId (plan 007)#75

Merged
DavertMik merged 6 commits into
mainfrom
fix/tool-success-telemetry
Jul 12, 2026
Merged

fix: unify tool-success mapping and honor telemetryFunctionId (plan 007)#75
DavertMik merged 6 commits into
mainfrom
fix/tool-success-telemetry

Conversation

@DavertMik

Copy link
Copy Markdown
Contributor

Implements plan 007 (plans/007-unify-tool-success-and-telemetry.md) — a P1 bug fix.

Stacked PR — base branch is fix/abort-ai-request-on-timeout (plan 005), because both touch provider.ts. Review/merge #73 first; this diff shows only plan 007's changes.

Problem 1 — two classifiers disagree

Provider.invokeConversation paired tool calls with results by array position and defaulted a missing success field to failed. Conversation.getToolExecutions paired by toolCallId, unwrapped the AI-SDK {type:json,value} envelope, and defaulted to passed. Tester consumes the former, Pilot's session log the latter — so Pilot could review a step as "succeeded" that Tester counted as failed. Positional pairing also mis-attributes outputs if the SDK returns results out of order.

Fix: one exported toToolExecution(toolName, input, rawOutput) owns the unwrap + success !== false default; both sites route through it. invokeConversation now pairs by toolCallId via a Map. Data-shaped tools with no success field now count as successful in both views (matching what Pilot already trusted — action/assertion tools always return explicit success via successToolResult/failedToolResult, so stuck-detection is unaffected).

Problem 2 — dead telemetryFunctionId option

Ten call sites passed telemetryFunctionId expecting a named Langfuse trace, but getTelemetry only read options.telemetry. Normalized the shorthand into { functionId } (explicit telemetry.functionId still wins). Deleted the never-read Conversation.telemetryFunctionId field/param/clone-arg.

Verification

  • toToolExecution unit tests (unwrap, default-true, explicit-false, missing output) + getTelemetry tests (shorthand honored, explicit wins).
  • bun test tests/unit → 734 pass, 0 fail; bun run lint clean.
  • bun test tests/integration/planner.test.ts (exercises the tester tool loop through invokeConversation) → 10 pass, 0 fail.

🤖 Generated with Claude Code

DavertMik and others added 5 commits July 11, 2026 14:00
The provider races each LLM call against rejectAfterIdle(timeout). When the
timer won it only rejected (and the retry condition retries on that message),
but nothing aborted the original request — so a legitimately slow generateText/
generateObject kept running while a retry started, producing duplicate
concurrent LLM calls (extra tokens/cost, spurious failures).

Create a per-attempt AbortController inside the withRetry callback, combine it
with the global executionController signal via AbortSignal.any, pass the combined
signal to the SDK call (after the ...config spread so it wins), and call
controller.abort() inside rejectAfterIdle before rejecting. Applied at both the
generateWithTools and generateObject race sites.

Deferred (see plan 005): making the 30s idle timeout configurable per agent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Provider.invokeConversation paired tool calls with results by array position and
defaulted a missing `success` field to failed; Conversation.getToolExecutions
paired by toolCallId, unwrapped the {type:json,value} envelope, and defaulted to
passed. Tester (former) and Pilot (latter) could disagree about the same call.

Extract one shared toToolExecution mapper (owns unwrap + `success !== false`
default) and route both sites through it; invokeConversation now pairs by
toolCallId via a Map. Data-shaped tools with no `success` field now count as
successful in both views, matching what Pilot already trusted.

Honor the telemetryFunctionId provider option in getTelemetry (ten call sites
passed it expecting a named Langfuse trace; only `telemetry.functionId` was read).
Explicit telemetry.functionId still wins. Delete the dead
Conversation.telemetryFunctionId field/param/clone-arg (never read by anything).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ad abortSignal

Per re-review of the idle-timeout abort fix:
- Remove the now-dead `abortSignal: executionController.getAbortSignal()` from the
  generateWithTools/generateObject config builders — both are overridden by the
  combined signal at the race site, so they were misleading.
- Collapse the duplicated `AbortSignal.any([...].filter(Boolean) as AbortSignal[])`
  at both race sites into a `combinedAbortSignal(controller)` helper (also drops
  the cast).
- Rename `rejectAfterIdle` -> `abortAfterIdle` (it now aborts, not just rejects)
  and its sentinel param `signal` -> `cancel` (it is not an AbortSignal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Base automatically changed from fix/abort-ai-request-on-timeout to main July 11, 2026 23:50
…metry

# Conflicts:
#	src/ai/provider.ts
#	tests/unit/provider.test.ts
@github-actions

Copy link
Copy Markdown

Explorbot Self-Regression

Commit eeb99cc · run

Scenario Result Attempts Duration
basic (native) PASS 3/3 30m
experience: control OK — failed as expected 1/1 1m
experience: seeded PASS 1/3 1m

Attempt details

  • basic (native) attempt 1 — FAIL: login evidence: PASS (post-login plan=true, post-login research=true); research: PASS (files=5, wellFormed=true, keywords=7/3); scenarios: PASS (tests=6/5, features=4/3); tests passed: FAIL (4 passed, 1 failed (reporter: 4 passed, 1 failed))
  • basic (native) attempt 2 — FAIL: login evidence: PASS (post-login plan=true, post-login research=true); research: PASS (files=6, wellFormed=true, keywords=7/3); scenarios: PASS (tests=5/5, features=4/3); tests passed: FAIL (4 passed, 1 failed (reporter: 4 passed, 1 failed))
  • basic (native) attempt 3 — PASS: login evidence: PASS (post-login plan=true, post-login research=true); research: PASS (files=7, wellFormed=true, keywords=6/3); scenarios: PASS (tests=6/5, features=4/3); tests passed: PASS (5 passed, 0 failed (reporter: 5 passed, 0 failed))
  • experience: control attempt 1 — PASS: control: OK — failed as expected (0 passed, 1 failed)
  • experience: seeded attempt 1 — PASS: seeded: PASS (1 passed, 0 failed)

Session analysis — basic (native):

Session Analysis

The issue tracking list page was tested for creation, search, and filtering capabilities. All core flows work correctly: creating issues, keyword search, status filtering, label filtering, and combined filters all produce the expected results.

Coverage

  • Pages: /issues
  • Features: Issue creation, keyword search, status filter, label filter, combined filters

What works

  • Issue creationET-1 Create New Issue
  • Keyword searchET-2 Search Issues by Keyword
  • Status filteringET-3 Filter Issues by Status
  • Label filteringET-4 Filter Issues by Label
  • Combined filtersET-5 Combined Search and Filter

Execution Issues

  • ET-4 Filter Issues by Label — intermittent click failures on dropdown elements, recovered automatically and final verification passed
  • ET-5 Combined Search and Filter — internal verification note was incorrect, actual verification passed

@DavertMik DavertMik merged commit 76b2d72 into main Jul 12, 2026
3 checks passed
DavertMik added a commit that referenced this pull request Jul 12, 2026
* chore: dead-code sweep steps 1-2 (action-result, action)

action-result.ts: delete uncalled saveBrowserLogs()/saveHtmlOutput(), the
now-dead browserLogsContent getter, and the unused Diff.ariaRemoved getter.

action.ts: delete uncalled expect(), caputrePageWithScreenshot(),
waitForInteraction(), setActor(), getCurrentState() (exact dup of
getActionResult()), the unused prevActionResult local and expectation field;
drop 6 unused imports. Remove the never-read `experience` param from
Action.attempt and Explorer.attemptAction and the dead arg at all call sites
(the callers' skip-experience intent was never functional — follow-up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: dead-code sweep step 3 (explorer)

Delete uncalled Explorer methods: ensureActiveTestPageAvailable(), getCurrentUrl(),
getConfigPath(), reload(), hasPlaywrightLocator(), and setUserResolve() + the
never-read userResolveFn field (drop the forwarding line in explorbot.ts; keep
ExplorBot.setUserResolve and App.tsx). Collapse the impossible `!this.config`
guards in getConfig()/start() (config is a required ctor param). Drop unused
UserResolveFunction/outputPath imports. Also strip the now-dead 3rd attempt() arg
from boat/doc-collector call sites (it imports Explorer from src).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: dead-code sweep step 4 (state-manager)

Delete uncalled getCurrentContext() and getPreviousState(), and loadHtmlFromFile()
(a byte-for-byte copy of ActionResult.loadHtmlFromFile). Drop the now-unused tag
and outputPath imports and an orphaned JSDoc block. (The knowledge scanner is left
untouched — plan 010 removes it and is not yet in main; see report.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: dead-code sweep step 5 (explorbot, navigator)

explorbot.ts: delete isReady(), needsInput field, getConfigParser(), the unused
agentEmoji local, and the dead captain.setQuartermaster/setHistorian +
rerunner.setQuartermaster wiring (captain consumes neither; rerunner consumes only
getHistorian). Keep tester's wiring and rerunner.setHistorian.

navigator.ts: delete the never-read experienceCompactor field/param/assignment/
import and drop the argument at the explorbot construction site; drop the unused
WebPageState import. planner.ts: drop the unused getStyles import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: dead-code sweep step 6 (conversation, provider)

Conversation: delete the write-only id field + generateId(), and the
production-unused addUserImage() (the two unit tests that used it now push a
non-string message directly, preserving their cleanupTag/hasTag coverage).

Provider: make getProviderOptionsForAgent/getReasoningForAgent private (only
internal callers), and move CONTEXT_LENGTH_PATTERNS to a module-level const.

Skipped (entangled with the still-open plan-007 PR #75, which rewrites clone()/
invokeConversation): Conversation.clone() and Provider.lastConversation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: DavertMik <davert@testomat.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant