Skip to content

[Experiment] orchestrator-v2 hardening: telemetry, resilience, capability routing#43

Open
matt-wright86 wants to merge 4 commits into
mainfrom
refactor/orchestrator-v2-hardening
Open

[Experiment] orchestrator-v2 hardening: telemetry, resilience, capability routing#43
matt-wright86 wants to merge 4 commits into
mainfrom
refactor/orchestrator-v2-hardening

Conversation

@matt-wright86

Copy link
Copy Markdown
Collaborator

Summary

Exploratory hardening of the orchestrator-v2 rebuild — telemetry propagation, failure
resilience, and capability-aware routing — plus a runnable real-engine example and a full
write-up. Bundles several distinct fixes for shared review (happy to split into focused PRs);
see orchestrator-v2/docs/experiment/.

Motivation

A session that audited and dogfooded orchestrator-v2 found that its "thin" terminal boundary
dropped everything but taskId — execution telemetry, task-source callback errors, and runner
capability all had to be threaded deliberately. This branch does that threading, with regression
tests, and captures the findings.

  • Related issue/rune: # (none — exploratory)

Type of change

  • Bug fix (non-breaking) — I3 (task-source callback resilience), G1 (resumable failed runs)
  • New feature (non-breaking) — I1 (capability routing), Route A (telemetry propagation)
  • Breaking change
  • Documentation — gap report, retrospective, dogfood-run notes
  • Refactor / cleanup — two /simplify passes
  • Test coverage — orchestrator.spec (I3), peer-registry.spec (I1)
  • Chore / tooling

Checklist

  • Raw go/npx were not used
  • make lint passes — N/A: orchestrator-v2 uses its own vp/oxlint toolchain (not wired into make); vp check is clean
  • make test passes — N/A: vp run -r test is green (43/43)
  • make build passes — N/A: vp run -r build is clean
  • Tests added/updated for new behavior
  • Docs updated where relevant
  • Commit messages are short, lowercase, imperative
  • No force-push to main; branch is current with main
  • Self-reviewed the diff (two /simplify passes)

Notes for reviewers

  • This is an experiment, not merge-ready. It bundles several fixes for shared review; if
    pursued, it should split into focused PRs (Route A / I1 / I3).
  • Open follow-ups (not in this branch): E3 (a task's output/message isn't propagated — same
    shape as the telemetry gap, for the result), reconcile 3 correctness-level doc gaps, build
    agent-4-workflow + a TaskSource read-side.
  • The dogfood's code-review swarm repeatedly flagged canHandle's fail-open default (null
    capabilities → handles anything) as surprising. It's deliberate (backward-compat for
    non-advertising runners, load-bearing for the stub-runner tests) but deserves an explicit
    rationale comment or an opt-in.
  • A packaging fix to orchestrator/package.json (the test-helpers export) was deliberately left
    out of this branch.
  • CONTRIBUTING.md predates orchestrator-v2/ and doesn't document its vp toolchain — worth updating.

🤖 Generated with Claude Code

matt-wright86 and others added 2 commits July 3, 2026 16:43
- Route A: propagate execution telemetry from the engine through the runner
  RPC to the TaskSource as a terminal outcome; unify ExecutionStats in
  interfaces-task-source.
- G1: persist sessionId (and telemetry) on the failure path so a failed run
  stays resumable.
- I3: guard task-source callbacks so a throw can't leak a peer's in-flight
  slot, hang the runner's terminal RPC, or escape as an unhandled rejection
  (settle/recordBestEffort helpers + a route().catch net).
- I1: capability-aware routing -- runners advertise their registered agents
  in the heartbeat; the orchestrator only dispatches a task to a capable peer
  (backward compatible: a runner that advertises nothing = capable of anything).
- Adds regression tests (orchestrator.spec I3, peer-registry.spec I1).
  43/43 tests pass; build + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- examples/claude-engine: a runnable real-engine deployment (runner.yaml
  onboarding + a claude-CLI Engine that maps CLI JSON to EngineResult, so real
  token/cost telemetry flows). Registers examples/* as a workspace member.
- docs/experiment: the session's gap report, retrospective (lessons), and
  dogfood-run notes with telemetry receipts (three escalating real-engine runs,
  incl. the G1 resume payoff).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@matt-wright86, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 94eefd43-8e53-47b2-ad7f-52ac23eab286

📥 Commits

Reviewing files that changed from the base of the PR and between e6856ec and 72fac71.

📒 Files selected for processing (11)
  • orchestrator-v2/docs/experiment/dogfood-runs.md
  • orchestrator-v2/docs/experiment/gap-report.md
  • orchestrator-v2/docs/experiment/lessons.md
  • orchestrator-v2/examples/claude-engine/package.json
  • orchestrator-v2/examples/claude-engine/run.mjs
  • orchestrator-v2/packages/orchestrator/src/dispatcher.ts
  • orchestrator-v2/packages/orchestrator/src/peer-registry.spec.ts
  • orchestrator-v2/packages/orchestrator/src/peer-registry.ts
  • orchestrator-v2/packages/orchestrator/src/rpc-router.ts
  • orchestrator-v2/packages/orchestrator/src/test-helpers.ts
  • orchestrator-v2/packages/runner/src/heartbeat.ts
📝 Walkthrough

Walkthrough

This PR adds telemetry (ExecutionStats) propagation from task engines through TaskSource/RPC to the orchestrator, introduces capability-aware peer routing via heartbeat capability advertisement and PeerRegistry matching, adds error-isolation helpers (recordBestEffort, RPC try/catch), and adds a Claude CLI example plus experiment documentation.

Changes

Telemetry propagation through TaskSource and RPC

Layer / File(s) Summary
ExecutionStats type, guard, and TaskSource interface
orchestrator-v2/packages/interfaces-task-source/src/types.ts, .../index.ts, orchestrator-v2/packages/interfaces-task/src/index.ts, .../types.ts
Adds ExecutionStats type and isExecutionStats guard; TaskSource.completeTask/failTask accept optional telemetry; re-exports consolidated to interfaces-task-source.
Task agent session persistence and telemetry return
orchestrator-v2/packages/agent-3-task/src/run-task-agent.ts
Persists sessionId before success/failure branching and returns telemetry in both outcomes.
Runner dispatch and orchestrator result handling
orchestrator-v2/packages/runner/src/dispatch-handler.ts, orchestrator-v2/packages/orchestrator/src/result-handler.ts, .../best-effort.ts, .../dispatch-ack-handler.ts
Runner forwards telemetry in task.complete/task.fail RPC calls; orchestrator's settle()/readTelemetry validate and record telemetry via recordBestEffort.

Capability-aware peer routing

Layer / File(s) Summary
Protocol capability key and heartbeat schema
orchestrator-v2/packages/protocol/src/types.ts, .../index.ts, .../frames.ts
Adds capabilityKey helper, optional capabilities on Heartbeat, and frame validation for capabilities as a string array.
Runner capability advertisement
orchestrator-v2/packages/runner/src/runner.ts, .../heartbeat.ts
Runner accumulates capability keys from registered agents and includes them in heartbeats.
PeerRegistry capability-based selection
orchestrator-v2/packages/orchestrator/src/peer-registry.ts, peer-registry.spec.ts, orchestrator.ts
PeerRegistry stores per-peer capabilities and filters selection via canHandle; orchestrator requests peers by capabilityKey(task.agentType, task.agentName).

RPC error isolation and dispatch/disconnect resilience

Layer / File(s) Summary
RPC router error isolation
orchestrator-v2/packages/orchestrator/src/rpc-router.ts
Catches route() rejections; wraps handleSetState/handleSchedulerCall in try/catch returning TASK_SOURCE_ERROR/SCHEDULER_ERROR.
Orchestrator regression test for callback resilience
orchestrator-v2/packages/orchestrator/src/orchestrator.spec.ts
Adds test verifying a throwing completeTask callback does not wedge the peer.

Claude-engine example, workspace config, and experiment documentation

Layer / File(s) Summary
Claude-engine example application
orchestrator-v2/examples/claude-engine/*, orchestrator-v2/pnpm-workspace.yaml, orchestrator-v2/publish.js
Adds a runnable example wiring Runner, Task Agent, and a claude CLI engine; updates workspace glob and publish notes.
Experiment retrospective, gap report, and dogfood-run docs
orchestrator-v2/docs/experiment/*
Adds README, lessons, gap-report, and dogfood-runs documentation for the hardening session.

Sequence Diagram(s)

sequenceDiagram
  participant RunTaskAgent
  participant Engine
  participant DispatchHandler
  participant ResultHandler
  participant TaskSource
  RunTaskAgent->>Engine: execute(context, sessionId)
  Engine-->>RunTaskAgent: engineResult (sessionId, stats)
  RunTaskAgent->>RunTaskAgent: ctx.setState(sessionId)
  RunTaskAgent-->>DispatchHandler: ScriptResult with telemetry
  DispatchHandler->>ResultHandler: task.complete/task.fail RPC {taskId, telemetry}
  ResultHandler->>TaskSource: completeTask/failTask(taskId, telemetry)
Loading
sequenceDiagram
  participant Runner
  participant PeerRegistry
  participant Orchestrator
  Runner->>PeerRegistry: heartbeat with capabilities
  PeerRegistry->>PeerRegistry: store capabilities set per peer
  Orchestrator->>PeerRegistry: waitForAvailablePeer(capabilityKey(agentType, agentName))
  PeerRegistry-->>Orchestrator: matching peer via canHandle()
Loading

Related Issues: None found in the provided context.

Related PRs: None found in the provided context.

Suggested labels: orchestrator-v2, telemetry, capability-routing, documentation

Suggested reviewers: devzeebo

🐰 A carrot-fueled hop through code so deep,
Telemetry now travels, no gaps to keep,
Runners advertise what they can do,
Peers matched by skill, not just by queue,
Docs and dogfood runs round out the leap.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: hardening orchestrator-v2 around telemetry, resilience, and capability routing.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the experiment, fixes, tests, and documentation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (1)
orchestrator-v2/packages/orchestrator/src/peer-registry.spec.ts (1)

29-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a test for explicit empty-capabilities advertisement.

Current tests cover omitted capabilities (treated as "capable of anything") and non-empty capability sets, but not a runner advertising capabilities: [] explicitly — which canHandle treats as "capable of nothing," the opposite of the omitted-field case. A regression test would help lock in this distinction (see related comment on runner.ts).

🤖 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 `@orchestrator-v2/packages/orchestrator/src/peer-registry.spec.ts` around lines
29 - 41, Add a regression test in PeerRegistry capability routing to cover the
explicit empty-capabilities case, since the current suite only checks omitted
capabilities and non-empty sets. Update peer-registry.spec.ts with a scenario
using a runner that advertises capabilities: [] and assert that
selecting_a_peer_for_the_special_agent does not choose that runner, reflecting
the canHandle behavior in runner.ts where an explicit empty list means capable
of nothing. Keep the existing capability-routing tests intact and name the new
case clearly so it documents the distinction from the omitted-field behavior.
🤖 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 `@orchestrator-v2/docs/experiment/dogfood-runs.md`:
- Around line 78-89: The telemetry receipt block in the dogfood runs doc is
using a bare fenced code block, which triggers markdownlint. Update the fenced
block to include an explicit language tag, using the existing telemetry table
snippet near the summary section so the document remains lint-clean. Keep the
content unchanged and only adjust the opening/closing fence around the table
output.

In `@orchestrator-v2/docs/experiment/lessons.md`:
- Around line 81-88: Update the prose in the “Hardest — a 3-stage pipeline with
retry + session-resume” lesson so the phrase “analyze task” is replaced with
“analysis task.” Keep the rest of the paragraph unchanged and ensure the wording
reads as a noun phrase for readability/lint compliance.

In `@orchestrator-v2/examples/claude-engine/README.md`:
- Around line 31-36: The expected-output fenced block in the README example is
missing a language tag, which causes the docs lint failure. Update the fenced
block around the sample output in the README to use an explicit tag such as text
or bash so the example stays lint-clean.

In `@orchestrator-v2/examples/claude-engine/run.mjs`:
- Around line 66-77: The demo TaskSource in run.mjs is stateless, so
checkpoint/session updates and failure telemetry are dropped. Update the source
object’s setState, completeTask, and failTask methods to keep a small in-memory
state store and persist incoming state changes, and make failTask forward its
telemetry argument in the log path just like completeTask does. Use the existing
source object and its methods as the place to wire this in so the demo exercises
the telemetry plumbing end to end.
- Around line 31-55: The catch block in the claude engine run path is dropping
useful JSON from failed CLI executions, so partial session data is lost. Update
the error handling around execFileAsync in run.mjs to inspect and parse
error.stdout before falling back to error.message, and use that parsed payload
to populate sessionId, lastMessage, and stats when available. Keep the existing
success path in the same function, but make the failure path preserve any valid
JSON emitted by claude -p --output-format json.

In `@orchestrator-v2/packages/interfaces-task-source/src/types.ts`:
- Around line 92-109: The isExecutionStats type guard currently treats any
number as valid, so NaN and Infinity can slip through as telemetry. Update the
validation in isExecutionStats to require each EXECUTION_STATS_FIELDS entry to
be a finite numeric value, using the existing record check in types.ts so
corrupt stats are rejected before reaching completeTask or failTask.

In `@orchestrator-v2/packages/orchestrator/src/rpc-router.ts`:
- Around line 65-70: The success reply is inside the same try/catch as the state
update and scheduler call, so a throw from sendRpcResponse can be misreported as
TASK_SOURCE_ERROR or SCHEDULER_ERROR and may send a duplicate rpc.response.
Refactor rpc-router.ts in handleTaskSourceUpdate and handleSchedulerCall so only
the risky operation (taskSource.setState or scheduler.call) is inside the try,
then sendRpcResponse after the try succeeds, mirroring ResultHandler.settle to
keep success replies outside the error-handling scope.
- Around line 20-24: The top-level catch in rpc-router’s route() fire-and-forget
path only logs and can leave a request without any response frame, causing the
caller’s RPC to hang. Update the error handling around this.router/route(peer,
payload.id, payload.method, payload.params) so that when an unexpected exception
escapes route(), you also send an error reply for that requestId via
sendRpcError (or equivalent), while keeping the existing log; if sendRpcError
can fail, guard that call too so the peer still won’t be left waiting
indefinitely.

---

Nitpick comments:
In `@orchestrator-v2/packages/orchestrator/src/peer-registry.spec.ts`:
- Around line 29-41: Add a regression test in PeerRegistry capability routing to
cover the explicit empty-capabilities case, since the current suite only checks
omitted capabilities and non-empty sets. Update peer-registry.spec.ts with a
scenario using a runner that advertises capabilities: [] and assert that
selecting_a_peer_for_the_special_agent does not choose that runner, reflecting
the canHandle behavior in runner.ts where an explicit empty list means capable
of nothing. Keep the existing capability-routing tests intact and name the new
case clearly so it documents the distinction from the omitted-field behavior.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 32972857-2f51-44a7-99b9-a8712cac6889

📥 Commits

Reviewing files that changed from the base of the PR and between e0be8b5 and e6856ec.

⛔ Files ignored due to path filters (1)
  • orchestrator-v2/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • orchestrator-v2/docs/experiment/README.md
  • orchestrator-v2/docs/experiment/dogfood-runs.md
  • orchestrator-v2/docs/experiment/gap-report.md
  • orchestrator-v2/docs/experiment/lessons.md
  • orchestrator-v2/examples/claude-engine/README.md
  • orchestrator-v2/examples/claude-engine/package.json
  • orchestrator-v2/examples/claude-engine/run.mjs
  • orchestrator-v2/packages/agent-3-task/src/run-task-agent.ts
  • orchestrator-v2/packages/interfaces-task-source/src/index.ts
  • orchestrator-v2/packages/interfaces-task-source/src/types.ts
  • orchestrator-v2/packages/interfaces-task/src/index.ts
  • orchestrator-v2/packages/interfaces-task/src/types.ts
  • orchestrator-v2/packages/orchestrator/src/best-effort.ts
  • orchestrator-v2/packages/orchestrator/src/dispatch-ack-handler.ts
  • orchestrator-v2/packages/orchestrator/src/orchestrator.spec.ts
  • orchestrator-v2/packages/orchestrator/src/orchestrator.ts
  • orchestrator-v2/packages/orchestrator/src/peer-registry.spec.ts
  • orchestrator-v2/packages/orchestrator/src/peer-registry.ts
  • orchestrator-v2/packages/orchestrator/src/result-handler.ts
  • orchestrator-v2/packages/orchestrator/src/rpc-router.ts
  • orchestrator-v2/packages/protocol/src/frames.ts
  • orchestrator-v2/packages/protocol/src/index.ts
  • orchestrator-v2/packages/protocol/src/types.ts
  • orchestrator-v2/packages/runner/src/dispatch-handler.ts
  • orchestrator-v2/packages/runner/src/heartbeat.ts
  • orchestrator-v2/packages/runner/src/runner.ts
  • orchestrator-v2/pnpm-workspace.yaml
  • orchestrator-v2/publish.js

Comment on lines +78 to +89
```
task att model outcome in out cacheR cost$ ms
------------------------------------------------------------------------
an-telemetry 1 haiku completed 10 473 21220 0.0171 6931
an-routing 1 haiku failed 10 531 17157 0.0253 6980
an-resilience 1 haiku completed 10 609 17157 0.0255 7265
an-routing 2 haiku completed 10 402 27639 0.0059 5037
synthesize 1 sonnet completed 11202 169 23131 0.0780 3334
critique 1 haiku completed 10 538 21220 0.0175 7613
------------------------------------------------------------------------
TOTAL 6 calls 11252 2722 127524 0.1693 37160
```

Copy link
Copy Markdown
Contributor

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

Label the telemetry receipt fence.

Markdownlint flags this bare fence. Add a language tag such as text so the doc stays lint-clean.

Fix
-```
+```text
 task          att model   outcome       in   out  cacheR    cost$     ms
 ------------------------------------------------------------------------
 an-telemetry  1   haiku   completed     10   473   21220   0.0171   6931
@@
 TOTAL                     6 calls    11252  2722  127524   0.1693  37160
-```
+```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
task att model outcome in out cacheR cost$ ms
------------------------------------------------------------------------
an-telemetry 1 haiku completed 10 473 21220 0.0171 6931
an-routing 1 haiku failed 10 531 17157 0.0253 6980
an-resilience 1 haiku completed 10 609 17157 0.0255 7265
an-routing 2 haiku completed 10 402 27639 0.0059 5037
synthesize 1 sonnet completed 11202 169 23131 0.0780 3334
critique 1 haiku completed 10 538 21220 0.0175 7613
------------------------------------------------------------------------
TOTAL 6 calls 11252 2722 127524 0.1693 37160
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 78-78: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@orchestrator-v2/docs/experiment/dogfood-runs.md` around lines 78 - 89, The
telemetry receipt block in the dogfood runs doc is using a bare fenced code
block, which triggers markdownlint. Update the fenced block to include an
explicit language tag, using the existing telemetry table snippet near the
summary section so the document remains lint-clean. Keep the content unchanged
and only adjust the opening/closing fence around the table output.

Source: Linters/SAST tools

Comment on lines +81 to +88
**Hardest — a 3-stage pipeline with retry + session-resume.** Three analyses fanned out to an
_analyzer_ runner (concurrent), a _synthesizer_ runner (a **sonnet** engine) combined them, then a
_critic_ runner (haiku) reviewed the result — capability-routed across three specialized runners with
**heterogeneous engines**. One analyze task was flaky: it failed after doing its work, and the task
source **retried it with `--resume`** — attempt 2 cost **$0.0059 vs. $0.0253** (4.3× cheaper) by
reusing the session instead of rebuilding context. That is the concrete payoff of the G1 fix (failed
runs stay resumable). Per-attempt telemetry receipts and the pipeline's (self-critiquing) output are
in **[dogfood-runs.md](./dogfood-runs.md)**.

Copy link
Copy Markdown
Contributor

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

Fix the noun phrase.

“analyze task” should be “analysis task” here; the current wording reads like a verb phrase and trips readability/lint checks.

🧰 Tools
🪛 LanguageTool

[grammar] ~84-~84: Ensure spelling is correct
Context: ...ers with heterogeneous engines. One analyze task was flaky: it failed after doing i...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 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 `@orchestrator-v2/docs/experiment/lessons.md` around lines 81 - 88, Update the
prose in the “Hardest — a 3-stage pipeline with retry + session-resume” lesson
so the phrase “analyze task” is replaced with “analysis task.” Keep the rest of
the paragraph unchanged and ensure the wording reads as a noun phrase for
readability/lint compliance.

Source: Linters/SAST tools

Comment on lines +31 to +36
```
orchestrator listening on ws://127.0.0.1:xxxxx
✓ t1 completed telemetry={in:10, out:61, turns:1, $0.02..}
✓ t2 completed telemetry={in:10, out:128, turns:1, $0.01..}
done.
```

Copy link
Copy Markdown
Contributor

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

Tag the output fence.

The expected-output block is missing a language tag, which triggers MD040 in the docs lint. Mark it as text or bash so the README stays lint-clean.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 31-31: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@orchestrator-v2/examples/claude-engine/README.md` around lines 31 - 36, The
expected-output fenced block in the README example is missing a language tag,
which causes the docs lint failure. Update the fenced block around the sample
output in the README to use an explicit tag such as text or bash so the example
stays lint-clean.

Source: Linters/SAST tools

Comment on lines +31 to +55
try {
const { stdout } = await execFileAsync("claude", args, {
cwd: ctx.workingDir,
maxBuffer: 10 * 1024 * 1024,
timeout: 90_000,
});
const r = JSON.parse(stdout);
return {
success: !r.is_error,
skipFulfill: false,
lastMessage: r.result ?? null,
sessionId: r.session_id,
stats: {
durationMs: r.duration_ms ?? 0,
inputTokens: r.usage?.input_tokens ?? 0,
outputTokens: r.usage?.output_tokens ?? 0,
cacheReadTokens: r.usage?.cache_read_input_tokens ?? 0,
cacheCreationTokens: r.usage?.cache_creation_input_tokens ?? 0,
totalCostUsd: r.total_cost_usd ?? 0,
numTurns: r.num_turns ?? 0,
},
};
} catch (error) {
return { success: false, skipFulfill: false, lastMessage: String(error?.message ?? error), stats: null };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the example file first
ast-grep outline orchestrator-v2/examples/claude-engine/run.mjs --view expanded

# Show the relevant section with line numbers
sed -n '1,220p' orchestrator-v2/examples/claude-engine/run.mjs | cat -n

# Search for related result/failure handling and any CLI contract docs
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.git/**' \
  'is_error|session_id|usage|total_cost_usd|num_turns|stats: null|skipFulfill|failTask|setState|claude' \
  orchestrator-v2

Repository: devzeebo/bifrost

Length of output: 33106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline orchestrator-v2/examples/claude-engine/run.mjs --view expanded
printf '\n--- run.mjs ---\n'
sed -n '1,220p' orchestrator-v2/examples/claude-engine/run.mjs | cat -n
printf '\n--- related symbols ---\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.git/**' \
  'is_error|session_id|usage|total_cost_usd|num_turns|stats: null|skipFulfill|failTask|setState|claude' \
  orchestrator-v2

Repository: devzeebo/bifrost

Length of output: 33148


🌐 Web query:

Claude Code CLI JSON output non-zero exit stdout stderr is_error session_id usage total_cost_usd

💡 Result:

When using Claude Code in headless mode via claude -p with the --output-format json flag, the CLI outputs a single JSON object to stdout containing comprehensive session metadata [1][2]. Key JSON Fields: - is_error: A boolean indicating whether the session encountered a fatal error [2]. - session_id: A unique string identifier for the session, used for resuming conversations via --resume [1][2][3]. - total_cost_usd: A numeric value representing the cost in USD [2][4]. In a fresh session, this is the cost of the specific call; in resumed sessions, it is cumulative across all turns in that session [3]. - usage: An object providing a flat token breakdown, including input_tokens, output_tokens, and cache-related fields [2][3]. - result: The model's primary text response [2]. - structured_output: Populated only when --json-schema is utilized [1][2]. Exit Codes and Error Handling: Currently, the CLI's exit code behavior is limited for programmatic automation [5][6]. The command typically returns an exit code of 1 for various failure types, including API errors (e.g., 500, 429, 401) and local execution errors [5]. Stderr is often empty in these scenarios, making it difficult for calling programs to distinguish between transient (retryable) and permanent failures based solely on standard streams or exit codes [5][6]. Developers often must parse the is_error field within the JSON object to programmatically confirm if a session failed [2]. It is important to note that the JSON output is a single object, not an array of messages [1][7]. While official documentation and examples recommend using jq to parse these fields (e.g., jq -r '.session_id'), inconsistencies in some documentation sections have occasionally mislabeled the output format as an array [1][7].

Citations:


Preserve JSON from failed CLI runs. claude -p --output-format json can still emit session metadata on a non-zero exit, but this catch block drops error.stdout and loses session_id, usage, and any partial result. Parse the rejected stdout before falling back to the message.

🤖 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 `@orchestrator-v2/examples/claude-engine/run.mjs` around lines 31 - 55, The
catch block in the claude engine run path is dropping useful JSON from failed
CLI executions, so partial session data is lost. Update the error handling
around execFileAsync in run.mjs to inspect and parse error.stdout before falling
back to error.message, and use that parsed payload to populate sessionId,
lastMessage, and stats when available. Keep the existing success path in the
same function, but make the failure path preserve any valid JSON emitted by
claude -p --output-format json.

Comment on lines +66 to +77
const source = {
async *watchTasks() {
for (const t of tasks) yield t;
},
async completeTask(taskId, telemetry) {
log(` ✓ ${taskId} completed telemetry=${fmt(telemetry)}`);
},
async failTask(taskId, error) {
log(` ✗ ${taskId} failed: ${error}`);
},
async pauseTask() {},
async setState() {},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep the demo TaskSource stateful.

setState() is a no-op and failTask() ignores its telemetry argument, so checkpoint/session updates disappear and the failure path never exercises the new telemetry plumbing. If this example is meant to validate the hardening work, keep a tiny in-memory store here and forward telemetry on failures too.

🤖 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 `@orchestrator-v2/examples/claude-engine/run.mjs` around lines 66 - 77, The
demo TaskSource in run.mjs is stateless, so checkpoint/session updates and
failure telemetry are dropped. Update the source object’s setState,
completeTask, and failTask methods to keep a small in-memory state store and
persist incoming state changes, and make failTask forward its telemetry argument
in the log path just like completeTask does. Use the existing source object and
its methods as the place to wire this in so the demo exercises the telemetry
plumbing end to end.

Comment on lines +92 to +109

const EXECUTION_STATS_FIELDS = [
"durationMs",
"inputTokens",
"outputTokens",
"cacheReadTokens",
"cacheCreationTokens",
"totalCostUsd",
"numTurns",
] as const;

export function isExecutionStats(value: unknown): value is ExecutionStats {
if (value === null || typeof value !== "object") {
return false;
}
const record = value as Record<string, unknown>;
return EXECUTION_STATS_FIELDS.every((field) => typeof record[field] === "number");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Type guard accepts NaN/Infinity as valid telemetry.

typeof value === "number" is true for NaN and Infinity. If an engine miscomputes a stat (e.g., division by zero), this guard will still validate it and let corrupt telemetry flow through to completeTask/failTask.

🛠️ Proposed fix
 export function isExecutionStats(value: unknown): value is ExecutionStats {
   if (value === null || typeof value !== "object") {
     return false;
   }
   const record = value as Record<string, unknown>;
-  return EXECUTION_STATS_FIELDS.every((field) => typeof record[field] === "number");
+  return EXECUTION_STATS_FIELDS.every((field) => Number.isFinite(record[field]));
 }
🤖 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 `@orchestrator-v2/packages/interfaces-task-source/src/types.ts` around lines 92
- 109, The isExecutionStats type guard currently treats any number as valid, so
NaN and Infinity can slip through as telemetry. Update the validation in
isExecutionStats to require each EXECUTION_STATS_FIELDS entry to be a finite
numeric value, using the existing record check in types.ts so corrupt stats are
rejected before reaching completeTask or failTask.

Comment on lines +20 to +24
// route() is fire-and-forget; ensure a rejecting handler can never escape as an
// unhandled promise rejection (which is process-fatal on default Node settings).
void this.route(peer, payload.id, payload.method, payload.params).catch((error: unknown) => {
console.error(`Failed to handle RPC "${payload.method}":`, error);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Top-level .catch only logs — never replies, so an unexpected error can still hang the caller's RPC.

The fix eliminates the unhandled-rejection risk, but if an exception escapes route() before any handler calls sendRpcResponse/sendRpcError for that requestId (e.g. an unforeseen error path), the peer never gets a response frame and its RPC call hangs indefinitely — exactly the "hang terminal RPCs" failure mode this cohort is meant to close.

🔧 Suggested fix: also reply with an error response
     void this.route(peer, payload.id, payload.method, payload.params).catch((error: unknown) => {
       console.error(`Failed to handle RPC "${payload.method}":`, error);
+      sendRpcError(peer, payload.id, "INTERNAL_ERROR", errorMessage(error));
     });

Note sendRpcError itself could throw (e.g. disconnected peer) so ideally wrap it too, but this closes the common gap.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// route() is fire-and-forget; ensure a rejecting handler can never escape as an
// unhandled promise rejection (which is process-fatal on default Node settings).
void this.route(peer, payload.id, payload.method, payload.params).catch((error: unknown) => {
console.error(`Failed to handle RPC "${payload.method}":`, error);
});
// route() is fire-and-forget; ensure a rejecting handler can never escape as an
// unhandled promise rejection (which is process-fatal on default Node settings).
void this.route(peer, payload.id, payload.method, payload.params).catch((error: unknown) => {
console.error(`Failed to handle RPC "${payload.method}":`, error);
sendRpcError(peer, payload.id, "INTERNAL_ERROR", errorMessage(error));
});
🤖 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 `@orchestrator-v2/packages/orchestrator/src/rpc-router.ts` around lines 20 -
24, The top-level catch in rpc-router’s route() fire-and-forget path only logs
and can leave a request without any response frame, causing the caller’s RPC to
hang. Update the error handling around this.router/route(peer, payload.id,
payload.method, payload.params) so that when an unexpected exception escapes
route(), you also send an error reply for that requestId via sendRpcError (or
equivalent), while keeping the existing log; if sendRpcError can fail, guard
that call too so the peer still won’t be left waiting indefinitely.

Comment on lines +65 to +70
try {
await this.taskSource.setState(parsed.taskId, parsed.taskState);
sendRpcResponse(peer, requestId, { ok: true });
} catch (error) {
sendRpcError(peer, requestId, "TASK_SOURCE_ERROR", errorMessage(error));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

try scope also covers the success-path sendRpcResponse, risking a duplicate/misleading error reply.

If sendRpcResponse throws after setState/scheduler.call already succeeded, the catch block reports it as TASK_SOURCE_ERROR/SCHEDULER_ERROR and sends a second rpc.response for the same requestId — misleading and potentially double-answering the caller. ResultHandler.settle (result-handler.ts) keeps the success reply outside the error-handling scope for this reason.

🔧 Suggested fix: only guard the risky call, reply outside the try
     try {
       await this.taskSource.setState(parsed.taskId, parsed.taskState);
-      sendRpcResponse(peer, requestId, { ok: true });
     } catch (error) {
       sendRpcError(peer, requestId, "TASK_SOURCE_ERROR", errorMessage(error));
+      return;
     }
+    sendRpcResponse(peer, requestId, { ok: true });

Apply the analogous change to handleSchedulerCall.

Also applies to: 84-89

🤖 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 `@orchestrator-v2/packages/orchestrator/src/rpc-router.ts` around lines 65 -
70, The success reply is inside the same try/catch as the state update and
scheduler call, so a throw from sendRpcResponse can be misreported as
TASK_SOURCE_ERROR or SCHEDULER_ERROR and may send a duplicate rpc.response.
Refactor rpc-router.ts in handleTaskSourceUpdate and handleSchedulerCall so only
the risky operation (taskSource.setState or scheduler.call) is inside the try,
then sendRpcResponse after the try succeeds, mirroring ResultHandler.settle to
keep success replies outside the error-handling scope.

matt-wright86 and others added 2 commits July 4, 2026 11:09
…t layout

- I1: treat null (unadvertised) capabilities as "no capabilities" rather than
  "handles anything" (fail-closed) per review; stub runners now advertise, and
  peer-registry.spec asserts an unadvertised runner is not selected.
- rpc-router: fold the error-message unwrap into sendRpcError and drop the
  standalone errorMessage helper.
- heartbeat: pass the capabilities array directly instead of cloning it.
- peer-registry.spec: move the Context type and helpers below the describe block.
- lessons: note the fail-closed capability behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
oxfmt pass over pre-existing files flagged by vp check.

Co-Authored-By: Claude Opus 4.8 (1M context) <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