Skip to content

fix(auth): persist stable --account alias on QR login - #53

Merged
NewFuture merged 5 commits into
NewFuture:mainfrom
ericcaiwx-star:fix/persist-login-account-alias
Aug 8, 2026
Merged

NewFuture merged 5 commits into
NewFuture:mainfrom
ericcaiwx-star:fix/persist-login-account-alias

Conversation

@ericcaiwx-star

Copy link
Copy Markdown
Collaborator

Summary

  • On QR login, persist credentials under both the stable CLI --account alias and the normalized ilink_bot_id, so multi-account configs (leader / jinjin / …) resolve without hand-copying JSON files.
  • Stale-userId cleanup keeps the primary + alias credential files.
  • Docs: CHANGELOG (zh/en) + multi-account README notes.

Motivation

openclaw channels login --channel openclaw-weixin --account collin currently only writes normalizeAccountId(ilink_bot_id).json. Operators who bind peers by readable accountId then need a manual cp of the credential file. This PR makes login write both paths.

Test plan

  • npx vitest run src/auth/account-store.test.ts src/channel.test.ts (31 pass)
  • Manual: channels login --account <alias> → both <alias>.json and <botHash>.json exist under credentials dir
  • Manual: gateway resolves peer binding by alias without copying files

Notes

  • No version bump (prefer maintainer auto-release).
  • Will also open a secondary port to Tencent/openclaw-weixin and cross-link.

Made with Cursor

Multi-account setups bind readable accountIds (leader/jinjin/…) while iLink
only returns hex@im.bot. Write both credential files and keep both during
stale-userId cleanup so alias-based config resolves without hand-copying.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ericcaiwx-star

ericcaiwx-star commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Secondary port to Tencent: Tencent/openclaw-weixin#248 (same change set).

Co-authored-by: Cursor <cursoragent@cursor.com>
@NewFuture
NewFuture requested a balanced review from Copilot August 6, 2026 16:03

Copilot AI 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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates Weixin QR-login persistence to optionally store credentials under a stable --account alias in addition to the server ilink_bot_id, improving multi-account setups that bind config to human-readable account IDs.

Changes:

  • Added persistWeixinLoginAccounts + resolveLoginAccountAlias and updated stale-account cleanup to accept a keep-list (primary + alias).
  • Updated channel login flows to use the new persistence helper and log primary/alias mapping when applicable.
  • Added/updated tests and documentation/changelog entries describing alias persistence behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/channel.ts Switches QR-login persistence to persistWeixinLoginAccounts and logs alias mapping.
src/channel.test.ts Updates mocks to match the new accounts API.
src/auth/accounts.ts Adds alias resolution + dual-write persistence; updates stale cleanup to accept keep-list.
src/auth/account-store.test.ts Adds tests for keep-list stale cleanup and new alias/persist helpers.
README_EN.md Documents using --account aliases and dual credential files.
README.md Chinese docs for stable alias usage and dual credential files.
CHANGELOG_EN.md Notes fix for persisting stable --account aliases on QR login.
CHANGELOG.md Chinese changelog entry for stable alias persistence fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/channel.ts Outdated
Comment thread src/auth/accounts.ts
Comment thread src/auth/accounts.ts
Comment thread src/channel.test.ts

@NewFuture NewFuture left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alias persistence flow still has four correctness issues; see the inline comments.

Comment thread src/auth/accounts.ts Outdated
Comment thread src/channel.ts
Comment thread src/channel.ts Outdated
Comment thread src/auth/accounts.ts Outdated

@ericcaiwx-star ericcaiwx-star left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collaborator note (author hat off — no merge from me)

Agree with @NewFuture’s four correctness blockers; they match how our multi-account gateway actually boots. I would not merge this shape as-is.

Confirming the blockers (prod lens)

  1. Dual registerWeixinAccountId (primary + alias)listAccountIds would spawn two monitors on one bot token (split sync-buffer / context-token / replay-dedupe). That reintroduces the double-delivery class of bugs we just spent time killing. Canonical runtime ID must be one (prefer explicit alias when present); bot-hash file can remain a lookup/compat credential, not a second indexed runtime account.
  2. "default" sentinel as requestedAccountId on bare channels login would invent a spurious default alias + dual index even when the user never passed --account. Need to exclude host DEFAULT / only treat explicitly supplied aliases.
  3. alreadyConnected / binded_redirect path skipping alias persistence makes the migration a no-op for the hash-only installs that need it most — must migrate unambiguous stored creds or fail loudly.
  4. Clear-stale before index publish is a crash-window data loss risk; publish canonical index first (or transactional rollback) + fault-injection test.

Suggested reshape (for a follow-up push)

  • Runtime index: alias XOR primary, never both.
  • Credential files: may still write bot-hash JSON for tooling, but only the canonical id is registered for gateway tasks.
  • Login entry tests: accountId: "default" must not create an alias; --account leader on already-bound hash must migrate or error.
  • Keep changelog under Unreleased (already good); no version bump.

I’ll rework #53 along those lines in a later commit unless @NewFuture prefers a different canonical-id rule. Merge remains with the owner.

…grate

Address NewFuture review on NewFuture#53: index only one runtime account (alias
preferred), ignore the host default sentinel, migrate unambiguous
hash-only bindings on binded_redirect, and publish the index before
stale cleanup with a fault-injection regression.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ericcaiwx-star

Copy link
Copy Markdown
Collaborator Author

Pushed a follow-up addressing the four review blockers:

  1. Single canonical runtime id — alias preferred; bot-hash credential may still be written for lookup, but is not dual-registered into listAccountIds.
  2. Host default sentinelresolveLoginAccountAlias ignores it; bare login indexes only the bot id.
  3. alreadyConnected / binded_redirectmigrateBoundAccountToAlias migrates an unambiguous token-bearing indexed account onto the requested alias, or fails with an actionable ambiguity / missing-creds error (no silent no-op when an alias was requested).
  4. Index before cleanuppublishCanonicalAccountIndex runs before clearStaleAccountsForUserId; added a fault-injection test that a failed index write leaves the previous index intact.

npm run check:fast: 584 tests passed. Merge still for @NewFuture.

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

src/auth/accounts.ts:193

  • If this alias already belongs to a different token, saveWeixinAccount overwrites only its credentials while retaining alias-scoped cursor, context-token, replay-dedupe, and allowFrom state. The new bot can therefore inherit authorization grants and protocol state from an unrelated account. Detect a differing existing token before writing and either reject the alias collision or explicitly clear all account-owned state before rebinding it.
  const aliasId = resolveLoginAccountAlias(params.requestedAccountId, primaryId);
  const canonicalId = aliasId ?? primaryId;
  if (aliasId) {
    saveWeixinAccount(aliasId, creds);

src/auth/accounts.ts:269

  • This migration path can also overwrite an existing unindexed alias that holds a different token, leaving that alias's old account-scoped state and authorization file in place. Apply the same token-collision validation/destructive-reset policy here before saving the source credentials onto the alias.
  saveWeixinAccount(aliasId, creds);

src/channel.ts:578

  • The new alreadyConnected adapter behavior is untested: src/channel.test.ts only adds mocks and never asserts migration, reload, error propagation, or the new connected: true result. Add adapter-level regression cases for an alias migration, a no-op/default account, and a migration failure so the changed channel contract is covered.
      } else if (result.alreadyConnected) {
        try {
          const migrated = migrateBoundAccountToAlias({
            requestedAccountId: params.accountId,
            onClearContextTokens: clearContextTokensForAccount,

Comment thread src/auth/accounts.ts Outdated
@ericcaiwx-star

Copy link
Copy Markdown
Collaborator Author

@NewFuture 按你上次标的四点都改完了(badcc80):

  1. 运行时索引只登记一个 canonical id(有别名时优先别名;bot-hash 凭证可留作查找,不再双 register)
  2. 宿主 default 哨兵不会被当成别名
  3. alreadyConnected / binded_redirect:不歧义时迁到 --account 别名,歧义/无凭证则报错(不再静默成功)
  4. 先发布索引再清 stale;索引写入失败保留旧索引(有故障注入测试)

当前 CI 全绿。请再扫一眼还有没有问题;没问题的话麻烦你这边 squash 合并(发版仍走你的 release 流程)。谢谢。

@NewFuture
NewFuture requested review from NewFuture and a balanced review from Copilot August 7, 2026 15:20

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Suppressed comments (10)

src/auth/accounts.ts:239

  • When the alias and its companion bot-hash ID are both indexed, this early return leaves both entries in accounts.json, so the gateway can still start two monitors with the same token—the condition this change is intended to repair. Republish the alias as canonical and drop the discovered companion before returning.
  const aliasEntry = indexedWithToken.find((entry) => entry.id === aliasId);
  if (aliasEntry) {
    const aliasToken = aliasEntry.data.token?.trim() ?? "";
    const companion = (aliasToken ? findCompanionBotAccountId(aliasToken, aliasId) : null) ?? aliasId;
    return { primaryId: companion, aliasId, canonicalId: aliasId };

src/auth/accounts.ts:285

  • This new diagnostic persists the alias and source account ID verbatim. Avoid recording account identifiers in logs; the migration event itself is sufficient for diagnosis.
  logger.info(`migrateBoundAccountToAlias: canonical=${aliasId} from source=${source.id}`);

src/channel.ts:420

  • Both the user-facing output and persistent info log include the alias/source account IDs verbatim. Confirm the migration without emitting either identifier so captured diagnostics do not disclose account-identifying data.
            log(`\n已将已绑定账号迁移为稳定别名 ${migrated.canonicalId}(凭证兼存 ${migrated.primaryId})。`);
            logger.info(
              `auth.login: migrated already-connected bot to alias=${migrated.canonicalId} from=${migrated.primaryId}`,
            );

src/channel.ts:569

  • This diagnostic logs the canonical and primary account IDs verbatim. Preserve the branch distinction without including identifier values.
          logger.info(
            aliasId
              ? `loginWithQrWait: saved account data canonical=${canonicalId} primary=${primaryId}`
              : `loginWithQrWait: saved account data for accountId=${canonicalId}`,
          );

src/channel.ts:584

  • The migration log records both account IDs verbatim. Log only that the already-connected credential was migrated, not the identifying values.
            logger.info(
              `loginWithQrWait: migrated already-connected bot to alias=${migrated.canonicalId} from=${migrated.primaryId}`,
            );

src/auth/accounts.ts:254

  • The ambiguity error embeds every indexed account ID and is subsequently copied to the CLI and error logs by both callers. Since the IDs can include normalized server bot identifiers, report the count and remediation without enumerating them.
    const ids = indexedWithToken.map((entry) => entry.id).join(", ");
    throw new Error(
      `weixin: already connected, but multiple bound accounts are ambiguous (${ids}). ` +
        `Re-login with force for a single account, or remove the extra credentials before migrating to --account ${aliasId}.`,
    );

src/auth/accounts.ts:70

  • writeFileSync opens and truncates the existing index before writing, so a real ENOSPC/I/O failure can leave accounts.json empty or partial; the new test only simulates an exception before the underlying write. This does not provide the documented guarantee that a failed publish preserves the previous index. Write a sibling temporary file and atomically replace the index (with cleanup) instead.
/** Replace the persistent account index in a single write. */
function writeAccountIndex(accountIds: string[]): void {
  const dir = resolveWeixinStateDir();
  fs.mkdirSync(dir, { recursive: true });
  fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify(accountIds, null, 2), "utf-8");

src/channel.ts:578

  • The adapter now turns alreadyConnected into success and invokes alias migration, but channel.test.ts only adds mocks and has no assertions for either login entry point. Add regression cases that verify the requested alias is forwarded, migration/reload occurs, connected is true for alreadyConnected, and migration failures reject; repository guidance requires tests for every behavior change.
      } else if (result.alreadyConnected) {
        try {
          const migrated = migrateBoundAccountToAlias({
            requestedAccountId: params.accountId,
            onClearContextTokens: clearContextTokensForAccount,

src/auth/accounts.ts:194

  • This writes both the stable alias and normalized server bot ID verbatim to persistent logs. Those identifiers can identify an account when logs are collected for support; keep the event but omit or redact the values.

This issue also appears in the following locations of the same file:

  • line 250
  • line 285
    logger.info(`persistWeixinLoginAccounts: wrote alias=${aliasId} alongside primary=${primaryId}`);

src/channel.ts:400

  • The success output exposes the normalized server bot ID (primaryId), which is account-identifying data and may be retained in terminal/installer logs. The operator only needs confirmation that the companion credential was saved.

This issue also appears in the following locations of the same file:

  • line 417
  • line 565
  • line 582
          log(
            aliasId
              ? `\n已将此 OpenClaw 连接到微信(账号 ${canonicalId},凭证兼存 ${primaryId})。`
              : `\n已将此 OpenClaw 连接到微信。`,
          );

Comment thread src/auth/accounts.ts Outdated
);
}

const source = indexedWithToken[0];
@NewFuture

Copy link
Copy Markdown
Owner

@ericcaiwx-star 感谢 badcc80 把前面四项都处理了:单 canonical index、default sentinel、alreadyConnected 不再静默、以及先发布索引再清 stale 都已确认修复。

我结合 OpenClaw 2026.7.1 的 channels 实现又走了一遍真实流程,还有一个设计层面的顾虑想和你确认:当前实现实际上是把 hash accountId 在线重命名成 alias,而不只是保存显示别名。

  • CLI 在 auth.login({ accountId: alias }) 返回后,会继续显式调用 channels.start(alias)
  • Gateway 对显式 accountId 直接创建对应 task;它不会把 alias canonicalize 到 hash,也不会淘汰仍在运行的 hash task。因此现有 hash monitor 可能与 alias monitor 短暂并存;gateway.reload.mode=off、配置写入失败或 watcher 失效时会持续并存。
  • accounts.json 改成 alias 后,原 hash 下的 poll cursor、context tokens、replay dedupe、pairing allow-list 仍留在旧 namespace(现有 thread r3735075938)。
  • OpenClaw bindings 和 channels.openclaw-weixin.accounts[...] 都按 accountId 精确匹配,因此旧 hash 配置也不会自动跟随。
  • 如果已有 alias 属于另一 token,当前写法还会覆盖凭证但保留 alias 旧状态/授权。

在不修改 OpenClaw core 的前提下,我觉得有两个相对小且安全的方向:

  1. 缩小本 PR 范围:alias 只允许在账号首次连接时确定;alreadyConnected 的 hash→alias 在线改名明确报错。实现最小,但存量 hash 用户暂时不能改 alias。
  2. 插件内逻辑映射:primary hash 始终作为 listAccountIds、monitor 和状态存储 ID;增加一对一 alias → hash 映射,bindings/出站使用 alias,旧 hash 配置作为回退,并阻止 alias lifecycle task 启动 transport。这样存量用户无需迁移状态,但 channels status/start/stop 仍以 hash 为准。

如果这个 PR 的核心需求必须覆盖存量 hash-only 用户,我更倾向方案 2;如果只解决新登录时稳定命名,方案 1 更简单。想听下你对目标使用场景和这两个取舍的看法,我们再决定是否需要继续调整实现。

@ericcaiwx-star

Copy link
Copy Markdown
Collaborator Author

@NewFuture 感谢把宿主侧真实生命周期也摊开了,这个顾虑成立。

我们这边更倾向方案 2(插件内 alias → hash 逻辑映射)

使用场景是单 gateway、多个人微:名册/bindings 用稳定别名(如 leader / jinjin / …)绑到不同 agent,现场往往已经有 hash-only 凭证。需要:

  • 对外继续用可读 accountId 做 bindings / 出站
  • transport、poll cursor、context token、replay dedupe 等状态始终落在 primary hash 上,避免在线改名导致双 monitor 或状态漂移
  • 存量用户尽量不用为了改别名再强制重扫

方案 1 对「只解决新登录命名」更干净,但覆盖不了我们这种多账号 roster + 存量 hash 的场景。

若你认可方案 2,我按这个方向改一版(primary hash 唯一进 listAccountIds / monitor;一对一映射;阻止 alias lifecycle 再起一条 transport),再请你审。

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Suppressed comments (10)

src/auth/accounts.ts:254

  • The thrown error includes every bound account ID and is subsequently logged verbatim by both login paths. This exposes sensitive account IDs contrary to AGENTS.md:34-37; report the candidate count and recovery action without listing IDs or the requested alias.
  if (indexedWithToken.length > 1) {
    const ids = indexedWithToken.map((entry) => entry.id).join(", ");
    throw new Error(
      `weixin: already connected, but multiple bound accounts are ambiguous (${ids}). ` +
        `Re-login with force for a single account, or remove the extra credentials before migrating to --account ${aliasId}.`,
    );

src/auth/accounts.ts:285

  • This diagnostic persists the canonical alias and source account ID verbatim. Account IDs are sensitive under AGENTS.md:34-37; log only that migration completed, or redact both values.
  logger.info(`migrateBoundAccountToAlias: canonical=${aliasId} from source=${source.id}`);

src/channel.ts:420

  • Both the runtime message and persistent diagnostic include raw alias/source account IDs. These IDs are sensitive under AGENTS.md:34-37; report migration success without interpolating either value.
            log(`\n已将已绑定账号迁移为稳定别名 ${migrated.canonicalId}(凭证兼存 ${migrated.primaryId})。`);
            logger.info(
              `auth.login: migrated already-connected bot to alias=${migrated.canonicalId} from=${migrated.primaryId}`,
            );

src/channel.ts:568

  • This persistent diagnostic logs the canonical alias and bot ID verbatim, which violates the account-ID handling rule in AGENTS.md:34-37. Use a value-free success message and narrow the earlier destructuring accordingly.
          logger.info(
            aliasId
              ? `loginWithQrWait: saved account data canonical=${canonicalId} primary=${primaryId}`
              : `loginWithQrWait: saved account data for accountId=${canonicalId}`,

src/channel.ts:584

  • This logs both migrated account IDs into the gateway log. Account IDs are sensitive under AGENTS.md:34-37; retain only the migration event, without the raw values.
            logger.info(
              `loginWithQrWait: migrated already-connected bot to alias=${migrated.canonicalId} from=${migrated.primaryId}`,
            );

src/auth/accounts.ts:109

  • The keep set protects the current alias and primary, but stale cleanup still enumerates only indexed IDs. After bot-old + alias-old is persisted, bot-old is deliberately unindexed; logging in the same user under a new bot removes only alias-old and leaves the old bot-hash credential, context tokens, and allow-list usable. Enumerate/track companion credential files during stale cleanup and remove stale companions while preserving every current keep ID.
  const keep = new Set(
    (Array.isArray(keepAccountIds) ? keepAccountIds : [keepAccountIds]).map((id) => id.trim()).filter(Boolean),
  );
  if (keep.size === 0) return;

src/auth/accounts.ts:194

  • This writes both account IDs verbatim to the persistent gateway log. Account IDs are classified as sensitive by AGENTS.md:34-37; keep this diagnostic value-free (or use an account-ID redaction helper) instead.

This issue also appears in the following locations of the same file:

  • line 249
  • line 285
    logger.info(`persistWeixinLoginAccounts: wrote alias=${aliasId} alongside primary=${primaryId}`);

src/channel.ts:399

  • The new success message sends the alias and server bot ID through runtime.log, exposing sensitive account IDs contrary to AGENTS.md:34-37. Keep the status message generic and destructure only aliasId if the other result fields are no longer needed.

This issue also appears in the following locations of the same file:

  • line 417
  • line 565
  • line 582
          log(
            aliasId
              ? `\n已将此 OpenClaw 连接到微信(账号 ${canonicalId},凭证兼存 ${primaryId})。`
              : `\n已将此 OpenClaw 连接到微信。`,

src/channel.ts:593

  • The plugin-level behavior changes are not exercised in channel.test.ts: the account helpers are mocked, but no test drives successful alias persistence, alreadyConnected migration, error propagation, or this new connected: true result. Add mocked channel tests for both login entry points so the host-facing contract is covered, as required for behavior changes by the repository testing rules.
        connected: result.connected || Boolean(result.alreadyConnected),

src/auth/account-store.test.ts:254

  • This opaque Weixin-shaped user ID is not clearly synthetic, unlike the surrounding user-a@im.wechat fixtures. Repository privacy rules require obviously synthetic identifiers in tests (AGENTS.md:34-37); replace it in both the input and assertion with a labeled synthetic value.
      userId: "o9cq80zLSSEWjtr2UODlOgvt3pO4@im.wechat",

Comment thread src/auth/accounts.ts Outdated
function writeAccountIndex(accountIds: string[]): void {
const dir = resolveWeixinStateDir();
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify(accountIds, null, 2), "utf-8");
Comment thread src/auth/accounts.ts Outdated
Comment on lines +235 to +240
const aliasEntry = indexedWithToken.find((entry) => entry.id === aliasId);
if (aliasEntry) {
const aliasToken = aliasEntry.data.token?.trim() ?? "";
const companion = (aliasToken ? findCompanionBotAccountId(aliasToken, aliasId) : null) ?? aliasId;
return { primaryId: companion, aliasId, canonicalId: aliasId };
}
Keep listAccountIds/monitors/state on the bot-hash id and store a 1:1
alias→hash map for bindings/outbound, so host start(alias) cannot spawn
a second transport after QR login.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ericcaiwx-star

Copy link
Copy Markdown
Collaborator Author

@NewFuture 按你提的方案 2改完了(35cef00),核心变化如下:

设计

  • primary hash 始终是 listAccountIds / monitor / poll cursor / context token / replay dedupe 的唯一命名空间
  • 新增一对一 account-aliases.jsonalias → hash 逻辑映射,供 bindings / 出站 resolveAccount 使用
  • 宿主在 auth.login({ accountId: alias }) 后显式 start(alias)不再启 transport(避免与 hash monitor 双开)
  • 入站 resolveAgentRoute 使用 routeAccountId(有别名时用别名),所以 openclaw.json bindings 仍可写可读 accountId
  • 不做在线改名,也不把状态从 hash 搬到 alias;alreadyConnected 只登记映射
  • accounts.json 改为原子写入(tmp + rename)

验证

  • 相关单测已覆盖:只索引 primary、alias 映射、resolve 走 alias、start(alias) no-op、start(primary)routeAccountId
  • 本地 typecheck + 相关 suite 通过

请再审一眼这个方向是否符合你走宿主生命周期后的预期。若 OK,麻烦你这边 squash 合并(发版仍走你的 release 流程)。谢谢。

@NewFuture NewFuture left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 35cef00 against the OpenClaw 2026.7.1 lifecycle. The mapping direction fixes the online-rename/state-namespace design, but three reachable edge cases remain.

Comment thread src/channel.ts
if (account.accountId !== primaryId) {
logger.info("gateway.startAccount: skipping alias lifecycle task (transport owned by primary)");
ctx.setStatus?.({ accountId: account.accountId, running: false });
return;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenClaw 2026.7.1 treats a fulfilled startAccount() as an unexpected channel exit. After this return, server-channels.ts records channel exited without an error and schedules up to 10 auto-restarts unless the runtime has terminalDisconnect: true. Since login explicitly starts the requested alias, reload-off/failure turns this intended no-op into a backoff loop. Mark this lifecycle terminal (or keep it pending until abort) and cover the host lifecycle behavior.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked against the minimum supported OpenClaw 2026.6.1: terminalDisconnect is not consulted in that version, so setting it alone would leave the restart loop there. The smallest cross-version fix is to make config.isEnabled return false when the resolved request is an alias (accountId !== primaryId). listAccountIds() already exposes only primary IDs, and explicit outbound sends still resolve the alias without this lifecycle check, while channels.start(alias) is rejected before any task is created. The early-return branch can remain as defensive behavior for direct adapter callers.

Comment thread src/auth/accounts.ts
if (aliasId !== source.primary) {
clearWeixinAccount(aliasId);
}
bindWeixinAccountAlias(aliasId, source.primary);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When requestedAccountId is the existing primary hash, resolveLoginAccountAlias(..., "") initially treats it as an alias. After the sole source is resolved, aliasId === source.primary, so this call throws (alias must differ from the primary bot id). Therefore rerunning channels login --account <existing-hash> on binded_redirect now fails instead of succeeding as a no-op. Detect equality after source resolution and skip alias binding.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still confirmed. The minimal fix is an equality no-op after resolving the source: if aliasId === source.primary, return null before calling bindWeixinAccountAlias(), then cover explicit hash relogin with a regression test. No migration machinery is needed for this case.

Comment thread src/auth/accounts.ts Outdated
const aliasId = resolveLoginAccountAlias(params.requestedAccountId, primaryId);
if (aliasId) {
// Drop leftover alias-scoped credential/state from the prior rename design.
adoptAccountStateNamespace(aliasId, primaryId);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This moves sync/context/allow-list state before proving that the leftover alias belongs to the newly confirmed primary. If leader still represents bot A from the prior online-rename state and a fresh login reuses leader for bot B, bot B inherits A's authorization/context and A's alias state is removed. Check the existing alias credential/mapping first and only adopt state when its token/companion proves the same bot; otherwise reject the collision (or rebind without inheriting state).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid overdesign here: badcc80 was never included in a release or tag; released users are hash-only, so there is no production alias namespace that must be migrated. The safer minimal fix is to remove adoptAccountStateNamespace() (and the companion fallback used only for that intermediate design), keep existing state on the primary hash, and reject an alias if an existing alias credential/map points to a different token/primary. Do not move sync, context, or pairing allowlist state between identities.

Disable alias accounts in config.isEnabled so start(alias) never creates a
restarting lifecycle task; treat hash relogin as a no-op; reject conflicting
alias credentials without moving state namespaces.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ericcaiwx-star

Copy link
Copy Markdown
Collaborator Author

@NewFuture 按你标的三点最小修法改完了(最新 commit):

  1. config.isEnabledaccountId !== primaryId(别名)时返回 false,让宿主 channels.start(alias) 在建 task 前被拒,避免 2026.6.1/2026.7.1 的 restart loop;startAccount 里对别名的 early-return 仍保留作防御。
  2. hash 重登 no-opmigrateBoundAccountToAlias 在解析到源账号后若 aliasId === source.primary,直接 return null,不再走 bindWeixinAccountAlias;已加回归测试。
  3. 去掉状态迁移:删除 adoptAccountStateNamespace 与 companion fallback;别名凭证/映射若指向不同 token/primary 则拒绝绑定,不搬 sync/context/allow-list。

相关单测已覆盖。请再扫一眼;没问题的话麻烦 squash 合并。谢谢。

Copilot AI 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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

src/auth/accounts.ts:71

  • This helper also writes account-aliases.json, which contains stable aliases and primary account IDs, but the temporary/final file is created with the process-default permissions. Restrict the file at creation time, as the credential store does, so another local user cannot read account identifiers during or after the rename.
  fs.writeFileSync(tmpPath, JSON.stringify(value, null, 2), "utf-8");

src/auth/account-store.test.ts:265

  • The PR description and manual test plan require both <alias>.json and <botHash>.json, but this assertion explicitly enforces that the alias credential file does not exist. Either implement the documented dual-file persistence or update the PR description/test plan to describe the logical alias-map design so the acceptance criterion is unambiguous.
    // Alias is logical only — no second credential / state namespace.
    expect(loadWeixinAccount("collin")).toBeNull();

src/messaging/process-message.ts:153

  • Add a focused regression test that passes distinct primary and routeAccountId values and verifies resolveAgentRoute receives the alias. The current process-message tests exercise only the fallback where both IDs are identical, so the new binding behavior can regress even though the channel-level test still confirms the option was forwarded to the monitor.
  const routeAccountId = deps.routeAccountId?.trim() || deps.accountId;
  const route = deps.channelRuntime.routing.resolveAgentRoute({
    cfg: deps.config,
    channel: "openclaw-weixin",
    accountId: routeAccountId,

Comment thread src/auth/accounts.ts
Comment on lines +341 to +345
saveWeixinAccount(primaryId, creds);

const aliasId = resolveLoginAccountAlias(params.requestedAccountId, primaryId);
if (aliasId) {
assertAliasCredentialCompatible(aliasId, params.token);
@NewFuture
NewFuture merged commit b4843a1 into NewFuture:main Aug 8, 2026
8 checks passed
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.

3 participants