Skip to content

[factory] p12: node-targeted placement + reject-and-reconcile#1141

Merged
khaliqgant merged 5 commits into
mainfrom
ricky/factory-p12-node-placement
Jun 17, 2026
Merged

[factory] p12: node-targeted placement + reject-and-reconcile#1141
khaliqgant merged 5 commits into
mainfrom
ricky/factory-p12-node-placement

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jun 16, 2026

Copy link
Copy Markdown
Member

Summary

Targeted placement by node name/self, repo-path-aware eligibility, reject-and-reconcile for unmapped repos. Minimal #1056 §6 slice; no least-loaded scheduler.

Factory issue p12 (node-placement). Built autonomously via the relayflows factory squad loop (standard review).

Spec: factory/planning/linear-issue-factory-fleet-p12-node-placement.md

Review

  • single-pass implement (impl-codex)
  • Claude fresh-eyes review/fix loop
  • deterministic acceptance: npm run build --if-present 2>&1 | tail -50 && npm test --if-present 2>&1 | tail -40

Review in cubic

Factory issue p12. Autonomous build via relayflows squad loop.
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 95d0325e-bf31-4224-bd6f-009b6634ff9e

📥 Commits

Reviewing files that changed from the base of the PR and between f5e4682 and 19b00c6.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • packages/sdk/src/messaging/placement.test.mts
  • packages/sdk/src/messaging/relaycast.ts
  • packages/sdk/src/messaging/types.ts
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/sdk/src/messaging/types.ts
  • packages/sdk/src/messaging/relaycast.ts
  • packages/sdk/src/messaging/placement.test.mts

📝 Walkthrough

Walkthrough

Adds placement.spawn() to RelaycastMessagingClient for node-targeted action dispatch. New public types model placement rejection/reconciliation/spawn contracts. The implementation polls for an eligible live node by capability and repo-key mapping, enforces TTL and queue limits, and exposes reconciliation hooks. A 523-line Vitest suite covers all placement states.

Changes

SDK Node Placement Feature

Layer / File(s) Summary
Placement type contracts
packages/sdk/src/messaging/types.ts
RelayNode gains repoKeys?; RelayActionInvocationAck gains handlerNodeId/dispatchedNodeId; new RelayPlacementRejectReason, RelayPlacementReconcileReason, RelayPlacementReconcileEvent, RelaySpawnPlacementInput, RelaySpawnPlacementAck types added; RelayMessagingClient gains placement.spawn() namespace.
Node normalization and placement infrastructure
packages/sdk/src/messaging/relaycast.ts
toRelayNode extended with repoKeys via new readRepoKeys helper; normalizeActionInvocationAck updated to handlerNodeId/dispatchedNodeId; RelayPlacementError class, validation/action-shape helpers, and bounded delay added; RelaycastMessagingOptions and client constructor wired with selfNodeName, placementTtlMs, maxQueuedPlacements, and placementLog.
placement.spawn loop and helpers + export
packages/sdk/src/messaging/relaycast.ts, packages/sdk/src/messaging/index.ts
placement.spawn async polling loop implemented with TTL enforcement, queue-full limit, hard-fail/retryable outcomes, and commands.invoke dispatch; resolvePlacementNode, selectPlacementNode, reconcilePlacement, and logPlacement helpers added; RelayPlacementError re-exported from index.ts.
Placement test suite, Vitest config, and changelog
packages/sdk/src/messaging/placement.test.mts, packages/sdk/src/messaging/vitest.placement.config.mts, CHANGELOG.md
Vitest Node-environment config targeting placement.test.mts; 523-line suite covers targeted spawn, capability/cli mismatch, self-node resolution, no-bleed dispatch, queue-full, fail-fast TTL, offline/unmapped queuing and draining, onReconcile exception isolation, and TTL-expiry logging; changelog entry added.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AgentWorkforce/relay#1139: Modifies the same RelayNode shape and toRelayNode normalization in relaycast.ts/types.ts, overlapping directly with the repoKeys and node-field normalization changes introduced here.

Suggested reviewers

  • willwashburn

🐇 A hop, a skip, a node to find,
With capability and repo aligned!
The queue awaits with TTL ticking,
placement.spawn keeps the relay clicking.
Through reconcile loops and hard-fail gates,
This bunny lands on the right compute mates! 🎯

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: implementing node-targeted placement with reject-and-reconcile functionality for the relay messaging system.
Description check ✅ Passed The description covers the summary and includes test validation checkpoints, though it lacks explicit checkbox confirmation for the template's 'Tests added/updated' and 'Manual testing completed' items despite mentioning test execution.
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.

✏️ 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 ricky/factory-p12-node-placement

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 and usage tips.

@khaliqgant khaliqgant marked this pull request as ready for review June 16, 2026 23:07
@khaliqgant khaliqgant requested a review from willwashburn as a code owner June 16, 2026 23:07
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a83d124b61

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

export * from './normalize.js';
export { RelaycastMessagingClient, type RelaycastMessagingOptions } from './relaycast.js';
export {
RelayPlacementError,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the missing changelog entry

The root AGENTS.md requires curating CHANGELOG.md [Unreleased] as PRs land. This commit exports new public SDK surface (RelayPlacementError plus placement.spawn and related types), but CHANGELOG.md has no placement.spawn/RelayPlacementError entry (checked with rg -n "placement\.spawn|RelayPlacement" CHANGELOG.md), so the release notes will omit this user-visible API. Please add a concise Added bullet under [Unreleased].

Useful? React with 👍 / 👎.

Comment thread packages/sdk/src/messaging/relaycast.ts Outdated
Comment on lines +310 to +311
if (placement.capability.startsWith('spawn:') && typeof payload.cli !== 'string') {
payload.cli = placement.capability.slice('spawn:'.length);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject mismatched spawn CLI payloads

When input.input already contains a string cli, this keeps it even though node eligibility was checked against capability. For example, capability: 'spawn:claude' with input: { cli: 'codex' } can select a node that only advertises spawn:claude but then invoke the broker spawn action with cli: 'codex' (the broker SpawnParams in crates/broker/src/types.rs:160-164 uses cli to choose the harness). Please overwrite cli from the spawn: capability or reject mismatches before invoking.

Useful? React with 👍 / 👎.

@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: 2

🧹 Nitpick comments (1)
packages/sdk/src/messaging/placement.test.mts (1)

247-390: ⚡ Quick win

Use deterministic timers for queue-drain tests to reduce CI flake risk.

These tests depend on tight wall-clock sleeps (e.g., 35ms vs 25ms polling). Under slower CI scheduling, that margin can intermittently fail.

Suggested approach
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

+beforeEach(() => {
+  vi.useFakeTimers();
+});
+
+afterEach(() => {
+  vi.useRealTimers();
+});

 // inside queue/drain tests
-    await new Promise((resolve) => setTimeout(resolve, 35));
+    await vi.advanceTimersByTimeAsync(35);
🤖 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 `@packages/sdk/src/messaging/placement.test.mts` around lines 247 - 390,
Replace the wall-clock sleeps in these four test cases with deterministic Jest
fake timers to eliminate CI flake risk. For each test (the ones starting with
"queues a targeted offline node", "queues a targeted node that does not map the
repo", "reconciles an unmapped repo", and "queues when no eligible node is
live"), add jest.useFakeTimers() at the beginning and replace each await new
Promise((resolve) => setTimeout(resolve, 35)) call with
jest.advanceTimersByTime(35) to deterministically advance time. After each test
completes, call jest.useRealTimers() to restore real timers. This removes the
dependency on tight wall-clock timing margins and makes the tests robust across
varying CI scheduling speeds.
🤖 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 `@packages/sdk/src/messaging/relaycast.ts`:
- Around line 1154-1166: The error code unmapped_repo is declared as part of the
public RelayPlacementError type union but is never actually thrown anywhere in
the placement.spawn logic, making it an unreachable code path. Either remove
unmapped_repo from the exported RelayPlacementError union type if it should not
be a valid error code, or identify where unmapped_repo conditions should be
detected (likely when repo mapping fails) and add the logic to throw a
RelayPlacementError with code unmapped_repo in those cases. Also check the
location referenced in the comment (lines 266-267) as the same issue applies
there.
- Around line 1308-1320: The reconcilePlacement and logPlacement methods call
user-provided callbacks (input.onReconcile, input.log, and this.placementLog)
without error handling. If these callbacks throw or reject, the entire
placement.spawn operation fails unnecessarily since the core node selection
logic has already succeeded. Wrap each caller-provided callback invocation in
try-catch blocks to isolate any errors they throw, allowing the placement to
complete successfully even if the observability callbacks fail. This applies to
the await input.onReconcile call in reconcilePlacement and the input.log and
this.placementLog calls in logPlacement.

---

Nitpick comments:
In `@packages/sdk/src/messaging/placement.test.mts`:
- Around line 247-390: Replace the wall-clock sleeps in these four test cases
with deterministic Jest fake timers to eliminate CI flake risk. For each test
(the ones starting with "queues a targeted offline node", "queues a targeted
node that does not map the repo", "reconciles an unmapped repo", and "queues
when no eligible node is live"), add jest.useFakeTimers() at the beginning and
replace each await new Promise((resolve) => setTimeout(resolve, 35)) call with
jest.advanceTimersByTime(35) to deterministically advance time. After each test
completes, call jest.useRealTimers() to restore real timers. This removes the
dependency on tight wall-clock timing margins and makes the tests robust across
varying CI scheduling speeds.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: eea16c22-7703-4f82-8ae6-17da57f73c31

📥 Commits

Reviewing files that changed from the base of the PR and between 53ff138 and a83d124.

📒 Files selected for processing (9)
  • .workflow-artifacts/factory-p12-node-placement/claude-fix.md
  • .workflow-artifacts/factory-p12-node-placement/claude-review.md
  • .workflow-artifacts/factory-p12-node-placement/self-reflection.md
  • .workflow-artifacts/factory-p12-node-placement/signoff.md
  • packages/sdk/src/messaging/index.ts
  • packages/sdk/src/messaging/placement.test.mts
  • packages/sdk/src/messaging/relaycast.ts
  • packages/sdk/src/messaging/types.ts
  • packages/sdk/src/messaging/vitest.placement.config.mts

Comment thread packages/sdk/src/messaging/relaycast.ts
Comment thread packages/sdk/src/messaging/relaycast.ts
agent-relay-code Bot and others added 3 commits June 16, 2026 23:15
…e-placement

# Conflicts:
#	packages/sdk/src/messaging/relaycast.ts
#	packages/sdk/src/messaging/types.ts
Resolve merge with main (node `live` optionality) and apply codex/CodeRabbit
review feedback on node-targeted placement:

- CHANGELOG: add the `[Unreleased] Added` entry for `placement.spawn` /
  `RelayPlacementError` (codex P1).
- placementActionInput: reject a spawn whose explicit `input.cli` does not match
  the `spawn:<cli>` capability instead of silently dispatching the wrong harness
  (codex P2); throws RelayPlacementError(capability_mismatch).
- placement.spawn fail/expiry: emit `RelayPlacementError.code = 'unmapped_repo'`
  when the failure reason is an unmappable repo, so the public code is reachable
  (CodeRabbit major).
- reconcilePlacement/logPlacement: wrap caller-provided onReconcile/log hooks in
  try/catch so a throwing observability sink can't break a valid placement
  (CodeRabbit major).
- Keep main's optional `RelayNode.live` (placement checks are already null-safe).
- Remove factory build artifacts (.workflow-artifacts/factory-p12-node-placement)
  and the unrelated bot-authored memory/INCIDENT-*.md from the PR diff.
- Tests: add cli-mismatch reject, cli-match overwrite, unmapped_repo fail-fast,
  and throwing-onReconcile isolation cases (15 placement tests pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@khaliqgant khaliqgant merged commit d274f9a into main Jun 17, 2026
41 checks passed
@khaliqgant khaliqgant deleted the ricky/factory-p12-node-placement branch June 17, 2026 09:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant