{"record":{"id":"fb83e3827cd9a858","repo":"multica-ai/multica","slug":"wecom-bot-owned-by-same-workspace","errorCode":"wecom_bot_owned_by_same_workspace","errorMessage":"wecom: this bot is already connected to another agent in this workspace","messagePattern":"wecom: this bot is already connected to another agent in this workspace","errorType":"exception","errorClass":null,"httpStatus":409,"severity":"error","filePath":"server/internal/integrations/wecom/installation.go","lineNumber":302,"sourceCode":"\t}\n\treturn Installation{}, nil\n}\n\n// Sentinels for the one conflict Upsert cannot resolve: the bot is already\n// connected somewhere else. UpsertChannelInstallation conflicts on\n// (workspace_id, agent_id, channel_type), but idx_channel_installation_type_appid\n// is UNIQUE on (channel_type, config->>'app_id') — so connecting the SAME bot to\n// a second agent misses the ON CONFLICT clause entirely and trips the index\n// instead. Without these the admin reads the raw Postgres text\n// (\"duplicate key value violates unique constraint …\") in a toast.\n//\n// One bot is one connection: the WeCom long connection allows a single\n// live subscriber per bot, so two agents cannot share one. The way out is\n// always to free the bot first, which is what each message says.\nvar (\n\t// ErrBotOwnedBySameWorkspace — another agent in the admin's own workspace\n\t// holds the bot. Reversible from the same settings screen.\n\tErrBotOwnedBySameWorkspace = errors.New(\"wecom: this bot is already connected to another agent in this workspace\")\n\n\t// ErrBotOwnedByArchivedAgent — the holder is archived, so it does not show\n\t// up in the agent list and the bot looks free while it is not.\n\tErrBotOwnedByArchivedAgent = errors.New(\"wecom: this bot is connected to an archived agent in this workspace\")\n\n\t// ErrBotOwnedByAnotherWorkspace — the holder is out of sight entirely and\n\t// only someone with access there can release it.\n\tErrBotOwnedByAnotherWorkspace = errors.New(\"wecom: this bot is already connected to a different Multica workspace\")\n)\n\n// pgUniqueViolation is Postgres' unique_violation SQLSTATE.\nconst pgUniqueViolation = \"23505\"\n\n// botSlotConflictErr answers one question: may this install act on the\n// (wecom, bot_id) routing slot at all? It returns the sentinel to refuse with,\n// or nil to proceed. MUST be called inside the transaction holding\n// LockChannelInstallationAppIDSlot — outside it the answer is a guess that a\n// concurrent install or reconnect can invalidate before it is used.","sourceCodeStart":284,"sourceCodeEnd":320,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/integrations/wecom/installation.go#L284-L320","documentation":"WeCom install ownership error: another agent in the SAME workspace already holds this bot. The UpsertChannelInstallation ON CONFLICT targets (workspace_id, agent_id, channel_type), but the UNIQUE index on (channel_type, config->>'app_id') makes a second agent trip the index instead; this sentinel replaces the raw Postgres duplicate-key text with a message naming the reachable fix. One bot = one live long-connection subscriber, so it cannot be shared.","triggerScenarios":"Connecting a WeCom bot to agent B while agent A in the same workspace already has it installed — the upsert misses the ON CONFLICT clause and raises unique_violation (SQLSTATE 23505) on idx_channel_installation_type_appid, which the service translates to this error.","commonSituations":"Reassigning which agent fronts a WeCom bot without disconnecting first; duplicated agents during setup; admin assumes bots are shareable across agents.","solutions":["Disconnect the bot from the other agent in this workspace (settings screen), then connect it to the intended agent.","If unsure who holds it, list installations for the workspace and find the (channel_type='wecom', same app_id) row.","For genuinely shared needs, connect each agent to a different WeCom bot."],"exampleFix":"// before: raw Postgres error reaches the admin\n// \"duplicate key value violates unique constraint idx_channel_installation_type_appid\"\n\n// after: service maps 23505 to actionable sentinels\nif errors.Is(err, wecom.ErrBotOwnedBySameWorkspace) {\n\trespond(w, 409, \"free the bot first: disconnect it from the other agent in this workspace\")\n\treturn\n}","handlingStrategy":"try-catch","validationCode":"owner, err := queries.GetChannelInstallationOwnerByAppID(ctx, \"wecom\", appID)\nif err == nil && owner.WorkspaceID == thisWorkspace && owner.AgentID != params.AgentID {\n\treturn respondConflict(w, \"disconnect the bot from its current agent first\")\n}\n_ = installSvc.Install(ctx, params)","typeGuard":null,"tryCatchPattern":"if err := installSvc.Install(ctx, params); err != nil {\n\tif errors.Is(err, wecom.ErrBotOwnedBySameWorkspace) {\n\t\t// 23505 on (channel_type, app_id) mapped to an actionable message\n\t\treturn respondConflict(w, \"free the bot: disconnect it from the other agent in this workspace\")\n\t}\n\treturn err\n}","preventionTips":["One WeCom bot per agent, always: disconnect before reconnecting elsewhere.","When a 23505 surfaces anywhere in the install path, translate it to these sentinels rather than showing raw Postgres text.","Remember the ON CONFLICT target differs from the unique index — a second agent never upserts, it trips the index."],"tags":["wecom","conflict","unique-constraint","installation","go"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}