Skip to content

fix(registry): preserve native_team_enabled + provider across ON CONFLICT in register() - #1684

Merged
namastex888 merged 2 commits into
devfrom
fix/registry-onconflict-preserve-native-fields
May 7, 2026
Merged

namastex888 merged 2 commits into
devfrom
fix/registry-onconflict-preserve-native-fields

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

Summary

Closes #1682.

register() in src/lib/agent-registry.ts was no-op'ing the protocol-related fields on its ON CONFLICT (id) DO UPDATE path. After findOrCreateAgent created the identity row with default values (native_team_enabled=false, provider=NULL, etc.), the subsequent register() carrying the real spawn intent left those defaults pinned — and routing kept reading nativeTeamEnabled === false, dispatching claude workers down injectToTmuxPane (the body→200ms→Enter race surface) instead of writeToNativeInbox.

Three live workers observed in this misrouted state: engineer, fix, fix-bug2.

Causal chain

  1. findOrCreateAgent('engineer', 'genie', 'engineer') — INSERT with native_team_enabled DEFAULT false, provider IS NULL.
  2. Spawn pipeline calls register({ ...id, nativeTeamEnabled: true, provider: 'claude', transport: 'tmux', nativeAgentId: 'engineer@genie', ... }).
  3. INSERT collides on (id); DO UPDATE SET … refreshes only pane_id, session, state, last_state_change, team, role, custom_name, updated_at — none of the protocol fields.
  4. agents.native_team_enabled stays false, agents.provider stays NULL.
  5. Routing in protocol-router reads stale row → injectToTmuxPane for claude workers.

Fix

Extend the ON CONFLICT … DO UPDATE SET clause at src/lib/agent-registry.ts:293:

  • native_team_enabled = EXCLUDED.native_team_enabled — spawn intent is authoritative; a respawn under different protocol settings must be allowed to re-flag the row.
  • native_agent_id, native_color, parent_session_id, provider, transport use COALESCE(EXCLUDED.<col>, agents.<col>) — preserve immutable identity values when later callers (heartbeat-style refreshes) omit them.

Regressing commit: dfc875ef.

Tests

Two new regressions added in src/lib/agent-registry.test.ts under a fresh top-level (non-skipped) describe block:

  1. register preserves nativeTeamEnabled + provider across findOrCreateAgent → register — drives the exact misroute path. Asserts native_team_enabled flips false → true (EXCLUDED) and provider/transport/native_agent_id/native_color/parent_session_id populate from NULL (COALESCE).
  2. register preserves established protocol fields when later caller omits them — confirms the COALESCE branches don't clobber values when a later refresh leaves the fields undefined.

These use real UUIDs (via randomUUID() from findOrCreateAgent) so migration 061's agents_id_shape_check accepts the inserts. The existing legacy describe.skip block above is untouched (still pending wish #175 fixture rewrite).

Backfill SQL — operator runs post-merge (NOT in this PR)

UPDATE agents a
SET native_team_enabled = true,
    provider = COALESCE(a.provider, t.provider)
FROM agent_templates t
WHERE a.custom_name = t.name
  AND a.team = t.team
  AND t.native_team_enabled = true
  AND a.state IS NOT NULL;

Idempotent — restores correctness for currently-misrouted live workers.

Coordination

Courtesy ping for wish #175 G2 #177 (retire-session-names-id-only). Same file, different region — G2 touches reconcileStaleSpawns (line 604+); this fix is at register() line 293. No conflict observed against origin/wish/retire-session-names-id-only at fetch time. Happy to fold into G2 if cleaner.

Validation

  • bun run typecheck → 0 errors ✅
  • bunx biome check src/lib/agent-registry.ts src/lib/agent-registry.test.ts → clean ✅

Test-gate caveat: bun test src/lib/agent-registry.test.ts hung in this workspace (unrelated environmental issue — collateral from in-flight dogfood work, see #1682 thread). Reviewer should run the test pre-merge to confirm the two new regression tests pass.

Out of scope

  • Companion injectToTmuxPane retirement (tracked separately as a hardening ticket).
  • Any rework of findOrCreateAgent (works correctly; bug is purely on the register path).
  • Codex worker behavior (unaffected).

Test plan

  • Reviewer: bun test src/lib/agent-registry.test.ts -t "1682" — both new tests pass.
  • Reviewer: confirm full bun test src/lib/agent-registry.test.ts stays green (no regression in the existing describe.skip block which remains skipped pending 🚀 Release: Interactive Onboarding with Ink Wizard (rc.27) #175).
  • Operator: run backfill SQL above against any environment with mid-flight misrouted workers.

🤖 Generated with Claude Code

…LICT in register()

Closes #1682

`findOrCreateAgent` writes the durable identity row first with all native_*
and provider fields defaulting to NULL/false. The subsequent `register()`
call carries the real protocol settings, but its ON CONFLICT (id) DO UPDATE
clause only refreshed pane/session/state — leaving nativeTeamEnabled=false
and provider=null pinned to the row.

Routing then read `nativeTeamEnabled === false` and dispatched claude
workers down `injectToTmuxPane` (the documented body→200ms→Enter race)
instead of `writeToNativeInbox`. Three live agents observed misrouted in
this state: engineer, fix, fix-bug2.

Causal chain:
  findOrCreateAgent INSERT (defaults)
  → register() ON CONFLICT no-op on protocol fields
  → routing reads stale defaults
  → injectToTmuxPane fallback for claude workers

Fix: extend the SET clause.
  - native_team_enabled = EXCLUDED  (spawn intent is authoritative;
    a respawn under different protocol settings must re-flag the row)
  - native_agent_id, native_color, parent_session_id, provider, transport
    use COALESCE — preserve immutable identity values when later callers
    omit them.

Regressing commit: dfc875e.

Coordination: courtesy ping for wish #175 G2 #177 (retire-session-names-
id-only) — same file, different region (G2 touches reconcileStaleSpawns
at line 604+, this fix is at register() line 293). Happy to fold into G2
if cleaner.

Backfill SQL (operator runs post-merge — NOT in this PR):

    UPDATE agents a
    SET native_team_enabled = true,
        provider = COALESCE(a.provider, t.provider)
    FROM agent_templates t
    WHERE a.custom_name = t.name
      AND a.team = t.team
      AND t.native_team_enabled = true
      AND a.state IS NOT NULL;

Idempotent — restores correctness for currently-misrouted live workers.

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

coderabbitai Bot commented May 7, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e84bcc50-0d1b-4367-8264-bc48ae3063c2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/registry-onconflict-preserve-native-fields

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request addresses issue #1682 by updating the register function to preserve protocol fields during database updates using ON CONFLICT logic and adding corresponding regression tests. Feedback indicates that the current implementation fails to preserve these fields correctly because hardcoded defaults in the VALUES clause (such as 'tmux' for transport and false for native_team_enabled) will overwrite existing values when those fields are omitted in subsequent calls.

Comment thread src/lib/agent-registry.ts Outdated
Comment on lines +293 to +303
// ON CONFLICT preservation (#1682):
// - `native_team_enabled` uses EXCLUDED — a respawn under different protocol
// settings must be allowed to re-flag the row. Spawn intent is authoritative.
// - The other native_* / provider / transport fields use COALESCE — the
// identity row created by `findOrCreateAgent` defaults them to NULL/false,
// so the first real `register()` after spawn must populate them. Later
// callers that omit these fields must NOT clobber the established values.
// Without this, the ON CONFLICT no-op left routing reading stale defaults
// (nativeTeamEnabled=false, provider=null), forcing claude workers down the
// injectToTmuxPane path instead of writeToNativeInbox.
await sql`INSERT INTO agents (id, pane_id, session, worktree, task_id, task_title, wish_slug, group_number, started_at, state, last_state_change, repo_path, window_name, window_id, role, custom_name, sub_panes, provider, transport, skill, team, tmux_window, native_agent_id, native_color, native_team_enabled, parent_session_id, suspended_at, auto_resume, resume_attempts, last_resume_attempt, max_resume_attempts, pane_color) VALUES (${agent.id}, ${agent.paneId}, ${agent.session}, ${agent.worktree ?? null}, ${agent.taskId ?? null}, ${agent.taskTitle ?? null}, ${agent.wishSlug ?? null}, ${agent.groupNumber ?? null}, ${agent.startedAt ?? now}, ${agent.state ?? 'spawning'}, ${agent.lastStateChange ?? now}, ${agent.repoPath}, ${agent.windowName ?? null}, ${agent.windowId ?? null}, ${agent.role ?? null}, ${agent.customName ?? null}, ${sql.json(agent.subPanes ?? [])}, ${agent.provider ?? null}, ${agent.transport ?? 'tmux'}, ${agent.skill ?? null}, ${agent.team ?? null}, ${agent.window ?? null}, ${agent.nativeAgentId ?? null}, ${agent.nativeColor ?? null}, ${agent.nativeTeamEnabled ?? false}, ${agent.parentSessionId ?? null}, ${agent.suspendedAt ?? null}, ${agent.autoResume ?? false}, ${agent.resumeAttempts ?? 0}, ${agent.lastResumeAttempt ?? null}, ${agent.maxResumeAttempts ?? 3}, ${agent.paneColor ?? null}) ON CONFLICT (id) DO UPDATE SET pane_id = EXCLUDED.pane_id, session = EXCLUDED.session, state = EXCLUDED.state, last_state_change = EXCLUDED.last_state_change, team = COALESCE(agents.team, EXCLUDED.team), role = COALESCE(agents.role, EXCLUDED.role), custom_name = COALESCE(agents.custom_name, EXCLUDED.custom_name), native_team_enabled = EXCLUDED.native_team_enabled, native_agent_id = COALESCE(EXCLUDED.native_agent_id, agents.native_agent_id), native_color = COALESCE(EXCLUDED.native_color, agents.native_color), parent_session_id = COALESCE(EXCLUDED.parent_session_id, agents.parent_session_id), provider = COALESCE(EXCLUDED.provider, agents.provider), transport = COALESCE(EXCLUDED.transport, agents.transport), updated_at = now()`;

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.

medium

The implementation of transport and native_team_enabled preservation is inconsistent with the stated intent in the comments and the PR description.

  1. transport logic error: The UPDATE clause uses COALESCE(EXCLUDED.transport, agents.transport), but the VALUES clause provides a hardcoded default: ${agent.transport ?? 'tmux'}. This ensures EXCLUDED.transport is never NULL, so the COALESCE will always return the new value (defaulting to 'tmux' if omitted). If an agent was registered with transport: 'inline', a subsequent register() call (like a heartbeat) that omits the field will incorrectly reset it to 'tmux'.
  2. native_team_enabled clobbering: The UPDATE clause uses EXCLUDED.native_team_enabled directly, and the VALUES clause defaults to false. If a heartbeat caller omits this field, it will be reset to false, even if it was previously true. This contradicts the goal of not clobbering established values during refreshes.

To fix this for transport (which is nullable in the DB), you should pass null in the VALUES clause when omitted and handle the default in the UPDATE clause: transport = COALESCE(EXCLUDED.transport, agents.transport, 'tmux').

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

ℹ️ 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".

Comment thread src/lib/agent-registry.ts Outdated
// Without this, the ON CONFLICT no-op left routing reading stale defaults
// (nativeTeamEnabled=false, provider=null), forcing claude workers down the
// injectToTmuxPane path instead of writeToNativeInbox.
await sql`INSERT INTO agents (id, pane_id, session, worktree, task_id, task_title, wish_slug, group_number, started_at, state, last_state_change, repo_path, window_name, window_id, role, custom_name, sub_panes, provider, transport, skill, team, tmux_window, native_agent_id, native_color, native_team_enabled, parent_session_id, suspended_at, auto_resume, resume_attempts, last_resume_attempt, max_resume_attempts, pane_color) VALUES (${agent.id}, ${agent.paneId}, ${agent.session}, ${agent.worktree ?? null}, ${agent.taskId ?? null}, ${agent.taskTitle ?? null}, ${agent.wishSlug ?? null}, ${agent.groupNumber ?? null}, ${agent.startedAt ?? now}, ${agent.state ?? 'spawning'}, ${agent.lastStateChange ?? now}, ${agent.repoPath}, ${agent.windowName ?? null}, ${agent.windowId ?? null}, ${agent.role ?? null}, ${agent.customName ?? null}, ${sql.json(agent.subPanes ?? [])}, ${agent.provider ?? null}, ${agent.transport ?? 'tmux'}, ${agent.skill ?? null}, ${agent.team ?? null}, ${agent.window ?? null}, ${agent.nativeAgentId ?? null}, ${agent.nativeColor ?? null}, ${agent.nativeTeamEnabled ?? false}, ${agent.parentSessionId ?? null}, ${agent.suspendedAt ?? null}, ${agent.autoResume ?? false}, ${agent.resumeAttempts ?? 0}, ${agent.lastResumeAttempt ?? null}, ${agent.maxResumeAttempts ?? 3}, ${agent.paneColor ?? null}) ON CONFLICT (id) DO UPDATE SET pane_id = EXCLUDED.pane_id, session = EXCLUDED.session, state = EXCLUDED.state, last_state_change = EXCLUDED.last_state_change, team = COALESCE(agents.team, EXCLUDED.team), role = COALESCE(agents.role, EXCLUDED.role), custom_name = COALESCE(agents.custom_name, EXCLUDED.custom_name), native_team_enabled = EXCLUDED.native_team_enabled, native_agent_id = COALESCE(EXCLUDED.native_agent_id, agents.native_agent_id), native_color = COALESCE(EXCLUDED.native_color, agents.native_color), parent_session_id = COALESCE(EXCLUDED.parent_session_id, agents.parent_session_id), provider = COALESCE(EXCLUDED.provider, agents.provider), transport = COALESCE(EXCLUDED.transport, agents.transport), updated_at = now()`;

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 Preserve existing transport when register caller omits it

transport is intended to be preserved via COALESCE(EXCLUDED.transport, agents.transport), but this UPSERT always inserts ${agent.transport ?? 'tmux'} into EXCLUDED.transport, so it is never NULL on conflict. That means any partial register() refresh that omits transport will force the row to 'tmux', including agents that were previously 'inline'; downstream logic (for example findDeadResumable filtering on w.transport === 'tmux') will then treat those rows as resumable tmux workers and can route into the wrong tmux-only path.

Useful? React with 👍 / 👎.

Addresses Gemini + Codex review on PR #1684. The prior patch wrote
`${agent.transport ?? 'tmux'}` into the INSERT VALUES clause, so
EXCLUDED.transport was never NULL — `transport = COALESCE(EXCLUDED.transport,
agents.transport)` always returned the new value. A heartbeat-style refresh
that omitted `transport` would silently rewrite a row registered with
`'inline'` back to `'tmux'`, and downstream `findDeadResumable` filters on
`w.transport === 'tmux'` would misclassify the row into the tmux-only
resume path.

Fix:
- VALUES clause: `${agent.transport ?? null}` (was `?? 'tmux'`).
- DO UPDATE clause: `transport = COALESCE(EXCLUDED.transport, agents.transport,
  'tmux')` — fresh INSERT with no transport still defaults to `'tmux'`, but
  a refresh that omits the field preserves the established value.

Tightened the ON CONFLICT comment with the caller contract for
`native_team_enabled` (spawn intent is authoritative, callers MUST set
explicitly — the `?? false` fallback in VALUES will clobber an existing
`true` if omitted).

Added a third regression test exercising the `'inline'` transport heartbeat
path that the existing two tests didn't cover (both used `'tmux'` on both
sides, so the COALESCE no-op was invisible).

Validation:
- `bun run typecheck` → 0 errors
- `bunx biome check src/lib/agent-registry.ts src/lib/agent-registry.test.ts`
  → clean
- `bun test src/lib/agent-registry.test.ts` hangs in this workspace (same
  environmental issue documented in the original PR body — collateral from
  in-flight dogfood work).

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

Copy link
Copy Markdown
Contributor Author

Addressed @gemini-code-assist and @chatgpt-codex-connector review in b784aa27:

  • Transport COALESCE no-op (both bots, correct): Changed VALUES to ${agent.transport ?? null} and DO UPDATE to COALESCE(EXCLUDED.transport, agents.transport, 'tmux'). Fresh INSERT with no transport still defaults to 'tmux'; a heartbeat refresh that omits the field now preserves the established value (e.g. 'inline').
  • native_team_enabled (Gemini): Author's design intent stands — spawn intent is authoritative on this field. Tightened the ON CONFLICT comment to make the caller contract explicit: every register() call MUST set nativeTeamEnabled because the ?? false fallback in VALUES will clobber an existing true if omitted. Surveyed the three current runtime callers (registerSpawnWorker, session.ts, protocol-router-spawn.ts) — all set the field explicitly.
  • Test coverage gap (both bots, implicit): Added a third regression test (register preserves non-default transport across heartbeat refresh (PR #1684 review)) that registers with transport: 'inline', then heartbeat-refreshes omitting the field, asserting it stays 'inline'. Pre-fix this would have silently flipped to 'tmux'.

Validation: bun run typecheck clean, bunx biome check clean. bun test hangs in this workspace (same environmental issue documented in PR body).

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Thanks for the detailed follow-up and for incorporating the regression test for the transport field. The logic in src/lib/agent-registry.ts now correctly handles the ON CONFLICT path by using COALESCE to preserve established protocol fields while allowing native_team_enabled to be updated by the authoritative spawn intent. The added regression tests in src/lib/agent-registry.test.ts provide excellent coverage for these edge cases. Everything looks correct and ready for merge.

@chatgpt-codex-connector

Copy link
Copy Markdown

No further code changes were needed — the branch is already at commit b784aa2 that addresses the transport/no-op and regression-test feedback described in the trigger, and the working tree is clean.

Checks run

  • ✅ cd /workspace/genie && git status --short
  • ✅ cd /workspace/genie && git rev-parse --abbrev-ref HEAD
  • ✅ cd /workspace/genie && git log --oneline -n 3

Because there were no new modifications to commit, I did not create an additional commit or call make_pr (per your constraints).

View task →

@namastex888
namastex888 merged commit d385277 into dev May 7, 2026
16 checks passed
@automagik-genie
automagik-genie deleted the fix/registry-onconflict-preserve-native-fields branch September 25, 2026 04:50
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