Skip to content

[v2] Work item source with dependencies + Bifrost adapter - #49

Merged
devzeebo merged 3 commits into
mainfrom
feat/bifrost-work-item-source
Jul 7, 2026
Merged

[v2] Work item source with dependencies + Bifrost adapter#49
devzeebo merged 3 commits into
mainfrom
feat/bifrost-work-item-source

Conversation

@devzeebo

@devzeebo devzeebo commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #34
Closes #40

  • Extends the WorkItemSource interface with draft/live lifecycle (createDraftWorkItem, startWorkItem) and dependency graph methods (setDependency, getDependencies), keeping dependency resolution inside the source rather than the orchestrator.
  • Adds @bifrost-ai/work-item-source-bifrost, a self-contained v2 port of the v1 Bifrost task source that maps runes to work items via the Bifrost HTTP API — including adaptive polling, agent tag filtering, draft creation, forge-to-live promotion, and dependency edges.
  • Updates orchestrator test helpers with stub implementations of the new interface methods; includes comprehensive vitest-gwt coverage with a mocked HTTP client.

Test plan

  • vp test in orchestrator-v2/packages/work-item-source-bifrost — dependency mapping, draft/live API calls, agent tag filtering, polling, complete/fail/pause/setState
  • vp test in orchestrator-v2/packages/work-item-source-bifrost — HTTP client unit tests
  • vp check && vp test from orchestrator-v2/ root to confirm no regressions across packages

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a new work-item source integration with support for loading configuration, credentials, and performing work-item actions.
    • Introduced draft creation, starting work items, dependency linking, and dependency lookup.
    • Expanded example templates to show how to set up and run the workflow system.
  • Bug Fixes

    • Improved polling and retry behavior for smoother background work-item processing.
    • Added support for more complete work-item state and metadata mapping.
  • Tests

    • Added coverage for API requests, lifecycle actions, dependency handling, and polling behavior.

Walkthrough

This PR extends the WorkItemSource interface with dependency and draft/start operations (createDraftWorkItem, startWorkItem, setDependency, getDependencies), updates the in-memory test helper accordingly, and adds a new work-item-source-bifrost package implementing these operations against the Bifrost HTTP API via config/credential loaders and an HTTP client. Also adds commented-out lvl3 example files.

Changes

Work item dependency support and Bifrost adapter

Layer / File(s) Summary
WorkItemSource interface and type extensions
orchestrator-v2/packages/interfaces-work/src/index.ts, orchestrator-v2/packages/interfaces-work/src/types.ts
Adds WorkItemDependency and CreateDraftWorkItemInput types and extends WorkItemSource with createDraftWorkItem, startWorkItem, setDependency, getDependencies; updates re-exports.
In-memory test helper updates
orchestrator-v2/packages/orchestrator/src/test-helpers.ts
Extends MemoryWorkItemSource with drafts, started, dependencies state and implements the corresponding new methods.
Bifrost package scaffolding and types
orchestrator-v2/packages/work-item-source-bifrost/package.json, vite.config.ts, src/types.ts
New package manifest, Vite config, and type definitions for Bifrost config, credentials, rune models, and create-rune requests.
Config and credential loaders
orchestrator-v2/packages/work-item-source-bifrost/src/config/config-loader.ts, src/config/credential-loader.ts
Loads and validates .bifrost.yaml config and credentials.yaml, resolving tokens by normalized URL.
BifrostHttpClient implementation and tests
orchestrator-v2/packages/work-item-source-bifrost/src/client/bifrost-http-client.ts, bifrost-http-client.spec.ts
Implements HTTP client for rune API calls (fetch/create/claim/forge/fulfill/fail/unclaim, dependencies, state), plus a full spec suite for endpoints, bodies, and error handling.
BifrostWorkItemSource implementation, exports, and tests
orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.ts, src/index.ts, bifrost-work-item-source.spec.ts
Implements the WorkItemSource adapter with polling, backoff/jitter, agent-tag filtering, draft/dependency/lifecycle methods, rune-to-work-item mapping, barrel exports, and a comprehensive test suite.
Commented lvl3 example files
orchestrator-v2/examples/lvl3/agents/cowsay/AGENT.md, doSomething.ts, orchestrator.ts, runner.ts
Adds a new cowsay agent spec and commented-out example wiring for orchestrator, runner, and script agent usage.

Sequence Diagram(s)

sequenceDiagram
    participant Source as BifrostWorkItemSource
    participant Config as loadConfig/CredentialLoader
    participant Client as BifrostHttpClient
    participant Bifrost as Bifrost API

    Source->>Config: loadConfig() + loadToken(url)
    Config-->>Source: url, realm, token
    Source->>Client: new BifrostHttpClient(url, realm, token)

    loop watchWorkItems polling
        Source->>Client: getReadyRunes()
        Client->>Bifrost: GET /api/ready
        Bifrost-->>Client: ready runes
        Client-->>Source: runes list
        Source->>Client: claimRune(id)
        Client->>Bifrost: POST /api/claim-rune
        Bifrost-->>Client: 200 or 409
        Source->>Client: getRune(id)
        Client->>Bifrost: GET /api/runes/:id
        Bifrost-->>Client: rune detail
        Source->>Source: mapToWorkItem(rune)
    end

    Source->>Client: createDraftWorkItem(input)
    Client->>Bifrost: POST /api/create-rune
    Bifrost-->>Client: created rune
    Client-->>Source: rune id

    Source->>Client: setDependency(id, dependsOnId)
    Client->>Bifrost: POST /api/add-dependency
Loading

Possibly related PRs

  • devzeebo/bifrost#44: Extends the same WorkItemSource interface in interfaces-work with createDraftWorkItem, startWorkItem, dependency APIs, and WorkItemDependency/CreateDraftWorkItemInput types.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly names the main change: a v2 work item source with dependencies plus a Bifrost adapter.
Description check ✅ Passed The description matches the changes and summarizes the new interface, Bifrost adapter, and tests.
Linked Issues check ✅ Passed The changes cover the new interface, Bifrost adapter, dependency handling, and mocked tests described in #34 and #40.
Out of Scope Changes check ✅ Passed The added examples, config, and package scaffolding support the new source and do not appear unrelated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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.

@devzeebo

devzeebo commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor
✅ 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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.spec.ts (1)

408-423: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

"Adaptive polling" test doesn't exercise the adapter.

This test only re-implements backoff arithmetic inline; it never calls into BifrostWorkItemSource, so it can't catch a regression in the actual backoff logic. Consider driving this via fake timers and asserting on the interval between fetch calls to /api/ready during watchWorkItems(), matching how the "gating" test observes the fetch mock.

🤖 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/work-item-source-bifrost/src/bifrost-work-item-source.spec.ts`
around lines 408 - 423, The adaptive polling test currently duplicates the
backoff math instead of exercising BifrostWorkItemSource, so update the
"adaptive polling" spec to drive watchWorkItems() and observe real polling
behavior. Use fake timers and the fetch mock, similar to the existing gating
test, then assert on the interval between repeated /api/ready requests to verify
exponential backoff and the maxPollInterval cap through the actual adapter
logic.
orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.ts (1)

157-164: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate dependency-mapping logic.

The {target_id, relationship} → {workItemId, type} mapping here is duplicated in mapToWorkItem (Lines 224-227). Extract a shared static helper to avoid drift if the shape changes.

♻️ Suggested refactor
+  private static mapDependencies(dependencies: RuneDetail["dependencies"]): WorkItemDependency[] {
+    return dependencies.map((dep) => ({
+      workItemId: dep.target_id,
+      type: dep.relationship,
+    }));
+  }
+
   public async getDependencies(workItemId: string): Promise<WorkItemDependency[]> {
     const client = await this.#getClient();
     const detail = await client.getRune(workItemId);
-    return detail.dependencies.map((dep) => ({
-      workItemId: dep.target_id,
-      type: dep.relationship,
-    }));
+    return BifrostWorkItemSource.mapDependencies(detail.dependencies);
   }
🤖 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/work-item-source-bifrost/src/bifrost-work-item-source.ts`
around lines 157 - 164, The dependency mapping logic in getDependencies is
duplicated with mapToWorkItem, so extract a shared static helper in
BifrostWorkItemSource for converting each dependency’s target_id and
relationship into { workItemId, type }. Update both getDependencies and
mapToWorkItem to use that helper so the mapping stays consistent if the shape
changes.
orchestrator-v2/packages/work-item-source-bifrost/src/client/bifrost-http-client.spec.ts (1)

1-256: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Minor coverage gaps.

Suite is thorough but doesn't cover unclaimRune, the getRune success path, or timeout/abort behavior of request().

🤖 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/work-item-source-bifrost/src/client/bifrost-http-client.spec.ts`
around lines 1 - 256, Add tests to cover the missing BifrostHttpClient paths:
implement a success case for getRune, a request to unclaimRune, and
timeout/abort behavior in request(). Use the existing BifrostHttpClient test
suite and mockFetch setup in bifrost-http-client.spec.ts, asserting the expected
endpoint, method/body, and that request() aborts or rejects appropriately when
the timeout is exceeded.
orchestrator-v2/packages/work-item-source-bifrost/src/client/bifrost-http-client.ts (2)

96-121: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Timeout aborts surface as generic AbortError.

When the 30s timeout fires, fetch rejects with an AbortError that isn't distinguished from other network failures, making it harder to diagnose Bifrost latency/timeouts vs. real API errors from logs.

💡 Suggested fix
     try {
       const response = await fetch(url, {
         ...options,
         headers: Object.assign(
           {
             "Content-Type": "application/json",
             Authorization: `Bearer ${this.token}`,
             "X-Bifrost-Realm": this.realm,
           },
           options.headers,
         ),
         signal: controller.signal,
       });
+    } catch (err) {
+      if ((err as Error).name === "AbortError") {
+        throw new Error(`Bifrost request timed out after ${this.timeout}ms: ${url}`);
+      }
+      throw err;
+    } finally {
🤖 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/work-item-source-bifrost/src/client/bifrost-http-client.ts`
around lines 96 - 121, The request logic in BifrostHttpClient.request treats all
fetch aborts the same, so timeout-triggered AbortError is indistinguishable from
other failures. Update the timeout path in request to mark when the
AbortController fired due to the setTimeout and, in the catch handling around
fetch, detect that case and log/throw a timeout-specific error message instead
of a generic AbortError; keep the normal network/error handling unchanged for
non-timeout failures.

122-136: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Generic 409/404 mapping applies to every endpoint, not just claim/get.

request() hardcodes "Rune already claimed" for any 409 and "Rune not found" for any 404, but this helper backs createRune, addDependency, updateRuneState, forgeRune, fulfillRune, failRune, and unclaimRune too. A 409 from addDependency (e.g. duplicate/cyclic edge) or createRune would be mislabeled as a claim conflict, and a 404 from updateRuneState/addDependency on an unknown target could be similarly misleading.

Consider making the error mapping endpoint-aware (e.g. pass a status-message map into request(), or map errors in each wrapper method).

🤖 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/work-item-source-bifrost/src/client/bifrost-http-client.ts`
around lines 122 - 136, The generic 409/404 handling in request() is too
specific for all callers, since it always throws “Rune already claimed” or “Rune
not found” even for endpoints like createRune, addDependency, updateRuneState,
forgeRune, fulfillRune, failRune, and unclaimRune. Update request() in
bifrost-http-client.ts to accept endpoint-specific status-to-message mapping or
move the 409/404 translation into each wrapper method so the thrown error
matches the operation that failed. Use the existing request() helper and the
endpoint methods to keep the mapping accurate per call site.
🤖 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/packages/work-item-source-bifrost/src/bifrost-work-item-source.spec.ts`:
- Around line 150-162: The detached async iteration over source.watchWorkItems()
in the bifrost-work-item-source.spec tests is never cancelled, while
watchWorkItems() itself is a perpetual poll loop, so it can keep running after
cleanup and interfere with later tests. Update the affected test blocks to
explicitly stop the iterator before cleanup by calling return() on the async
iterator or by wiring an AbortSignal into watchWorkItems(), and make sure this
is done in both test cases that use the void async loop.

In
`@orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.ts`:
- Around line 127-134: The setState method in BifrostWorkItemSource is
swallowing persistence failures by catching and only logging, unlike the sibling
mutation methods. Remove the internal try/catch in setState so errors from
this.#getClient() or client.updateRuneState(...) propagate to the caller,
keeping behavior consistent with completeWorkItem, failWorkItem, pauseWorkItem,
startWorkItem, and setDependency.
- Around line 96-107: The backoff logic in BifrostWorkItemSource is inverted:
successful polling in the work-item loop should not always increase the delay,
while failures in the catch branch should. Update the polling flow around
BifrostWorkItemSource.mapToWorkItem and BifrostWorkItemSource.sleep so
pollInterval only grows when an error occurs and resets (or stays at
defaultPollInterval) when work is found, ensuring busy queues stay responsive
and repeated failures back off progressively.
- Around line 86-99: The post-claim fetch path in
BifrostWorkItemSource.claimAndYield leaves a rune claimed if client.getRune()
fails after client.claimRune() succeeds. Update this flow to compensate on fetch
failure by calling a release/unclaim method on the claimed rune before the error
is handled, and keep the existing 409 skip behavior intact. Use the existing
claimRune, getRune, and BifrostWorkItemSource mapping flow as the touchpoints so
a transient get failure does not strand the work item.

---

Nitpick comments:
In
`@orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.spec.ts`:
- Around line 408-423: The adaptive polling test currently duplicates the
backoff math instead of exercising BifrostWorkItemSource, so update the
"adaptive polling" spec to drive watchWorkItems() and observe real polling
behavior. Use fake timers and the fetch mock, similar to the existing gating
test, then assert on the interval between repeated /api/ready requests to verify
exponential backoff and the maxPollInterval cap through the actual adapter
logic.

In
`@orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.ts`:
- Around line 157-164: The dependency mapping logic in getDependencies is
duplicated with mapToWorkItem, so extract a shared static helper in
BifrostWorkItemSource for converting each dependency’s target_id and
relationship into { workItemId, type }. Update both getDependencies and
mapToWorkItem to use that helper so the mapping stays consistent if the shape
changes.

In
`@orchestrator-v2/packages/work-item-source-bifrost/src/client/bifrost-http-client.spec.ts`:
- Around line 1-256: Add tests to cover the missing BifrostHttpClient paths:
implement a success case for getRune, a request to unclaimRune, and
timeout/abort behavior in request(). Use the existing BifrostHttpClient test
suite and mockFetch setup in bifrost-http-client.spec.ts, asserting the expected
endpoint, method/body, and that request() aborts or rejects appropriately when
the timeout is exceeded.

In
`@orchestrator-v2/packages/work-item-source-bifrost/src/client/bifrost-http-client.ts`:
- Around line 96-121: The request logic in BifrostHttpClient.request treats all
fetch aborts the same, so timeout-triggered AbortError is indistinguishable from
other failures. Update the timeout path in request to mark when the
AbortController fired due to the setTimeout and, in the catch handling around
fetch, detect that case and log/throw a timeout-specific error message instead
of a generic AbortError; keep the normal network/error handling unchanged for
non-timeout failures.
- Around line 122-136: The generic 409/404 handling in request() is too specific
for all callers, since it always throws “Rune already claimed” or “Rune not
found” even for endpoints like createRune, addDependency, updateRuneState,
forgeRune, fulfillRune, failRune, and unclaimRune. Update request() in
bifrost-http-client.ts to accept endpoint-specific status-to-message mapping or
move the 409/404 translation into each wrapper method so the thrown error
matches the operation that failed. Use the existing request() helper and the
endpoint methods to keep the mapping accurate per call site.
🪄 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: 63397ff3-408b-41e0-b80d-946b57d9cad5

📥 Commits

Reviewing files that changed from the base of the PR and between 415f673 and 6a2b68c.

⛔ Files ignored due to path filters (1)
  • orchestrator-v2/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • orchestrator-v2/examples/lvl3/agents/cowsay/AGENT.md
  • orchestrator-v2/examples/lvl3/doSomething.ts
  • orchestrator-v2/examples/lvl3/orchestrator.ts
  • orchestrator-v2/examples/lvl3/package.json
  • orchestrator-v2/examples/lvl3/runner.ts
  • orchestrator-v2/packages/interfaces-work/src/index.ts
  • orchestrator-v2/packages/interfaces-work/src/types.ts
  • orchestrator-v2/packages/orchestrator/src/test-helpers.ts
  • orchestrator-v2/packages/work-item-source-bifrost/package.json
  • orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.spec.ts
  • orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.ts
  • orchestrator-v2/packages/work-item-source-bifrost/src/client/bifrost-http-client.spec.ts
  • orchestrator-v2/packages/work-item-source-bifrost/src/client/bifrost-http-client.ts
  • orchestrator-v2/packages/work-item-source-bifrost/src/config/config-loader.ts
  • orchestrator-v2/packages/work-item-source-bifrost/src/config/credential-loader.ts
  • orchestrator-v2/packages/work-item-source-bifrost/src/index.ts
  • orchestrator-v2/packages/work-item-source-bifrost/src/types.ts
  • orchestrator-v2/packages/work-item-source-bifrost/vite.config.ts

Comment on lines +150 to +162
void (async (): Promise<void> => {
for await (const workItem of source.watchWorkItems()) {
workItems.push(workItem);
}
})();

await new Promise((resolve) => setTimeout(resolve, 300));

expect(workItems).toHaveLength(0);
expect(callCount).toBeGreaterThanOrEqual(1);

await cleanup();
}, 10000);

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the spec and the watchWorkItems implementation
git ls-files | rg 'orchestrator-v2/packages/work-item-source-bifrost/src/.*(\.spec\.ts|\.ts)$'

# Find references to watchWorkItems
rg -n "watchWorkItems|cleanup\(" orchestrator-v2/packages/work-item-source-bifrost/src -S

# Inspect the relevant test sections
sed -n '120,190p' orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.spec.ts
printf '\n====\n'
sed -n '360,430p' orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.spec.ts

# Inspect the implementation of watchWorkItems
rg -n "async \*|watchWorkItems\s*\(" orchestrator-v2/packages/work-item-source-bifrost/src -S

Repository: devzeebo/bifrost

Length of output: 7542


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the implementation of watchWorkItems and surrounding helpers
sed -n '1,220p' orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.ts

# Read the relevant test areas with line numbers for precise context
cat -n orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.spec.ts | sed -n '95,170p'
printf '\n====\n'
cat -n orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.spec.ts | sed -n '345,415p'

Repository: devzeebo/bifrost

Length of output: 12689


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,240p' orchestrator-v2/packages/work-item-source-bifrost/src/client/bifrost-http-client.ts
printf '\n====\n'
rg -n "createTestSource|cleanup\s*=" orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.spec.ts -n -A 20 -B 20

Repository: devzeebo/bifrost

Length of output: 16480


Cancel the detached watchWorkItems() loops
The void (async () => { for await (...) { ... } })() loops in bifrost-work-item-source.spec.ts:150-162 and :390-404 are never stopped, while watchWorkItems() in bifrost-work-item-source.ts runs an infinite while (true) poll loop with no cancellation path. That can keep calling the next test’s global.fetch mock after cleanup(), causing flaky cross-test interference. Break the iterator explicitly (return()/AbortSignal) before cleanup, or stop after the expected assertion.

🤖 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/work-item-source-bifrost/src/bifrost-work-item-source.spec.ts`
around lines 150 - 162, The detached async iteration over
source.watchWorkItems() in the bifrost-work-item-source.spec tests is never
cancelled, while watchWorkItems() itself is a perpetual poll loop, so it can
keep running after cleanup and interfere with later tests. Update the affected
test blocks to explicitly stop the iterator before cleanup by calling return()
on the async iterator or by wiring an AbortSignal into watchWorkItems(), and
make sure this is done in both test cases that use the void async loop.

Comment on lines +86 to +99
try {
await client.claimRune(rune.id);
} catch (error) {
if ((error as { status?: number }).status === 409) {
continue;
}
throw error;
}

const detail = await client.getRune(rune.id);
pollInterval = defaultPollInterval;

yield BifrostWorkItemSource.mapToWorkItem(detail, agentName);
}

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 | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and inspect the relevant section with line numbers.
ast-grep outline orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.ts --view expanded || true
echo "-----"
sed -n '1,280p' orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.ts | cat -n

echo "===== SEARCH: client methods ====="
rg -n "claimRune|unclaimRune|getRune|setState|pollInterval|defaultPollInterval" orchestrator-v2/packages -g '*.ts'

Repository: devzeebo/bifrost

Length of output: 18535


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Bifrost client type and any rune detail shapes relevant to acceptance criteria.
rg -n "interface .*Rune|type .*Rune|acceptance_criteria|acceptanceCriteria|satisfied" orchestrator-v2/packages -g '*.ts'

Repository: devzeebo/bifrost

Length of output: 1765


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the implementation of setState and any surrounding error handling patterns.
rg -n "async setState|setState\\(" orchestrator-v2/packages -g '*.ts'

Repository: devzeebo/bifrost

Length of output: 1147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the target file and related client/type definitions.
echo "=== bifrost-work-item-source.ts ==="
sed -n '1,280p' orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.ts | cat -n

echo
echo "=== search relevant symbols ==="
rg -n "claimRune|unclaimRune|getRune|setState|pollInterval|defaultPollInterval|acceptance_criteria|acceptanceCriteria|satisfied" orchestrator-v2/packages -g '*.ts'

Repository: devzeebo/bifrost

Length of output: 17773


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' orchestrator-v2/packages/work-item-source-bifrost/src/client/bifrost-http-client.ts | cat -n
echo "-----"
sed -n '1,120p' orchestrator-v2/packages/work-item-source-bifrost/src/types.ts | cat -n

Repository: devzeebo/bifrost

Length of output: 6977


Claimed runes need a compensating release on post-claim fetch failure orchestrator-v2/packages/work-item-source-bifrost/src/bifrost-work-item-source.ts:86-99claimRune() succeeds before getRune(), and any fetch error falls into the outer catch, which only logs and sleeps; that leaves the rune claimed and unavailable to other workers. Add a retry or unclaimRune() in this path so a transient failure doesn’t strand the work item.

🤖 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/work-item-source-bifrost/src/bifrost-work-item-source.ts`
around lines 86 - 99, The post-claim fetch path in
BifrostWorkItemSource.claimAndYield leaves a rune claimed if client.getRune()
fails after client.claimRune() succeeds. Update this flow to compensate on fetch
failure by calling a release/unclaim method on the claimed rune before the error
is handled, and keep the existing 409 skip behavior intact. Use the existing
claimRune, getRune, and BifrostWorkItemSource mapping flow as the touchpoints so
a transient get failure does not strand the work item.

Comment on lines +96 to +107
pollInterval = defaultPollInterval;

yield BifrostWorkItemSource.mapToWorkItem(detail, agentName);
}

pollInterval = Math.min(pollInterval * 2, maxPollInterval);
const jitter = pollInterval * 0.2 * (Math.random() * 2 - 1);
await BifrostWorkItemSource.sleep(pollInterval + jitter);
} catch (error) {
console.error(error);
await BifrostWorkItemSource.sleep(pollInterval);
}

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 | 🟠 Major | ⚡ Quick win

Adaptive backoff logic doesn't behave as intended.

pollInterval is reset to defaultPollInterval per processed rune (Line 96), but Line 101 unconditionally doubles it right after the loop regardless of whether work was found, so a busy queue still gets progressively slower polling. Meanwhile the error branch (104-107) never grows pollInterval, so repeated failures retry at a flat interval instead of backing off — the opposite of where backoff is actually needed.

🔧 Suggested fix
+        let foundWork = false;
         for (const rune of readyRunes) {
           ...
-          pollInterval = defaultPollInterval;
+          foundWork = true;
           yield BifrostWorkItemSource.mapToWorkItem(detail, agentName);
         }
 
-        pollInterval = Math.min(pollInterval * 2, maxPollInterval);
+        pollInterval = foundWork
+          ? defaultPollInterval
+          : Math.min(pollInterval * 2, maxPollInterval);
         const jitter = pollInterval * 0.2 * (Math.random() * 2 - 1);
         await BifrostWorkItemSource.sleep(pollInterval + jitter);
       } catch (error) {
         console.error(error);
+        pollInterval = Math.min(pollInterval * 2, maxPollInterval);
         await BifrostWorkItemSource.sleep(pollInterval);
       }
📝 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
pollInterval = defaultPollInterval;
yield BifrostWorkItemSource.mapToWorkItem(detail, agentName);
}
pollInterval = Math.min(pollInterval * 2, maxPollInterval);
const jitter = pollInterval * 0.2 * (Math.random() * 2 - 1);
await BifrostWorkItemSource.sleep(pollInterval + jitter);
} catch (error) {
console.error(error);
await BifrostWorkItemSource.sleep(pollInterval);
}
let foundWork = false;
for (const rune of readyRunes) {
...
foundWork = true;
yield BifrostWorkItemSource.mapToWorkItem(detail, agentName);
}
pollInterval = foundWork
? defaultPollInterval
: Math.min(pollInterval * 2, maxPollInterval);
const jitter = pollInterval * 0.2 * (Math.random() * 2 - 1);
await BifrostWorkItemSource.sleep(pollInterval + jitter);
} catch (error) {
console.error(error);
pollInterval = Math.min(pollInterval * 2, maxPollInterval);
await BifrostWorkItemSource.sleep(pollInterval);
}
🤖 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/work-item-source-bifrost/src/bifrost-work-item-source.ts`
around lines 96 - 107, The backoff logic in BifrostWorkItemSource is inverted:
successful polling in the work-item loop should not always increase the delay,
while failures in the catch branch should. Update the polling flow around
BifrostWorkItemSource.mapToWorkItem and BifrostWorkItemSource.sleep so
pollInterval only grows when an error occurs and resets (or stays at
defaultPollInterval) when work is found, ensuring busy queues stay responsive
and repeated failures back off progressively.

Comment on lines +127 to +134
public async setState(workItemId: string, state: Record<string, unknown>): Promise<void> {
try {
const client = await this.#getClient();
await client.updateRuneState(workItemId, state);
} catch (err) {
console.error(`Failed to update state for work item ${workItemId}:`, err);
}
}

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

setState silently swallows errors, unlike sibling mutation methods.

completeWorkItem, failWorkItem, pauseWorkItem, startWorkItem, and setDependency all propagate errors to the caller, but setState catches and only logs. Callers have no way to know a state persist failed, which is risky if downstream orchestration logic relies on state being durably saved.

🤖 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/work-item-source-bifrost/src/bifrost-work-item-source.ts`
around lines 127 - 134, The setState method in BifrostWorkItemSource is
swallowing persistence failures by catching and only logging, unlike the sibling
mutation methods. Remove the internal try/catch in setState so errors from
this.#getClient() or client.updateRuneState(...) propagate to the caller,
keeping behavior consistent with completeWorkItem, failWorkItem, pauseWorkItem,
startWorkItem, and setDependency.

@devzeebo
devzeebo merged commit ec4372a into main Jul 7, 2026
1 check passed
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.

[v2] Bifrost work item source adapter [v2] Task source with dependencies + draft/live

1 participant