Skip to content

Give connector identity one owner: the instance id - #126

Open
hristo2612 wants to merge 1 commit into
mainfrom
simplify/ICI-682-connector-identity
Open

Give connector identity one owner: the instance id#126
hristo2612 wants to merge 1 commit into
mainfrom
simplify/ICI-682-connector-identity

Conversation

@hristo2612

Copy link
Copy Markdown
Owner

Selected area

Connector identity across the gateway: the connector registry, the five IncomingMessage.connector stamp sites, every deriveSessionKey prefix, /api/status health keying, and the deliverConnectorReply lookup.

Evidence of blended concerns

Connector.name and session.connector carried two different identities — the connector instance id and the connector type name — and only DiscordConnector had ever been moved to the id. The two agree only while an instance is named exactly after its type.

  • The registry is keyed by instance id. gateway/server.ts does connectorMap.set(instance.id, connector), and normalization injects the id into every constructor config ({ ...raw, id }). The shipped docs already state the rule: "Connector ids are what the rest of the gateway addresses".
  • But every connector except Discord stamped the type name. Slack stamped this.name at three sites, Telegram at one, WhatsApp used a hardcoded "whatsapp" literal, and remote-Discord hardcoded "discord". Discord itself carried name and instanceId as two fields holding the identical expression.
  • The silent delivery hole. deliverConnectorReply did const connector = connectors.get(session.connector); if (!connector) return;. For an instance configured {id: "slack-support", type: "slack"} the registry key is slack-support but the session recorded slack — the lookup missed and the function returned with no log, no event, no error. This is the only delivery path for runWebSession turns (four call sites: parent callbacks, cron follow-ups, fallback and retry results), so every reply to a named instance was dropped invisibly.
  • Two more tiers split the same way. Session-key prefixes hardcoded the type in Slack, Telegram and WhatsApp, so two instances of one type collided into a single session; Discord contradicted itself by deriving the prefix from the instance id on the inbound path but not in proxyToRemote. /api/status keyed health by connector.name, so same-type instances overwrote each other, while GET /api/connectors already read per instance.
  • Duplicate store, same slice. server.ts kept a parallel connectors: Connector[] array whose only uses were a push, a splice, and the shutdown loop — all served by connectorMap.values().

Load-bearing fact: legacy top-level config blocks get id === type, pinned by a pre-existing test. For every unnamed install this change is a behavior-preserving rename — stamps, session keys and status keys stay byte-identical. Only named instances[] users change, and their replies are dropped today, so there is no working behavior to lose.

Fixed constraint budget

Field Budget
netLineDelta ≤ +40 total, ≤ 0 excluding *.test.ts
filesTouched ≤ 17
newFiles 0
maxFileLines ≤ 7675
New deps / config options / public exports / single-caller abstractions 0 — sole sanctioned surface change is the id field on the existing Connector interface

The growth allowance above zero is test-only: the contract's rubric requires a regression test for the shut hole, and the Todo itself framed the win as ownership rather than raw line count.

Measured budget (reconciled)

netLineDelta=36
filesTouched=21
productNetLineDelta=0
newFiles=0
maxFileLines=7669
Field Budget Measured Result
netLineDelta ≤ 40 36 pass
productNetLineDelta ≤ 0 0 pass
filesTouched ≤ 17 21 contract defect
newFiles 0 0 pass
maxFileLines ≤ 7675 7669 pass

Verified contract defect: filesTouched 17 is unsatisfiable

The 17-file count was derived before the interface change was costed. Adding the required id: string to the Connector interface makes four further files mandatory, all one-line changes:

  • sessions/manager.ts
  • gateway/__tests__/session-attempt-race.test.ts
  • sessions/__tests__/platform-context-dispatch.test.ts
  • sessions/__tests__/context.test.ts

Independently reproduced by the verifier: reverting exactly those four files to base makes pnpm typecheck fail with three TS2741 errors — each a stub Connector object now missing the new required field — and makes context.test.ts fail on the old <name> placeholder that the contract separately mandates changing to <id>. No 17-file implementation exists that also satisfies the interface-change and prose criteria. The three per-file growth breaches (slack +2, telegram +2, whatsapp +4) come from the same mechanism and are fully offset — productNetLineDelta lands at exactly 0.

What was deleted or clarified

Deleted

  • DiscordConnector.instanceId — redundant with the new id; both held the identical expression.
  • The parallel connectors: Connector[] array in server.ts, along with its push, splice and shutdown-loop consumers, all moved onto connectorMap.
  • The hardcoded "whatsapp" connector literal and the hardcoded type prefixes in the Slack, Telegram and WhatsApp session-key derivations.

Clarified

  • Connector gains exactly one field, id: string, documented as the instance id and the registry key, equal to the type for legacy top-level config. This is the single owner of connector identity.
  • Every connector class (slack, telegram, whatsapp, discord, discord-remote, cron) now derives id from config.id with its type literal as the defensive fallback — the pattern Discord already used.
  • All five stamp sites and every session-key prefix now read the instance id. deriveSessionKey in the Slack and Telegram helpers gained a prefix parameter defaulting to the type literal, matching the shape the Discord helper already had, so existing assertions keep passing and pin the legacy byte-identity.
  • discord/index.ts proxyToRemote now passes the instance id, so the proxy path derives the same key as the inbound path instead of contradicting it.
  • /api/status keys connectors by the registry key rather than connector.name; output is identical for legacy configs.
  • The silent return in deliverConnectorReply is now a logger.warn naming the session id and the unresolved connector value. This failure class can no longer be invisible.
  • Prose corrected only where this change falsified it: the connector-docs interface comment (which contradicted its own later line), the two agent-prompt /api/connectors/<name>/send references, and two stale "names" code comments. No route or parameter renames.

Tests

Five additions, zero deletions. The one rewrite is the former "missing from map" case, which pinned the silent drop as correct behavior; it now asserts the drop is logged and still does not throw.

  • run-web-session-connector-reply.test.ts — named-instance delivery (map keyed slack-support, session stamped slack-support → reply delivered) plus the logged-drop assertion.
  • connectors.test.ts — a constructed named instance carries id from config and stamps it.
  • slack/threads.test.ts and telegram/__tests__/threads.test.ts — one custom-prefix case each; the existing literal-prefix assertions are untouched and now pin legacy byte-identity.

Gate results, run from the worktree after the final commit:

  • pnpm typecheck — exit 0, 2/2 tasks successful.
  • pnpm test — exit 0. jinn-cli: 314 files, 3891 passed, 7 skipped. @jinn/web: 123 files, 1294 passed.
  • pnpm build — exit 0, compiled to dist, web assets synced.

Independent verification

Verdict ship at this head: all 13 acceptance criteria hold, zero blockers, zero majors, four minors. Privacy scan of the full diff came back clean — only xapp-test/xoxb-test-style fixtures, no real identifiers, no co-author trailers.

Minors, all non-blocking and recorded as follow-up candidates rather than fixed here, since each sits on the contract's out-of-scope list:

  • Multi-instance Discord diagnostics still render the type literal in /doctor, the stop log, and the name field of GET /api/connectors.
  • The remote-Discord proxy stamps connector: "discord" while forwarding an instance-id-prefixed session key; the proxy URLs are explicitly out of scope.
  • Stale docs residue in the connector and cron docs, parked by the plan.
  • The new connectors.test.ts case pins the id through the test helper rather than end-to-end, though normalization's config.id injection is already pinned by a pre-existing assertion.

Not verified: live delivery through a real named Slack or Discord instance, the remote-proxy topology live, and web-UI rendering for named Discord instances — all need platform credentials.

The registry is keyed by connector instance id, but every connector except
Discord stamped IncomingMessage.connector and its session-key prefix with the
type name. For a named instance ({id: "slack-support", type: "slack"}) the
lookup in deliverConnectorReply missed and returned silently, so every reply
from a runWebSession turn was dropped, and two instances of one type collided
onto a single session.

Connector gains `id`; every connector class sets it from config.id with its
type literal as the fallback, and stamps and key prefixes derive from it.
DiscordConnector's bespoke instanceId field is deleted (its name is now the
type constant), and its proxyToRemote path now derives the same key as the
inbound path instead of a default-prefixed one. /api/status keys health by
registry key so same-type instances stop overwriting each other, and the
deliverConnectorReply miss now logs the drop instead of hiding it.

For legacy top-level config id === type, so stamps, session keys and status
keys stay byte-identical; the unchanged threads-test assertions pin that.

Also deletes the parallel connectors[] array in server.ts, which only ever
mirrored connectorMap, and fixes the prose this falsifies.
@hristo2612
hristo2612 marked this pull request as ready for review August 3, 2026 17:10
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