Skip to content

Daemon pool: collected low-severity per-member assumptions (cp_routing blob, thread-owner filter, channel/agents bind, env gate fail-open, usage RMW, replay log noise, unused data-plane transcript table) #1041

Description

@zfy0701

Collected low-severity leftovers from the same audit: "one daemon owns this agent for its lifetime" assumptions that are wrong on the pool but do not break a user-visible flow on their own. Filed together so they are recorded without a separate issue each. Related class: #955.

1. cp_routing is one install-wide row rewritten from each member's memory

local-store.ts:3120 upserts on the literal id = 1 (CHECK (id = 1), :831), driven by router/cp-routing-layer.ts:62, which serializes the process's whole assignments map on every upsertAssign / applyUpdate / converge. Each member's write erases every other member's. CpRoutingLayer's constructor (:27) then hydrates whichever member wrote last, so a member boots holding a foreign routing map and a foreign routingEpoch — and applyUpdate's stale guard (:41, if (u.routingEpoch < this.routingEpoch) return) will then discard legitimate global-rule updates until the real epoch catches up. Bounded because converge() on register/ok reasserts from the CP snapshot, so the exposure is boot → register/ok plus continuous cross-member write amplification on one hot row. Fix: key the row by member, or stop persisting it (the CP re-sends the full snapshot on every connect, which is what makes the corruption survivable today).

2. findThreadOwner excludes every pool agent

packages/control-plane/src/persistence/repositories/session.repo.ts:1673:

where: { channel, thread, agent: { daemonId: { not: null }, integrations: { some: { botId, status: 'active' } } } }

The predicate requires a non-null daemonId, so the pull-on-miss fallback that routes un-mentioned thread follow-ups (channel-root / case-2a sessions) never fires for a pool agent. Above it, orchestrator/httpBot.ts:417 (lookupThread) returns SharedThreadAgent.daemonId verbatim — an FK-less column (prisma/schema.prisma:2018) that names a reaped member after a rollout. Note orchestrator/agentRouting.ts:15 explicitly defers thread affinity to a later milestone, so treat the lookupThread half as known-deferred; the findThreadOwner filter is a separate, independently fixable blind spot (resolve through the placement resolver instead of requiring the column).

3. channel/agents ownership bind uses the ingress projection for an authority question

packages/control-plane/src/ws/handlers/channel-agents.ts:131:

const requester = orgRoster.find((a) => a.agentId === requesterAgentId)
if (requester?.daemonId !== conn.daemonId) return []   // null/undefined never equals a daemonId

resolveDirectory deliberately uses routableDaemon (confirmed holders only). Between a duty grant and the member's first confirming digest, a member that genuinely holds the agent is refused and the agent is told it has no peers. Fix: bind with the resolver's may-act predicate (holders, not confirmed holders) while keeping resolveDirectory for the wake targets.

4. The organization-environment compatibility gate fails open for pool agents

packages/control-plane/src/http/routes/organization-environment.ts:139:

if (daemonId === null) return true        // "unplaced is always fine"

called at :158 and :200 with agent.daemonId. A pool agent is placed but names no machine, so it reads as "unplaced" and the AGENT_CONFIG_REVISION_FEATURE precondition is skipped entirely. This is the inverse of the other findings in this class — a silent pass rather than a silent refusal — and worth fixing in the same pass so the two directions do not drift apart.

5. sessions.usage accumulation is a read-modify-write

local-store.ts:2029 / :2043 / :2059 / :2070: getUsage → merge in JS → writeUsage. addTokenUsage and addCost are genuinely additive and will lose increments under concurrency. Blast radius is one session and a session has one owner outside a handover, so this is token-accounting drift rather than a correctness break — but it is the same pattern, and a single relative UPDATE would close it.

6. Boot inbox replay logs one warn per foreign row

daemon.ts:20530: a member boots holding nothing, so replayInbox walks the whole install-wide inbox and logs durable inbox: skipping replay of <id> — unknown agent "<agentId>" for every pending row of every organization. Accurate when the store was one daemon's SQLite file; on the shared store it is per-Pod-start noise proportional to fleet backlog. Drop to debug, or scope the boot read.

7. The org-fenced data-plane transcript table is off the write path

PostgresDataPlane.transcripts — the agentconnect_data_plane.transcript table that carries org_id in every index (store/postgres-migrations.ts:28) — is constructed and then never read or written by the daemon (grep '\.transcripts\b' matches only its own constructor and flush). All transcript traffic goes to the LocalStore mirror in agentconnect_cloud_store, whose transcript / transcript_recipient tables have no org_id column at all (local-store.ts:798). Not a live bug — reads are keyed by (channel, thread) and go through agent-scoped callers — but the tenant fence that was written for this data is not the one in force, which is worth either wiring up or deleting so the schema stops implying a guarantee it does not provide.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions