From 7ff4c81deeb54d75d8ea621ac4273eb6e9b1715d Mon Sep 17 00:00:00 2001 From: Daniel Compton Date: Mon, 3 Aug 2026 12:50:56 +1200 Subject: [PATCH] Normalize local thread IDs during activation --- src/codex-micro-renderer-bridge.ts | 14 +++++++++++--- test/micro-bridge.test.ts | 8 +++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/codex-micro-renderer-bridge.ts b/src/codex-micro-renderer-bridge.ts index 36c209e..425263d 100644 --- a/src/codex-micro-renderer-bridge.ts +++ b/src/codex-micro-renderer-bridge.ts @@ -42,6 +42,12 @@ export type AgentDispatchPlan = | { kind: "native"; slot: number; threadKey: string } | { kind: "direct"; threadKey: string }; +export function localThreadKeyVariants(threadKey: string): string[] { + return threadKey.startsWith("local:") + ? [threadKey, threadKey.slice("local:".length)] + : [threadKey, `local:${threadKey}`]; +} + export function resolveAgentDispatch( snapshot: MicroSnapshot, requestedSlot: number, @@ -352,8 +358,10 @@ export class CodexMicroRendererBridge { } private async ensureThreadActivated(threadKey: string): Promise { + const threadKeyVariants = localThreadKeyVariants(threadKey); const result = await this.evaluate<"active" | "opened" | "missing" | "failed">(`(async () => { const threadKey = ${JSON.stringify(threadKey)}; + const threadKeyVariants = ${JSON.stringify(threadKeyVariants)}; const activeThreadKey = () => document.querySelector('[data-above-composer-conversation-id]') ?.getAttribute('data-above-composer-conversation-id') ?? document.querySelector('[data-app-action-sidebar-thread-id][data-app-action-sidebar-thread-active="true"]') @@ -364,14 +372,14 @@ export class CodexMicroRendererBridge { const waitForActive = async (duration) => { const deadline = Date.now() + duration; while (Date.now() < deadline) { - if (activeThreadKey() === threadKey) return true; + if (threadKeyVariants.includes(activeThreadKey())) return true; await new Promise((resolve) => setTimeout(resolve, 25)); } - return activeThreadKey() === threadKey; + return threadKeyVariants.includes(activeThreadKey()); }; if (await waitForActive(250)) return 'active'; const item = [...document.querySelectorAll('[data-app-action-sidebar-thread-id]')] - .find((element) => element.getAttribute('data-app-action-sidebar-thread-id') === threadKey); + .find((element) => threadKeyVariants.includes(element.getAttribute('data-app-action-sidebar-thread-id'))); if (!item) return 'missing'; const selector = 'button, a, [role="button"], [role="link"]'; const clickable = item.matches(selector) ? item : item.querySelector(selector) ?? item.closest(selector) ?? item; diff --git a/test/micro-bridge.test.ts b/test/micro-bridge.test.ts index b272d59..2a086a5 100644 --- a/test/micro-bridge.test.ts +++ b/test/micro-bridge.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; import { - REASONING_ENCODER_KEYS, resolveAgentDispatch, retainEvaluationPromise, selectCodexMainTarget + localThreadKeyVariants, REASONING_ENCODER_KEYS, resolveAgentDispatch, retainEvaluationPromise, selectCodexMainTarget } from "../src/codex-micro-renderer-bridge.js"; import { ADDITIONAL_KEYCAPS, OFFICIAL_KEYCAP_IDS } from "../src/keycaps.js"; import { visualStatusFromMicro } from "../src/status.js"; @@ -77,6 +77,12 @@ test("renderer evaluations retain their awaited promise until CDP has collected assert.match(namespaced, /codex-deck-bridge-a-1/); }); +test("renderer activation accepts local-prefixed and bare forms of the same thread key", () => { + const threadId = "019fc4e4-4ecc-7f20-b7f5-855c11da7b37"; + assert.deepEqual(localThreadKeyVariants(`local:${threadId}`), [`local:${threadId}`, threadId]); + assert.deepEqual(localThreadKeyVariants(threadId), [threadId, `local:${threadId}`]); +}); + test("agent routing follows the stable thread identity when a cross-host slot is stale", () => { const snapshot = { slots: Array.from({ length: 6 }, (_, id) => ({