From b95fb9c531d7342d4d3d318dcc47814b3fb3a79c Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Fri, 14 Aug 2026 21:48:50 +0200 Subject: [PATCH 1/2] fix(agent-manager): transfer wrapper registry identity --- ...feature-agent-registry-wrapper-transfer.md | 28 ++++++ ...feature-agent-registry-wrapper-transfer.md | 29 +++++++ ...feature-agent-registry-wrapper-transfer.md | 24 ++++++ ...feature-agent-registry-wrapper-transfer.md | 32 +++++++ ...feature-agent-registry-wrapper-transfer.md | 27 ++++++ .../src/__tests__/utils/AgentRegistry.test.ts | 86 +++++++++++++++++++ .../agent-manager/src/utils/AgentRegistry.ts | 30 ++++++- 7 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 docs/ai/design/2026-08-14-feature-agent-registry-wrapper-transfer.md create mode 100644 docs/ai/implementation/2026-08-14-feature-agent-registry-wrapper-transfer.md create mode 100644 docs/ai/planning/2026-08-14-feature-agent-registry-wrapper-transfer.md create mode 100644 docs/ai/requirements/2026-08-14-feature-agent-registry-wrapper-transfer.md create mode 100644 docs/ai/testing/2026-08-14-feature-agent-registry-wrapper-transfer.md diff --git a/docs/ai/design/2026-08-14-feature-agent-registry-wrapper-transfer.md b/docs/ai/design/2026-08-14-feature-agent-registry-wrapper-transfer.md new file mode 100644 index 00000000..a163a845 --- /dev/null +++ b/docs/ai/design/2026-08-14-feature-agent-registry-wrapper-transfer.md @@ -0,0 +1,28 @@ +--- +phase: design +title: Agent Registry Wrapper Transfer Design +description: Atomically reconcile same-type name ownership during discovery batches +--- + +# Agent Registry Wrapper Transfer Design + +## Design + +`registerBatch()` treats an incoming row whose name is owned by another PID of the same agent type as an identity transfer. It merges from the old name owner, assigns the incoming type and PID, deletes the old identity and any existing target-PID fallback, then saves the merged row in the same transaction. + +Transfer candidates are snapshotted before processing the batch. A duplicate name introduced by two incoming rows is therefore not mistaken for a wrapper handoff and still rolls back the batch with a uniqueness error. + +The merge preserves the old name, tmux session, and start time while preferring non-empty incoming cwd and session fields. + +## Safety Boundary + +- Transfer is available only through `registerBatch()`, the live-discovery write path. +- `register()` retains ordinary same-identity merge and unique-name enforcement. +- A name owned by another agent type is not transferable and remains a constraint error. +- A name created earlier in the same batch is not transferable. + +## Components + +- `AgentRegistry.mergeEntry()` must assign incoming identity fields when merging across PIDs. +- `AgentRegistry.registerBatch()` detects and applies transfers atomically. +- Registry unit tests cover transfers with and without a pre-existing child row. diff --git a/docs/ai/implementation/2026-08-14-feature-agent-registry-wrapper-transfer.md b/docs/ai/implementation/2026-08-14-feature-agent-registry-wrapper-transfer.md new file mode 100644 index 00000000..bb46e408 --- /dev/null +++ b/docs/ai/implementation/2026-08-14-feature-agent-registry-wrapper-transfer.md @@ -0,0 +1,29 @@ +--- +phase: implementation +title: Agent Registry Wrapper Transfer Implementation +description: Implementation notes for batch identity handoff in AgentRegistry +--- + +# Agent Registry Wrapper Transfer Implementation + +## Changed Files + +- `packages/agent-manager/src/utils/AgentRegistry.ts` + - Keeps `register()` on the normal same-PID merge path. + - Resolves a same-name, same-type transfer source during `registerBatch()`. + - Snapshots transfer candidates before iteration so new batch rows cannot become transfer sources. + - Deletes the source and any target fallback before saving the merged child row. + - Assigns incoming type and PID when merging entries across identities. +- `packages/agent-manager/src/__tests__/utils/AgentRegistry.test.ts` + - Covers direct transfer, transfer over a cached child fallback, and same-batch duplicate rejection. + +## Behavior + +The registry remains the single writer during list refresh. Wrapper-aware adapters can return the child PID with the wrapper's name, and the registry completes the handoff without adapter-side persistence or public API changes. + +## Edge Cases + +- Existing child fallback: removed before insertion so the wrapper start time is retained. +- Cross-type owner: not transferred. +- Same-batch duplicate: rejected and rolled back. +- Single registration: does not claim another live PID's name. diff --git a/docs/ai/planning/2026-08-14-feature-agent-registry-wrapper-transfer.md b/docs/ai/planning/2026-08-14-feature-agent-registry-wrapper-transfer.md new file mode 100644 index 00000000..ce840464 --- /dev/null +++ b/docs/ai/planning/2026-08-14-feature-agent-registry-wrapper-transfer.md @@ -0,0 +1,24 @@ +--- +phase: planning +title: Agent Registry Wrapper Transfer Plan +description: TDD and delivery tasks for wrapper-to-child identity reconciliation +--- + +# Agent Registry Wrapper Transfer Plan + +## Tasks + +- [x] Reproduce the live Gemini wrapper/child unique-name collision. +- [x] Add a failing registry test for direct wrapper-to-child transfer. +- [x] Implement minimal same-type batch identity transfer. +- [x] Add a failing test for transfer over an existing child fallback. +- [x] Preserve managed metadata and current child session data. +- [x] Keep duplicate names introduced within one batch as strict constraint failures. +- [x] Prove the regressions fail without the production fix and pass with it restored. +- [x] Run focused tests, full agent-manager tests, CLI tests, lint, typecheck, build, and a local CLI smoke test. +- [x] Prepare a validated, review-ready branch for commit and publication. + +## Risks + +- An overly broad transfer could hide unrelated name collisions. Mitigation: restrict transfer to batch writes and same-type owners. +- Replacing a cached child row could lose wrapper metadata. Mitigation: merge from the wrapper owner and replace both identities atomically. diff --git a/docs/ai/requirements/2026-08-14-feature-agent-registry-wrapper-transfer.md b/docs/ai/requirements/2026-08-14-feature-agent-registry-wrapper-transfer.md new file mode 100644 index 00000000..2ebe9a4d --- /dev/null +++ b/docs/ai/requirements/2026-08-14-feature-agent-registry-wrapper-transfer.md @@ -0,0 +1,32 @@ +--- +phase: requirements +title: Agent Registry Wrapper Transfer Requirements +description: Prevent live wrapper-to-child transitions from violating unique agent names +--- + +# Agent Registry Wrapper Transfer Requirements + +## Problem + +Gemini CLI runs a Node wrapper and child process. Detection returns the child PID while carrying the wrapper's registry name. If the live wrapper row still owns that name, `AgentManager.listAgents()` writes the child row and SQLite rejects it with `UNIQUE constraint failed: agents.name`. + +## Goals + +- Let batch discovery transfer a registry name between PIDs of the same agent type. +- Preserve the wrapper's managed name, tmux session, and original start time. +- Use the detected child's PID and current session metadata. +- Handle transitions both before and after a child fallback row is cached. +- Keep explicit single-entry registration and cross-type name conflicts strict. +- Reject duplicate names first introduced by separate entries in the same discovery batch. + +## Success Criteria + +- Both wrapper-to-child orderings complete atomically with one target-PID row. +- `agent list` no longer fails during the reproduced Gemini transition. +- Existing registry, manager, adapter, and CLI tests remain green. + +## Constraints + +- Keep the public `AgentRegistry` API unchanged. +- Do not add adapter-specific registry writes. +- Keep all deletes and insertion within the existing SQLite batch transaction. diff --git a/docs/ai/testing/2026-08-14-feature-agent-registry-wrapper-transfer.md b/docs/ai/testing/2026-08-14-feature-agent-registry-wrapper-transfer.md new file mode 100644 index 00000000..2d3d9218 --- /dev/null +++ b/docs/ai/testing/2026-08-14-feature-agent-registry-wrapper-transfer.md @@ -0,0 +1,27 @@ +--- +phase: testing +title: Agent Registry Wrapper Transfer Testing +description: Regression and integration evidence for wrapper-to-child registry transfers +--- + +# Agent Registry Wrapper Transfer Testing + +## Regression Cases + +- [x] A live wrapper name transfers to a newly detected child PID. +- [x] A live wrapper name replaces an existing child fallback row. +- [x] Managed tmux metadata and original start time survive transfer. +- [x] Incoming child session ID and file path replace stale wrapper metadata. +- [x] Duplicate names introduced within one batch remain constraint failures and roll back. +- [x] Removing the production change restores both regression failures, including the SQLite uniqueness error. + +## Validation + +- [x] Focused registry suite: 30 passed. +- [x] Full agent-manager coverage suite with process access: 513 passed; 89.03% statements, 77.95% branches, 95.94% functions, and 92.3% lines overall. +- [x] `AgentRegistry.ts` coverage: 98.63% statements and 92.15% branches. +- [x] CLI agent service and command tests: 104 passed. +- [x] Agent-manager typecheck and lint passed. +- [x] Monorepo build passed for all six projects. +- [x] Built local `agent list --json` completed against the previously failing local registry. +- [x] Base and feature docs lint passed. diff --git a/packages/agent-manager/src/__tests__/utils/AgentRegistry.test.ts b/packages/agent-manager/src/__tests__/utils/AgentRegistry.test.ts index 9f382fa6..0b180444 100644 --- a/packages/agent-manager/src/__tests__/utils/AgentRegistry.test.ts +++ b/packages/agent-manager/src/__tests__/utils/AgentRegistry.test.ts @@ -132,6 +132,92 @@ describe('AgentRegistry', () => { expect(registry.list()).toHaveLength(1); expect(registry.lookup('custom-name')?.pid).toBe(process.pid); }); + + it('transfers a same-type name from a live wrapper pid to its detected child pid', () => { + registry.register(makeEntry({ + name: 'managed-gemini', + type: 'gemini_cli', + pid: process.ppid, + tmuxSession: 'managed-gemini', + startedAt: '2026-05-30T00:00:00.000Z', + sessionId: `pid-${process.ppid}`, + sessionFilePath: '', + })); + + registry.registerBatch([makeEntry({ + name: 'managed-gemini', + type: 'gemini_cli', + pid: process.pid, + tmuxSession: '', + startedAt: '2026-05-31T00:00:00.000Z', + sessionId: 'gemini-session', + sessionFilePath: '/tmp/gemini-session.json', + })]); + + expect(registry.list()).toEqual([ + expect.objectContaining({ + name: 'managed-gemini', + type: 'gemini_cli', + pid: process.pid, + tmuxSession: 'managed-gemini', + startedAt: '2026-05-30T00:00:00.000Z', + sessionId: 'gemini-session', + sessionFilePath: '/tmp/gemini-session.json', + }), + ]); + }); + + it('replaces an existing child fallback when transferring its live wrapper name', () => { + registry.register(makeEntry({ + name: 'managed-gemini', + type: 'gemini_cli', + pid: process.ppid, + tmuxSession: 'managed-gemini', + startedAt: '2026-05-30T00:00:00.000Z', + sessionId: `pid-${process.ppid}`, + sessionFilePath: '', + })); + registry.register(makeEntry({ + name: `ai-devkit-${process.pid}`, + type: 'gemini_cli', + pid: process.pid, + tmuxSession: '', + startedAt: '2026-05-31T00:00:00.000Z', + sessionId: `pid-${process.pid}`, + sessionFilePath: '', + })); + + registry.registerBatch([makeEntry({ + name: 'managed-gemini', + type: 'gemini_cli', + pid: process.pid, + tmuxSession: '', + startedAt: '2026-06-01T00:00:00.000Z', + sessionId: 'gemini-session', + sessionFilePath: '/tmp/gemini-session.json', + })]); + + expect(registry.list()).toEqual([ + expect.objectContaining({ + name: 'managed-gemini', + type: 'gemini_cli', + pid: process.pid, + tmuxSession: 'managed-gemini', + startedAt: '2026-05-30T00:00:00.000Z', + sessionId: 'gemini-session', + sessionFilePath: '/tmp/gemini-session.json', + }), + ]); + }); + + it('rejects duplicate names introduced within the same batch', () => { + expect(() => registry.registerBatch([ + makeEntry({ name: 'duplicate', pid: process.pid }), + makeEntry({ name: 'duplicate', pid: process.ppid }), + ])).toThrow(/UNIQUE constraint failed: agents\.name/); + + expect(registry.list()).toEqual([]); + }); }); describe('lookup', () => { diff --git a/packages/agent-manager/src/utils/AgentRegistry.ts b/packages/agent-manager/src/utils/AgentRegistry.ts index 4b866dd0..47328bbb 100644 --- a/packages/agent-manager/src/utils/AgentRegistry.ts +++ b/packages/agent-manager/src/utils/AgentRegistry.ts @@ -79,6 +79,8 @@ export class AgentRegistry { const incomingIsManaged = Boolean(incoming.tmuxSession); return { ...existing, + type: incoming.type, + pid: incoming.pid, name: incomingIsManaged ? incoming.name : existing.name, tmuxSession: incoming.tmuxSession || existing.tmuxSession, cwd: incoming.cwd || existing.cwd, @@ -157,15 +159,39 @@ export class AgentRegistry { } register(entry: RegistryEntry): void { - this.registerBatch([entry]); + this.db.transaction(() => { + const existing = this.findByIdentity(entry.type, entry.pid); + this.save(this.mergeEntry(entry, existing)); + }); } registerBatch(entries: RegistryEntry[]): void { if (entries.length === 0) return; this.db.transaction(() => { + const preExistingByName = new Map(this.list().map((entry) => [entry.name, entry])); for (const incoming of entries) { const existing = this.findByIdentity(incoming.type, incoming.pid); - this.save(this.mergeEntry(incoming, existing)); + const nameOwner = preExistingByName.get(incoming.name); + const transferSource = nameOwner?.type === incoming.type + && nameOwner.pid !== incoming.pid + ? nameOwner + : undefined; + const merged = this.mergeEntry(incoming, transferSource ?? existing); + + if (transferSource) { + this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [ + transferSource.type, + transferSource.pid, + ]); + if (existing) { + this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [ + existing.type, + existing.pid, + ]); + } + } + + this.save(merged); } }); } From d8af2a922ace534d8be626bee1976062ffac6847 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Fri, 14 Aug 2026 21:50:51 +0200 Subject: [PATCH 2/2] docs(agent-manager): record post-rebase validation --- .../2026-08-14-feature-agent-registry-wrapper-transfer.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ai/testing/2026-08-14-feature-agent-registry-wrapper-transfer.md b/docs/ai/testing/2026-08-14-feature-agent-registry-wrapper-transfer.md index 2d3d9218..8fc8a9ce 100644 --- a/docs/ai/testing/2026-08-14-feature-agent-registry-wrapper-transfer.md +++ b/docs/ai/testing/2026-08-14-feature-agent-registry-wrapper-transfer.md @@ -18,8 +18,8 @@ description: Regression and integration evidence for wrapper-to-child registry t ## Validation - [x] Focused registry suite: 30 passed. -- [x] Full agent-manager coverage suite with process access: 513 passed; 89.03% statements, 77.95% branches, 95.94% functions, and 92.3% lines overall. -- [x] `AgentRegistry.ts` coverage: 98.63% statements and 92.15% branches. +- [x] Full post-rebase agent-manager coverage suite with process access: 521 passed; 89.03% statements, 78.01% branches, 95.94% functions, and 92.3% lines overall. +- [x] `AgentRegistry.ts` coverage: 98.64% statements and 92.85% branches. - [x] CLI agent service and command tests: 104 passed. - [x] Agent-manager typecheck and lint passed. - [x] Monorepo build passed for all six projects.