Merge branch 'dev' into feat/session-native-missions-v2

This commit is contained in:
Pascal André 2026-10-02 10:54:01 +02:00 • committed by GitHub
commit 8d89f7f3a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 4009 additions and 356 deletions

View file

@ -1,7 +1,7 @@
import fs from "fs"
import path from "path"
import type { Logger } from "../logger"
import { hashPassword, type PasswordHashRecord, verifyPassword } from "./password-hash"
import { hashPassword, hashPasswordSync, type PasswordHashRecord, verifyPassword } from "./password-hash"
export interface AuthFile {
version: 1
@ -20,6 +20,7 @@ export class AuthStore {
private cachedFile: AuthFile | null = null
private overrideAuth: AuthFile | null = null
private bootstrapUsername: string | null = null
private passwordChanges: Promise<void> = Promise.resolve()
constructor(private readonly authFilePath: string, private readonly logger: Logger) {}
@ -65,7 +66,7 @@ export class AuthStore {
const runtime: AuthFile = {
version: 1,
username: params.username,
password: hashPassword(password),
password: hashPasswordSync(password),
userProvided: true,
updatedAt: now,
}
@ -97,7 +98,7 @@ export class AuthStore {
)
}
validateCredentials(username: string, password: string): boolean {
async validateCredentials(username: string, password: string): Promise<boolean> {
const auth = this.load()
if (!auth) {
return false
@ -107,10 +108,21 @@ export class AuthStore {
return false
}
return verifyPassword(password, auth.password)
const valid = await verifyPassword(password, auth.password)
// A password change can commit while scrypt is off-thread. Do not grant a
// session from superseded credentials or retry the login automatically.
return valid && this.load() === auth
}
setPassword(params: { password: string; markUserProvided: boolean }): AuthStatus {
setPassword(params: { password: string; markUserProvided: boolean }): Promise<AuthStatus> {
// Keep admission order and bootstrap state atomic across asynchronous hashing.
// A failed change must not poison subsequent requests.
const change = this.passwordChanges.then(() => this.changePassword(params))
this.passwordChanges = change.then(() => {}, () => {})
return change
}
private async changePassword(params: { password: string; markUserProvided: boolean }): Promise<AuthStatus> {
if (this.overrideAuth) {
throw new Error(
"Server password is provided via CLI/env and cannot be changed while running. Restart without --password / CODENOMAD_SERVER_PASSWORD to use auth.json.",
@ -127,7 +139,7 @@ export class AuthStore {
const created: AuthFile = {
version: 1,
username: this.bootstrapUsername,
password: hashPassword(params.password),
password: await hashPassword(params.password),
userProvided: params.markUserProvided,
updatedAt: new Date().toISOString(),
}
@ -139,7 +151,7 @@ export class AuthStore {
const next: AuthFile = {
...current,
password: hashPassword(params.password),
password: await hashPassword(params.password),
userProvided: params.markUserProvided,
updatedAt: new Date().toISOString(),
}

View file

@ -71,7 +71,7 @@ export class AuthManager {
return this.tokenManager.consume(token)
}
validateLogin(username: string, password: string): boolean {
async validateLogin(username: string, password: string): Promise<boolean> {
if (!this.authEnabled) {
return true
}

View file

@ -0,0 +1,32 @@
import assert from "node:assert/strict"
import crypto from "node:crypto"
import { it } from "node:test"
import { hashPassword, verifyPassword, type PasswordHashRecord } from "./password-hash"
it("produces the existing v1 scrypt format and unchanged default parameters", async () => {
const password = "test-password"
const record = await hashPassword(password)
assert.equal(record.algorithm, "scrypt")
assert.equal(Buffer.from(record.saltBase64, "base64").length, 16)
assert.equal(record.keyLength, 64)
assert.deepEqual(record.params, { N: 16384, r: 8, p: 1, maxmem: 32 * 1024 * 1024 })
const expected = crypto.scryptSync(password, Buffer.from(record.saltBase64, "base64"), record.keyLength, record.params)
assert.equal(record.hashBase64, expected.toString("base64"))
assert.equal(await verifyPassword(password, record), true)
assert.equal(await verifyPassword("wrong-password", record), false)
})
it("verifies existing records using their saved salt, key length and custom scrypt parameters", async () => {
const salt = Buffer.from("custom-salt")
const params = { N: 1024, r: 8, p: 2, maxmem: 16 * 1024 * 1024 }
const record: PasswordHashRecord = {
algorithm: "scrypt", saltBase64: salt.toString("base64"),
hashBase64: crypto.scryptSync("existing-password", salt, 32, params).toString("base64"),
keyLength: 32, params,
}
assert.equal(await verifyPassword("existing-password", record), true)
assert.equal(await verifyPassword("wrong-password", record), false)
assert.equal(await verifyPassword("existing-password", { ...record, hashBase64: Buffer.from("short").toString("base64") }), false)
assert.equal(await verifyPassword("existing-password", { ...record, algorithm: "unsupported" as "scrypt" }), false)
await assert.rejects(verifyPassword("existing-password", { ...record, params: { ...params, N: 3 } }), /scrypt params/)
})

View file

@ -20,11 +20,24 @@ const DEFAULT_SCRYPT_PARAMS = {
maxmem: 32 * 1024 * 1024,
}
export function hashPassword(password: string): PasswordHashRecord {
// Startup-only: AuthManager initializes its CLI/env override before serving HTTP.
export function hashPasswordSync(password: string): PasswordHashRecord {
const salt = crypto.randomBytes(16)
const params = DEFAULT_SCRYPT_PARAMS
const keyLength = 64
const derived = crypto.scryptSync(password, salt, keyLength, params)
return buildRecord(salt, derived, keyLength, params)
}
export async function hashPassword(password: string): Promise<PasswordHashRecord> {
const salt = crypto.randomBytes(16)
const params = DEFAULT_SCRYPT_PARAMS
const keyLength = 64
const derived = await derivePassword(password, salt, keyLength, params)
return buildRecord(salt, derived, keyLength, params)
}
function buildRecord(salt: Buffer, derived: Buffer, keyLength: number, params: PasswordHashRecord["params"]): PasswordHashRecord {
return {
algorithm: "scrypt",
saltBase64: salt.toString("base64"),
@ -34,16 +47,25 @@ export function hashPassword(password: string): PasswordHashRecord {
}
}
export function verifyPassword(password: string, record: PasswordHashRecord): boolean {
export async function verifyPassword(password: string, record: PasswordHashRecord): Promise<boolean> {
if (record.algorithm !== "scrypt") {
return false
}
const salt = Buffer.from(record.saltBase64, "base64")
const expected = Buffer.from(record.hashBase64, "base64")
const derived = crypto.scryptSync(password, salt, record.keyLength, record.params)
const derived = await derivePassword(password, salt, record.keyLength, record.params)
if (expected.length !== derived.length) {
return false
}
return crypto.timingSafeEqual(expected, Buffer.from(derived))
}
function derivePassword(password: string, salt: Buffer, keyLength: number, params: PasswordHashRecord["params"]): Promise<Buffer> {
return new Promise((resolve, reject) => {
crypto.scrypt(password, salt, keyLength, params, (error, derived) => {
if (error) reject(error)
else resolve(derived)
})
})
}

View file

@ -27,6 +27,7 @@ export async function forwardRuntimeRequest(args: {
args.reply.raw.once("close", disconnect)
if (args.reply.raw.destroyed || args.request.raw.aborted) controller.abort()
try {
controller.signal.throwIfAborted()
const headers = new Headers()
for (const [key, value] of Object.entries(args.headers)) {
if (typeof value === "string") headers.set(key, value)

View file

@ -42,13 +42,25 @@ export async function readSessionOutline(db: DatabaseSync, scope: HistoryScope,
FROM session_message WHERE ${where} AND seq>? AND seq<=? ORDER BY seq LIMIT 512`)
const entries: OutlineEntry[] = [], checkpoints: Array<OutlineCheckpoint & { changed: boolean }> = []
let examined = 0
// Row counts do not bound time: one assistant can contain megabytes of tool
// output. Cooperate during both checkpoint reads and JSON projection. A
// single SQLite step is still synchronous, so this is not a hard deadline.
let turnStarted = performance.now()
do {
signal.throwIfAborted()
const previous = known.find(item => item.after === after)
// Grow the last range to 512 rows before creating another checkpoint.
// Otherwise one append per refresh eventually exhausts the manifest budget.
const end = Math.min(previous && previous !== known.at(-1) ? previous.through : through, through)
const rows = headers.all(...params, after, end)
const rows = []
for (const row of headers.iterate(...params, after, end)) {
signal.throwIfAborted()
rows.push(row)
if (rows.length % 128 === 0 || performance.now() - turnStarted >= 8) {
await yieldTurn(undefined, { signal })
turnStarted = performance.now()
}
}
// If a cached range gained >512 rows, split and rebuild it instead of
// declaring the unseen suffix unchanged.
const last = rows.at(-1)
@ -59,17 +71,22 @@ export async function readSessionOutline(db: DatabaseSync, scope: HistoryScope,
|| chunkEnd >= maximum
if (changed) {
for (const row of project.iterate(...params, after, chunkEnd)) {
signal.throwIfAborted()
const tools = Number(row.tools)
entries.push({ id: String(row.id), seq: Number(row.seq), type: row.type as OutlineEntry["type"],
tools, reasoning: Number(row.reasoning),
...(tools ? { toolName: boundedToolName(row.tool_name) } : {}) })
if (entries.length % 128 === 0) await yieldTurn(undefined, { signal })
if (entries.length % 128 === 0 || performance.now() - turnStarted >= 8) {
await yieldTurn(undefined, { signal })
turnStarted = performance.now()
}
}
}
checkpoints.push({ after, through: chunkEnd, digest, changed })
after = chunkEnd
examined += rows.length
await yieldTurn(undefined, { signal })
turnStarted = performance.now()
// Old ranges shrink after deletions. Reserve a whole block before reading
// another; the page budget is not necessarily a multiple of 512 anymore.
} while (after < through && examined <= 16384 - 512 && checkpoints.length < 512)

View file

@ -0,0 +1,37 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { DatabaseSync } from "node:sqlite"
import { readSessionOutline } from "./outline-index"
for (const phase of ["headers", "projection"] as const) test(`outline ${phase} admit pending cancellation before consuming an expensive checkpoint`, async (t) => {
const db = new DatabaseSync(":memory:")
let reads = 0
let clock = 0
const controller = new AbortController()
let pending: ReturnType<typeof setImmediate> | undefined
const freeReads = phase === "projection" ? 512 : 0
// Charge a deterministic cost to SQLite payload access instead of asserting
// wall-clock timing on CI. The real SQL/iterator/transaction still execute.
db.function("payload", (data) => {
reads++
if (reads > freeReads) {
clock += 2
pending ??= setImmediate(() => controller.abort())
}
return data
})
t.mock.method(performance, "now", () => clock)
db.exec(`CREATE TABLE session_v2(id TEXT,directory TEXT,project_id TEXT,workspace_id TEXT,revert TEXT);
CREATE TABLE stored_messages(id TEXT PRIMARY KEY,session_id TEXT,type TEXT,seq INTEGER,data TEXT,time_updated INTEGER);
CREATE UNIQUE INDEX stored_session_seq ON stored_messages(session_id,seq);
CREATE VIEW session_message AS SELECT id,session_id,type,seq,payload(data) AS data,time_updated FROM stored_messages;
INSERT INTO session_v2 VALUES ('s','/repo','p',NULL,NULL);`)
const insert = db.prepare("INSERT INTO stored_messages VALUES (?,'s','assistant',?,?,0)")
for (let i = 0; i < 512; i++) insert.run(`m${i}`, i, '{"content":[{"type":"tool","name":"read"}]}')
try {
await assert.rejects(readSessionOutline(db, { directory: "/repo", projectID: "p", sessionID: "s" },
undefined, controller.signal), /abort/i)
assert(reads - freeReads < 32, `cancelled scan accessed ${reads - freeReads} expensive payloads before giving the event loop a turn`)
assert.equal(db.isTransaction, false, "cancellation releases its read snapshot")
} finally { if (pending) clearImmediate(pending); db.close() }
})

View file

@ -0,0 +1,236 @@
import assert from "node:assert/strict"
import http from "node:http"
import { test } from "node:test"
import Fastify from "fastify"
import replyFrom from "@fastify/reply-from"
import { OpenCode, type OpenCodeClient } from "@opencode/client"
import type { Logger } from "../../logger"
import { createRuntimeFetch } from "../../opencode/compatibility/transport"
import { WorktreeDeletionFence } from "../../workspaces/worktree-session-evacuation"
import { registerInstanceProxyRoutes, type InstanceProxyWorkspaceManager } from "../http-server"
function deferred<T>() {
let resolve!: (value: T) => void
const promise = new Promise<T>((done) => { resolve = done })
return { promise, resolve }
}
function send(url: string, method = "POST") {
let request!: http.ClientRequest
const result = new Promise<number>((resolve, reject) => {
request = http.request(url, { method, agent: false, headers: { "content-type": "application/json" } }, response => {
response.resume()
response.once("end", () => resolve(response.statusCode!))
})
request.once("error", reject)
request.end(method === "POST" ? "{}" : undefined)
})
void result.catch(() => undefined)
return { request, result }
}
type Phase = "connection" | "profile" | "session" | "ownership" | "snapshot" | "environment" | "forward"
async function harness(phase: Phase, nativePreflight = false) {
const blocked = deferred<void>(), started = deferred<void>(), closed = deferred<void>()
const cleanupFinished = deferred<void>()
const nativeClosed = deferred<void>()
let signal: AbortSignal | undefined
const calls = { session: 0, snapshot: 0, environment: 0, forward: 0, complete: 0, invalidation: 0 }
const hold = async (at: Phase, requestSignal?: AbortSignal) => {
if (phase !== at) return
signal = requestSignal
started.resolve()
// Deliberately ignore cancellation in the fake: the observer must also
// retire locally and fence continuations from non-cancellable shared work.
await blocked.promise
}
const upstream = Fastify()
upstream.all("/*", async (request, reply) => {
if (nativePreflight && request.method === "GET" && request.url === "/api/session/held") {
reply.raw.once("close", () => nativeClosed.resolve())
await hold("session")
return { id: "held", location: { directory: "/repo" } }
}
calls.forward++
await hold("forward")
calls.complete++
return { path: request.url }
})
await upstream.listen({ host: "127.0.0.1", port: 0 })
const upstreamAddress = upstream.server.address()
assert.ok(upstreamAddress && typeof upstreamAddress === "object")
const endpoint = `http://127.0.0.1:${upstreamAddress.port}`
const native = OpenCode.make({ baseUrl: endpoint, fetch: createRuntimeFetch({ url: endpoint }) })
const client = {
session: {
get: async ({ sessionID }: { sessionID: string }, options?: { signal?: AbortSignal }) => {
calls.session++
if (nativePreflight && sessionID === "held") return native.session.get({ sessionID }, options)
if (sessionID !== "witness") await hold("session", options?.signal)
return { id: sessionID, location: { directory: "/repo" } }
},
environment: async (_input: unknown, options?: { signal?: AbortSignal }) => {
calls.environment++
await hold("environment", options?.signal)
},
instructions: { entry: { remove: async () => {}, put: async () => {} } },
},
} as unknown as OpenCodeClient
const connection = {
endpoint: { url: endpoint }, client,
fetch: createRuntimeFetch({ url: endpoint }),
assertCurrent() {}, invalidate() { calls.invalidation++ },
profile: async (requestSignal?: AbortSignal) => { await hold("profile", requestSignal); return "modern" as const },
}
const manager = {
get: () => ({ id: "workspace", path: "/repo" }),
getSharedServiceConnection: async () => { await hold("connection"); return connection },
getSharedServiceEndpoint: async () => connection.endpoint,
getSharedServiceClient: async () => client,
getSharedServiceFetch: async () => connection.fetch,
getInstanceAuthorizationHeader: () => undefined,
getServiceDirectory: () => "/repo",
getServiceDirectoryForPath: async () => "/repo",
getServicePathForPath: async () => "/repo",
getWorktreeIdentityForPath: async () => "repo",
getSessionEnvironment: async (_id: string, requestSignal?: AbortSignal) => {
calls.snapshot++
await hold("snapshot", requestSignal)
return {}
},
ownsDirectory: async () => true,
ownsLocation: async (_id: string, _location: unknown, _client?: unknown, requestSignal?: AbortSignal) => {
await hold("ownership", requestSignal)
return true
},
ownsPath: async () => true,
} as unknown as InstanceProxyWorkspaceManager
const app = Fastify()
const captured: Array<{ request: http.IncomingMessage; response: http.ServerResponse; abortedListeners: number; closeListeners: number }> = []
app.addHook("onRequest", async (request, reply) => {
captured.push({ request: request.raw, response: reply.raw,
abortedListeners: request.raw.listenerCount("aborted"), closeListeners: reply.raw.listenerCount("close") })
if (!request.url.endsWith("/witness")) reply.raw.once("close", () => {
closed.resolve()
// The admission race settles and disposes in the following microtasks.
setImmediate(() => cleanupFinished.resolve())
})
})
await app.register(replyFrom)
const fence = new WorktreeDeletionFence()
registerInstanceProxyRoutes(app, {
workspaceManager: manager, worktreeDeletionFence: fence,
logger: { debug() {}, error() {}, isLevelEnabled() { return false } } as unknown as Logger,
})
await app.listen({ host: "127.0.0.1", port: 0 })
const address = app.server.address()
assert.ok(address && typeof address === "object")
const base = `http://127.0.0.1:${address.port}/workspaces/workspace/instance/api/session`
return {
base, calls, blocked, started, closed, cleanupFinished, captured, fence, nativeClosed,
signal: () => signal,
async dispose() { blocked.resolve(); await app.close(); await upstream.close() },
}
}
for (const action of ["prompt", "compact"] as const) {
test(`disconnect cancels actual Promise SDK HTTP preflight for ${action}`, { timeout: 10_000 }, async () => {
const h = await harness("session", true)
try {
const pending = send(`${h.base}/held/${action}`)
await h.started.promise
pending.request.destroy()
await h.nativeClosed.promise
assert.equal(h.calls.forward, 0)
assert.equal(h.calls.environment, 0)
assert.equal(h.calls.invalidation, 0)
} finally { await h.dispose() }
})
test(`disconnect cancels held ${action} session preflight, without blocking another session`, { timeout: 10_000 }, async t => {
const h = await harness("session")
try {
const pending = send(`${h.base}/held/${action}`)
await h.started.promise
const witnessStarted = performance.now()
const witness = await send(`${h.base}/witness`, "GET").result
t.diagnostic(`another session responded while the preflight was held: ${Math.round(performance.now() - witnessStarted)} ms`)
assert.equal(witness, 200) // causal independence, not a machine-speed threshold
pending.request.destroy()
await h.closed.promise
await h.cleanupFinished.promise
assert.equal(h.signal()?.aborted, true)
h.blocked.resolve()
await new Promise<void>(resolve => setImmediate(resolve))
assert.equal(h.calls.forward, 1, "only the independent witness reached native forwarding")
assert.equal(h.calls.environment, 0)
assert.equal(h.calls.invalidation, 0)
} finally { await h.dispose() }
})
}
for (const phase of ["connection", "profile", "ownership", "snapshot", "environment"] as const) {
test(`disconnect retires a held ${phase} boundary and fences late admission`, { timeout: 10_000 }, async () => {
const h = await harness(phase)
try {
const pending = send(`${h.base}/held/prompt`)
await h.started.promise
pending.request.destroy()
await h.closed.promise
await h.cleanupFinished.promise
if (phase !== "connection") assert.equal(h.signal()?.aborted, true)
const callsAtDisconnect = { ...h.calls }
h.blocked.resolve()
await new Promise<void>(resolve => setImmediate(resolve))
assert.deepEqual(h.calls, callsAtDisconnect, "late shared work must not start any subsequent calls")
assert.equal(h.calls.forward, 0)
assert.equal(h.calls.invalidation, 0)
// A preflight-abandoned mutation cannot keep the deletion fence reserved.
await h.fence.run("repo", ["repo"], async () => {})
for (const entry of h.captured) {
assert.equal(entry.request.listenerCount("aborted"), entry.abortedListeners)
assert.equal(entry.response.listenerCount("close"), entry.closeListeners)
}
} finally { await h.dispose() }
})
}
for (const action of ["prompt", "compact"] as const) {
test(`disconnect after native ${action} admission does not interrupt, replay or invalidate execution`, { timeout: 10_000 }, async () => {
const h = await harness("forward")
try {
const pending = send(`${h.base}/held/${action}`)
await h.started.promise
pending.request.destroy()
await h.closed.promise
await h.cleanupFinished.promise
assert.equal(h.calls.forward, 1)
h.blocked.resolve()
await new Promise<void>(resolve => setImmediate(resolve))
assert.equal(h.calls.complete, 1, "the admitted upstream operation can finish after its HTTP observer leaves")
assert.equal(h.calls.forward, 1)
assert.equal(h.calls.invalidation, 0)
} finally { await h.dispose() }
})
}
test("normal POST body close preserves admission; success and denial dispose admission listeners", { timeout: 10_000 }, async () => {
const h = await harness("session")
try {
const pending = send(`${h.base}/held/compact`)
await h.started.promise
assert.equal(h.captured[0].request.complete, true)
assert.equal(h.signal()?.aborted, false)
h.blocked.resolve()
assert.equal(await pending.result, 200)
assert.equal(await send(`${h.base}/held/not-allowed`).result, 403)
await new Promise<void>(resolve => setImmediate(resolve))
for (const entry of h.captured) {
assert.equal(entry.request.listenerCount("aborted"), entry.abortedListeners)
// Node's response pipeline retains its own finished-stream callbacks;
// check our admission/forwarding listeners, not those library internals.
assert.equal(entry.response.listeners("close").some(listener => ["close", "disconnect"].includes(listener.name)), false)
}
} finally { await h.dispose() }
})

View file

@ -49,6 +49,7 @@ import type { SideCarManager } from "../sidecars/manager"
import type { PreviewManager } from "../previews/manager"
import { buildPreviewRuntimeBridge, rewritePreviewImportMap, rewritePreviewJavaScriptImports } from "../previews/runtime-bridge"
import { forwardRuntimeRequest } from "../opencode/compatibility/proxy"
import { requestAdmission } from "./request-admission"
import { LOCATION_CONTEXT_HEADER, locationRequestOptions, readLocationContext } from "../opencode/compatibility/location"
import { decodeSessionListScope, prepareLocationImport, readRequestLocations, type SessionListScope } from "../opencode/compatibility/proxy-locations"
import type { RemoteProxySessionManager } from "./remote-proxy"
@ -433,7 +434,7 @@ export interface InstanceProxyWorkspaceManager {
getServicePathForPath?(id: string, candidate: string): Promise<string | undefined>
getSharedServiceClient(): Promise<OpenCodeClient>
getSessionEnvironment(id: string, signal?: AbortSignal): Promise<Record<string, string>>
ownsLocation(id: string, location: LocationRef, client?: OpenCodeClient): ReturnType<WorkspaceManager["ownsLocation"]>
ownsLocation(id: string, location: LocationRef, client?: OpenCodeClient, signal?: AbortSignal): ReturnType<WorkspaceManager["ownsLocation"]>
ownsDirectory(id: string, directory: string): Promise<boolean>
ownsPath(id: string, candidate: string): Promise<boolean>
}
@ -649,15 +650,31 @@ export function registerInstanceProxyRoutes(app: FastifyInstance, deps: Instance
})
}
async function proxyWorkspaceRequest(args: {
interface InstanceProxyRequestArgs {
request: FastifyRequest
reply: FastifyReply
workspaceManager: InstanceProxyWorkspaceManager
worktreeDeletionFence: WorktreeDeletionFence
logger: Logger
pathSuffix?: string
}) {
}
async function proxyWorkspaceRequest(args: InstanceProxyRequestArgs) {
const admission = requestAdmission(args.request, args.reply)
try {
admission.signal.throwIfAborted()
return await proxyWorkspaceAdmission(args, admission)
} catch (error) {
if (!admission.signal.aborted) throw error
} finally {
admission.dispose()
}
}
async function proxyWorkspaceAdmission(args: InstanceProxyRequestArgs, admission: ReturnType<typeof requestAdmission>) {
const { request, reply, workspaceManager, logger, worktreeDeletionFence } = args
const { signal } = admission
const wait = admission.wait
const workspaceId = (request.params as { id: string }).id
const workspace = workspaceManager.get(workspaceId)
@ -682,15 +699,18 @@ async function proxyWorkspaceRequest(args: {
return
}
const connection = await workspaceManager.getSharedServiceConnection?.(workspaceId)
const endpoint = connection?.endpoint ?? await workspaceManager.getSharedServiceEndpoint(workspaceId)
const clientForRequest = () => connection ? Promise.resolve(connection.client) : workspaceManager.getSharedServiceClient()
const connection = await wait(Promise.resolve(workspaceManager.getSharedServiceConnection?.(workspaceId)))
const endpoint = connection?.endpoint ?? await wait(workspaceManager.getSharedServiceEndpoint(workspaceId))
const clientForRequest = () => {
signal.throwIfAborted()
return wait(connection ? Promise.resolve(connection.client) : workspaceManager.getSharedServiceClient())
}
if (!endpoint) {
reply.code(502).send({ error: "OpenCode service is not ready" })
return
}
await connection?.profile()
await wait(Promise.resolve(connection?.profile(signal)))
let locationContext: LocationRef | undefined
try { locationContext = readLocationContext(request.headers[LOCATION_CONTEXT_HEADER]) }
catch { return reply.code(400).send({ error: "Invalid location context" }) }
@ -707,34 +727,36 @@ async function proxyWorkspaceRequest(args: {
return
}
const client = await clientForRequest()
const active = await client.session.active()
const active = await wait(client.session.active({ signal }))
const entries = await Promise.all(Object.entries(active).map(async ([sessionId, status]) => {
try {
const session = await client.session.get({ sessionID: sessionId })
return await workspaceManager.ownsLocation(workspaceId, session.location, client) ? [sessionId, status] as const : null
const session = await wait(client.session.get({ sessionID: sessionId }, { signal }))
return await wait(workspaceManager.ownsLocation(workspaceId, session.location, client, signal)) ? [sessionId, status] as const : null
} catch {
return null
}
}))
signal.throwIfAborted()
reply.send({ data: Object.fromEntries(entries.filter((entry): entry is NonNullable<typeof entry> => entry !== null)) })
return
}
if (pathname.replace(/\/+$/, "") === "/api/project") {
const projects = await (await clientForRequest()).project.list()
const projects = await wait((await clientForRequest()).project.list({ signal }))
const ownedProjects = await Promise.all(projects.map(async (project) => {
if (!await workspaceManager.ownsDirectory(workspaceId, project.canonical)) return null
if (!await wait(workspaceManager.ownsDirectory(workspaceId, project.canonical))) return null
const sandboxes = (await Promise.all(project.sandboxes.map(async (directory) => (
await workspaceManager.ownsDirectory(workspaceId, directory) ? directory : null
await wait(workspaceManager.ownsDirectory(workspaceId, directory)) ? directory : null
)))).filter((directory): directory is string => directory !== null)
return { ...project, sandboxes }
}))
signal.throwIfAborted()
reply.send(ownedProjects.filter((project): project is NonNullable<typeof project> => project !== null))
return
}
const sessionListHasScope = request.method === "GET"
&& pathname.replace(/\/+$/, "") === "/api/session"
&& (targetUrl.searchParams.has("cursor") || targetUrl.searchParams.has("project"))
const sessionListScope = await authorizeSessionList(targetUrl, request.method, workspaceManager, workspaceId, connection?.client)
const sessionListScope = await wait(authorizeSessionList(targetUrl, request.method, workspaceManager, workspaceId, connection?.client, signal))
if (sessionListScope !== "allowed") {
reply.code(sessionListScope === "invalid" ? 400 : 403).send({ error: "Session list does not belong to workspace" })
return
@ -796,15 +818,15 @@ async function proxyWorkspaceRequest(args: {
return
}
if (requestLocations.invalid
|| !(await allDirectoriesOwned(workspaceManager, workspaceId, requestLocations.directories))
|| !(await allLocationsOwned(workspaceManager, workspaceId, requestLocations.locations, connection?.client))) {
|| !(await wait(allDirectoriesOwned(workspaceManager, workspaceId, requestLocations.directories)))
|| !(await wait(allLocationsOwned(workspaceManager, workspaceId, requestLocations.locations, connection?.client, signal)))) {
reply.code(requestLocations.invalid ? 400 : 403).send({ error: "Location does not belong to workspace" })
return
}
const translatedDirectories = new Map<string, string>()
for (const directory of new Set(requestLocations.directories)) {
const translated = workspaceManager.getServiceDirectoryForPath
? await workspaceManager.getServiceDirectoryForPath(workspaceId, directory)
? await wait(workspaceManager.getServiceDirectoryForPath(workspaceId, directory))
: directory
if (!translated) {
reply.code(403).send({ error: "Location does not belong to workspace" })
@ -823,12 +845,12 @@ async function proxyWorkspaceRequest(args: {
return
}
if (fileListPath.candidate) {
if (!(await workspaceManager.ownsPath(workspaceId, fileListPath.candidate))) {
if (!(await wait(workspaceManager.ownsPath(workspaceId, fileListPath.candidate)))) {
reply.code(403).send({ error: "Filesystem path does not belong to workspace" })
return
}
const translated = workspaceManager.getServicePathForPath
? await workspaceManager.getServicePathForPath(workspaceId, fileListPath.candidate)
? await wait(workspaceManager.getServicePathForPath(workspaceId, fileListPath.candidate))
: fileListPath.candidate
if (!translated) {
reply.code(403).send({ error: "Filesystem path does not belong to workspace" })
@ -839,7 +861,7 @@ async function proxyWorkspaceRequest(args: {
const mutationIdentities = new Set<string>()
if (request.method !== "GET" && request.method !== "HEAD") {
for (const directory of requestLocations.directories) {
const identity = await workspaceManager.getWorktreeIdentityForPath(workspaceId, directory)
const identity = await wait(workspaceManager.getWorktreeIdentityForPath(workspaceId, directory))
if (!identity) {
reply.code(403).send({ error: "Location does not belong to workspace" })
return
@ -848,14 +870,14 @@ async function proxyWorkspaceRequest(args: {
}
}
const serviceBody = replaceRequestDirectories(targetUrl, imported.body, translatedDirectories, pathname, request.method)
if (promptFiles.invalid || !(await allPathsOwned(workspaceManager, workspaceId, promptFiles.paths))) {
if (promptFiles.invalid || !(await wait(allPathsOwned(workspaceManager, workspaceId, promptFiles.paths)))) {
reply.code(promptFiles.invalid ? 400 : 403).send({ error: "Prompt file does not belong to workspace" })
return
}
const translatedPromptPaths = new Map<string, string>()
for (const candidate of new Set(promptFiles.paths)) {
const translated = workspaceManager.getServicePathForPath
? await workspaceManager.getServicePathForPath(workspaceId, candidate)
? await wait(workspaceManager.getServicePathForPath(workspaceId, candidate))
: candidate
if (!translated) {
reply.code(403).send({ error: "Prompt file does not belong to workspace" })
@ -870,10 +892,10 @@ async function proxyWorkspaceRequest(args: {
directory: requestedDirectory ? translatedDirectories.get(requestedDirectory) ?? serviceDirectory : serviceDirectory,
workspaceID: locationContext?.workspaceID ?? explicitLocations.find(location => location.directory === requestedDirectory)?.workspaceID,
}
const runtimeLocationOptions = locationRequestOptions(runtimeLocation)
const runtimeLocationOptions = { ...locationRequestOptions(runtimeLocation), signal }
if (pathname.replace(/\/+$/, "") === "/api/pty" && request.method === "GET") {
const result = await (await clientForRequest()).pty.list({ location: { directory: runtimeLocation.directory } }, runtimeLocationOptions)
const ownership = await Promise.all(result.data.map((pty) => workspaceManager.ownsDirectory(workspaceId, pty.cwd)))
const result = await wait((await clientForRequest()).pty.list({ location: { directory: runtimeLocation.directory } }, runtimeLocationOptions))
const ownership = await wait(Promise.all(result.data.map((pty) => workspaceManager.ownsDirectory(workspaceId, pty.cwd))))
reply.send({ ...result, data: result.data.filter((_, index) => ownership[index]) })
return
}
@ -881,8 +903,8 @@ async function proxyWorkspaceRequest(args: {
const ptyId = getPtyRouteId(pathname)
if (ptyId) {
try {
const pty = await (await clientForRequest()).pty.get({ ptyID: ptyId, location: { directory: runtimeLocation.directory } }, runtimeLocationOptions)
if (!(await workspaceManager.ownsDirectory(workspaceId, pty.data.cwd))) {
const pty = await wait((await clientForRequest()).pty.get({ ptyID: ptyId, location: { directory: runtimeLocation.directory } }, runtimeLocationOptions))
if (!(await wait(workspaceManager.ownsDirectory(workspaceId, pty.data.cwd)))) {
reply.code(403).send({ error: "PTY does not belong to workspace" })
return
}
@ -896,8 +918,8 @@ async function proxyWorkspaceRequest(args: {
}
if (pathname.replace(/\/+$/, "") === "/api/shell" && request.method === "GET") {
const result = await (await clientForRequest()).shell.list({ location: { directory: runtimeLocation.directory } }, runtimeLocationOptions)
const ownership = await Promise.all(result.data.map((shell) => workspaceManager.ownsDirectory(workspaceId, shell.cwd)))
const result = await wait((await clientForRequest()).shell.list({ location: { directory: runtimeLocation.directory } }, runtimeLocationOptions))
const ownership = await wait(Promise.all(result.data.map((shell) => workspaceManager.ownsDirectory(workspaceId, shell.cwd))))
reply.send({ ...result, data: result.data.filter((_, index) => ownership[index]) })
return
}
@ -905,8 +927,8 @@ async function proxyWorkspaceRequest(args: {
const shellId = getShellRouteId(pathname)
if (shellId) {
try {
const shell = await (await clientForRequest()).shell.get({ id: shellId, location: { directory: runtimeLocation.directory } }, runtimeLocationOptions)
if (!(await workspaceManager.ownsDirectory(workspaceId, shell.data.cwd))) {
const shell = await wait((await clientForRequest()).shell.get({ id: shellId, location: { directory: runtimeLocation.directory } }, runtimeLocationOptions))
if (!(await wait(workspaceManager.ownsDirectory(workspaceId, shell.data.cwd)))) {
reply.code(403).send({ error: "Shell does not belong to workspace" })
return
}
@ -923,7 +945,7 @@ async function proxyWorkspaceRequest(args: {
if (sessionId && !isGlobalFormAction(pathname, request.method)) {
let session
try {
session = await (await clientForRequest()).session.get({ sessionID: sessionId })
session = await wait((await clientForRequest()).session.get({ sessionID: sessionId }, { signal }))
} catch (error) {
if (isInvalidRequestError(error)) {
reply.code(400).send({ error: "Invalid session ID" })
@ -937,12 +959,12 @@ async function proxyWorkspaceRequest(args: {
}
throw error
}
if (!(await workspaceManager.ownsLocation(workspaceId, session.location, await clientForRequest()))) {
if (!(await wait(workspaceManager.ownsLocation(workspaceId, session.location, await clientForRequest(), signal)))) {
reply.code(403).send({ error: "Session does not belong to workspace" })
return
}
if (request.method !== "GET" && request.method !== "HEAD") {
const sessionWorktree = await workspaceManager.getWorktreeIdentityForPath(workspaceId, session.location.directory)
const sessionWorktree = await wait(workspaceManager.getWorktreeIdentityForPath(workspaceId, session.location.directory))
if (!sessionWorktree) {
reply.code(403).send({ error: "Session does not belong to workspace" })
return
@ -953,6 +975,7 @@ async function proxyWorkspaceRequest(args: {
const body = applyDefaultWorkspaceLocation(targetUrl, promptBody, request.method, serviceDirectory, requestLocations.directories.length > 0 || sessionListHasScope, Boolean(sessionId) && !isGlobalFormAction(pathname, request.method))
const instanceAuthHeader = workspaceManager.getInstanceAuthorizationHeader(workspaceId)
signal.throwIfAborted()
const releaseMutation = request.method === "GET" || request.method === "HEAD"
? undefined
: worktreeDeletionFence.enter([...mutationIdentities])
@ -965,53 +988,49 @@ async function proxyWorkspaceRequest(args: {
try {
connection?.assertCurrent()
if (request.method === "POST" && /^\/api\/session\/[^/]+\/(?:prompt|command|shell)\/?$/.test(pathname)) {
const disconnected = new AbortController()
const onDisconnect = () => disconnected.abort()
reply.raw.once("close", onDisconnect)
try {
try {
const signal = AbortSignal.any([disconnected.signal, AbortSignal.timeout(15_000)])
if (reply.raw.destroyed || request.raw.aborted) disconnected.abort()
signal.throwIfAborted()
const variables = await workspaceManager.getSessionEnvironment(workspaceId, signal)
signal.throwIfAborted()
connection?.assertCurrent()
await (await clientForRequest()).session.environment({ sessionID: sessionId!, variables }, { signal })
signal.throwIfAborted()
connection?.assertCurrent()
} catch {
// Never log the SDK error: it can contain the complete environment body.
releaseMutation?.()
logger.error({ workspaceId, sessionId }, "Failed to apply profile environment")
return reply.code(502).send({ error: SESSION_ENVIRONMENT_FAILED_ERROR_CODE })
}
if (!pathname.replace(/\/$/, "").endsWith("/shell")) {
try {
connection?.assertCurrent()
await syncSessionGitContext(await clientForRequest(), sessionId!, disconnected.signal)
} catch {
// Advisory context must not turn Git recovery into another send blocker.
// Do not log SDK bodies (they may include unrelated session context).
logger.debug({ workspaceId, sessionId }, "Unable to update Git availability context")
}
}
disconnected.signal.throwIfAborted()
const environmentSignal = AbortSignal.any([signal, AbortSignal.timeout(15_000)])
const variables = await wait(workspaceManager.getSessionEnvironment(workspaceId, environmentSignal))
const client = await clientForRequest()
environmentSignal.throwIfAborted()
connection?.assertCurrent()
} finally {
reply.raw.off("close", onDisconnect)
await wait(client.session.environment({ sessionID: sessionId!, variables }, { signal: environmentSignal }))
environmentSignal.throwIfAborted()
connection?.assertCurrent()
} catch {
signal.throwIfAborted()
// Never log the SDK error: it can contain the complete environment body.
releaseMutation?.()
logger.error({ workspaceId, sessionId }, "Failed to apply profile environment")
return reply.code(502).send({ error: SESSION_ENVIRONMENT_FAILED_ERROR_CODE })
}
if (!pathname.replace(/\/$/, "").endsWith("/shell")) {
try {
connection?.assertCurrent()
await wait(syncSessionGitContext(await clientForRequest(), sessionId!, signal))
} catch {
// Advisory context must not turn Git recovery into another send blocker.
// Do not log SDK bodies (they may include unrelated session context).
logger.debug({ workspaceId, sessionId }, "Unable to update Git availability context")
}
}
signal.throwIfAborted()
connection?.assertCurrent()
}
if (connection || workspaceManager.getSharedServiceFetch) {
const headers = sanitizeInstanceProxyRequestHeaders(request.headers, instanceAuthHeader)
delete headers[LOCATION_CONTEXT_HEADER]
if (locationContext) Object.assign(headers, locationRequestOptions({ ...locationContext, directory: translatedDirectories.get(locationContext.directory)! }, { includeDirectory: true })?.headers)
if (globalFormLocation) headers["x-opencode-directory"] = encodeURIComponent(translatedDirectories.get(globalFormLocation.directory)!)
const runtimeFetch = connection?.fetch ?? await wait(workspaceManager.getSharedServiceFetch!())
signal.throwIfAborted()
return await forwardRuntimeRequest({
request, reply, url: targetUrl.toString(), body, headers,
fetch: connection?.fetch ?? await workspaceManager.getSharedServiceFetch!(), release: releaseMutation,
fetch: runtimeFetch, release: releaseMutation,
invalidate: () => connection ? connection.invalidate() : workspaceManager.invalidateSharedServiceConnection?.(),
})
}
signal.throwIfAborted()
return reply.from(targetUrl.toString(), {
...(body !== request.body ? { body } : {}),
rewriteRequestHeaders: (_originalRequest, headers) => {
@ -1151,9 +1170,9 @@ async function allDirectoriesOwned(manager: InstanceProxyWorkspaceManager, works
return (await Promise.all(directories.map((directory) => manager.ownsDirectory(workspaceId, directory)))).every(Boolean)
}
async function allLocationsOwned(manager: InstanceProxyWorkspaceManager, workspaceId: string, locations: LocationRef[], client?: OpenCodeClient) {
async function allLocationsOwned(manager: InstanceProxyWorkspaceManager, workspaceId: string, locations: LocationRef[], client?: OpenCodeClient, signal?: AbortSignal) {
const unique = new Map(locations.map((location) => [JSON.stringify([location.directory, location.workspaceID]), location]))
return (await Promise.all([...unique.values()].map((location) => manager.ownsLocation(workspaceId, location, client)))).every(Boolean)
return (await Promise.all([...unique.values()].map((location) => manager.ownsLocation(workspaceId, location, client, signal)))).every(Boolean)
}
async function allPathsOwned(manager: InstanceProxyWorkspaceManager, workspaceId: string, paths: string[]) {
@ -1202,6 +1221,7 @@ async function authorizeSessionList(
manager: InstanceProxyWorkspaceManager,
workspaceId: string,
client?: OpenCodeClient,
signal?: AbortSignal,
): Promise<"allowed" | "invalid" | "foreign"> {
if (method !== "GET" || targetUrl.pathname.replace(/\/+$/, "") !== "/api/session") return "allowed"
const cursors = targetUrl.searchParams.getAll("cursor")
@ -1212,10 +1232,11 @@ async function authorizeSessionList(
// A project cursor embeds its original wide scope; never reinterpret it as
// a directory cursor when Git authority is unavailable.
if (scope.project && !(await readGitStatus()).available) return "foreign"
signal?.throwIfAborted()
for (const key of ["directory", "location[directory]", "project", "subpath"]) {
targetUrl.searchParams.delete(key)
}
return ownsSessionListScope(manager, workspaceId, scope, client)
return ownsSessionListScope(manager, workspaceId, scope, client, signal)
}
const projects = targetUrl.searchParams.getAll("project")
@ -1231,10 +1252,11 @@ async function authorizeSessionList(
const project = projects[0]
const subpath = subpaths[0]
if (!project || (subpath !== undefined && !isSafeRelativePath(subpath))) return "invalid"
const ownership = await ownsSessionListScope(manager, workspaceId, { project, subpath }, client)
const ownership = await ownsSessionListScope(manager, workspaceId, { project, subpath }, client, signal)
if (ownership !== "allowed") return ownership
if (!(await readGitStatus()).available) {
if (directory && !await manager.ownsLocation(workspaceId, { directory }, client)) return "foreign"
signal?.throwIfAborted()
if (directory && !await manager.ownsLocation(workspaceId, { directory }, client, signal)) return "foreign"
// Narrow the initial query, preserving native directory-scoped pagination.
// Unlike a project cursor this request has no already-established scope.
const root = manager.getServiceDirectory?.(workspaceId) ?? manager.get(workspaceId)?.path
@ -1255,20 +1277,25 @@ async function ownsSessionListScope(
workspaceId: string,
scope: SessionListScope,
client?: OpenCodeClient,
signal?: AbortSignal,
): Promise<"allowed" | "foreign"> {
signal?.throwIfAborted()
if (scope.directory) {
const owned = await manager.ownsLocation(workspaceId, { directory: scope.directory }, client)
const owned = await manager.ownsLocation(workspaceId, { directory: scope.directory }, client, signal)
return owned ? "allowed" : "foreign"
}
if (!scope.project) return "foreign"
const project = (await (client ?? await manager.getSharedServiceClient()).project.list()).find((candidate) => candidate.id === scope.project)
const serviceClient = client ?? await manager.getSharedServiceClient()
signal?.throwIfAborted()
const project = (await serviceClient.project.list({ signal })).find((candidate) => candidate.id === scope.project)
signal?.throwIfAborted()
if (!project) return "foreign"
const directory = scope.subpath === undefined
? project.canonical
: /^[A-Za-z]:[\\/]|^\\\\/.test(project.canonical)
? path.win32.resolve(project.canonical, scope.subpath)
: path.posix.resolve(project.canonical, scope.subpath)
const owned = await manager.ownsLocation(workspaceId, { directory }, client)
const owned = await manager.ownsLocation(workspaceId, { directory }, client, signal)
return owned ? "allowed" : "foreign"
}

View file

@ -0,0 +1,45 @@
import assert from "node:assert/strict"
import { EventEmitter } from "node:events"
import { test } from "node:test"
import type { FastifyReply, FastifyRequest } from "fastify"
import { requestAdmission } from "./request-admission"
function fixture() {
const request = Object.assign(new EventEmitter(), { aborted: false })
const reply = Object.assign(new EventEmitter(), { destroyed: false, writableFinished: false })
const admission = requestAdmission({ raw: request } as unknown as FastifyRequest, { raw: reply } as unknown as FastifyReply)
return { request, reply, admission }
}
test("normal upload close and completed response close do not revoke admission", () => {
const h = fixture()
h.request.emit("close")
assert.equal(h.admission.signal.aborted, false)
h.reply.writableFinished = true
h.reply.emit("close")
assert.equal(h.admission.signal.aborted, false)
h.admission.dispose()
assert.equal(h.request.listenerCount("aborted"), 0)
assert.equal(h.reply.listenerCount("close"), 0)
})
test("aborted uploads and incomplete response close retire a held observer and dispose listeners", async () => {
for (const upload of [true, false]) {
const h = fixture()
const pending = h.admission.wait(new Promise<void>(() => {}))
const rejected = assert.rejects(pending, { name: "AbortError" })
if (upload) h.request.emit("aborted")
else h.reply.emit("close")
await rejected
h.admission.dispose()
assert.equal(h.request.listenerCount("aborted"), 0)
assert.equal(h.reply.listenerCount("close"), 0)
}
})
test("already disconnected admission observes late rejected work without unhandled rejection", async () => {
const h = fixture()
h.reply.emit("close")
await assert.rejects(h.admission.wait(Promise.reject(new Error("late shared failure"))), { name: "AbortError" })
h.admission.dispose()
})

View file

@ -0,0 +1,41 @@
import type { FastifyReply, FastifyRequest } from "fastify"
/** Cancellation belongs to this HTTP observer, never to shared connection work.
* IncomingMessage.close also fires after a normal request body; only an aborted
* upload or the downstream response closing before completion revokes admission.
*/
export function requestAdmission(request: FastifyRequest, reply: FastifyReply) {
const controller = new AbortController()
const abort = () => controller.abort()
const close = () => { if (!reply.raw.writableFinished) abort() }
request.raw.once("aborted", abort)
reply.raw.once("close", close)
if (request.raw.aborted || reply.raw.destroyed) abort()
return {
signal: controller.signal,
async wait<T>(pending: Promise<T>): Promise<T> {
if (controller.signal.aborted) {
// A caller may already have allocated the Promise. Observe its rejection
// even when admission was revoked immediately before this boundary.
void pending.catch(() => undefined)
controller.signal.throwIfAborted()
}
let rejectAbort!: () => void
const aborted = new Promise<never>((_resolve, reject) => {
rejectAbort = () => reject(controller.signal.reason)
controller.signal.addEventListener("abort", rejectAbort, { once: true })
})
try {
const result = await Promise.race([pending, aborted])
controller.signal.throwIfAborted()
return result
} finally {
controller.signal.removeEventListener("abort", rejectAbort)
}
},
dispose() {
request.raw.off("aborted", abort)
reply.raw.off("close", close)
},
}
}

View file

@ -0,0 +1,272 @@
import assert from "node:assert/strict"
import crypto from "node:crypto"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { performance } from "node:perf_hooks"
import { it } from "node:test"
import Fastify from "fastify"
import pino from "pino"
import { AuthManager } from "../../auth/manager"
import { registerAuthRoutes } from "./auth"
const username = "fixture-user"
const password = "fixture-password"
async function fixture(options: { persist?: boolean; generateToken?: boolean; password?: string; dangerouslySkipAuth?: boolean } = {}) {
const tempRoot = path.join(os.tmpdir(), "opencode")
await fs.mkdir(tempRoot, { recursive: true })
const directory = await fs.mkdtemp(path.join(tempRoot, "codenomad-auth-responsive-"))
// An existing v1 record, produced independently with the old synchronous API.
const salt = Buffer.from("0123456789abcdef")
const params = { N: 16384, r: 8, p: 1, maxmem: 32 * 1024 * 1024 }
if (options.persist !== false) await fs.writeFile(path.join(directory, "auth.json"), JSON.stringify({
version: 1,
username,
password: {
algorithm: "scrypt", saltBase64: salt.toString("base64"),
hashBase64: crypto.scryptSync(password, salt, 64, params).toString("base64"),
keyLength: 64, params,
},
userProvided: true,
updatedAt: "2026-01-01T00:00:00.000Z",
}))
const manager = new AuthManager({
configPath: path.join(directory, "config.yaml"), username, generateToken: false, ...options,
}, pino({ level: "silent" }))
const app = Fastify({ logger: false })
registerAuthRoutes(app, { authManager: manager })
return {
app, manager, directory,
async close() {
await app.close()
await fs.rm(directory, { recursive: true, force: true })
},
}
}
function observeWork<T>(work: () => T, state: { pending: number; completed: number; maxCallMs: number }): T {
state.pending++
const start = performance.now()
try {
const result = work()
state.maxCallMs = Math.max(state.maxCallMs, performance.now() - start)
if (result && typeof (result as unknown as Promise<unknown>).then === "function") {
return Promise.resolve(result).finally(() => { state.pending--; state.completed++ }) as T
}
state.pending--
state.completed++
return result
} catch (error) {
state.pending--
state.completed++
throw error
}
}
it("services a witness endpoint while a bounded burst of login verifications is pending", async (t) => {
const f = await fixture()
try {
const state = { pending: 0, completed: 0, maxCallMs: 0 }
f.app.get("/fixture-heartbeat", async () => ({ pending: state.pending, completed: state.completed }))
const validate = f.manager.validateLogin.bind(f.manager)
let probe: Promise<{ pending: number; completed: number }> | undefined
let probeMs = 0
f.manager.validateLogin = (...args) => {
if (!probe) {
const start = performance.now()
probe = new Promise((resolve, reject) => setImmediate(() => {
f.app.inject({ method: "GET", url: "/fixture-heartbeat" }).then((response) => {
probeMs = performance.now() - start
resolve(response.json())
}, reject)
}))
}
return observeWork(() => validate(...args), state)
}
const start = performance.now()
const responses = await Promise.all(Array.from({ length: 8 }, () => f.app.inject({
method: "POST", url: "/api/auth/login", payload: { username, password: "invalid-password" },
})))
const witness = await probe!
t.diagnostic(JSON.stringify({ operation: "login", totalMs: performance.now() - start, probeMs, ...state, witness }))
for (const response of responses) {
assert.equal(response.statusCode, 401)
assert.deepEqual(response.json(), { error: "Invalid credentials" })
assert.equal(response.headers["set-cookie"], undefined)
}
assert.ok(witness.pending > 0, "the HTTP witness must run before all expensive verifications finish")
} finally {
await f.close()
}
})
it("awaits login results for valid credentials, rejects wrong usernames and authenticates the cookie", async () => {
const f = await fixture()
try {
const responses = await Promise.all([
{ username, password },
{ username: "not-the-user", password },
{ username, password: "not-the-password" },
].map((payload) => f.app.inject({ method: "POST", url: "/api/auth/login", payload })))
assert.deepEqual(responses.map((response) => response.statusCode), [200, 401, 401])
assert.deepEqual(responses[0].json(), { ok: true })
const cookie = String(responses[0].headers["set-cookie"])
assert.match(cookie, /HttpOnly; Path=\/; SameSite=Lax/)
const status = await f.app.inject({ method: "GET", url: "/api/auth/status", headers: { cookie } })
assert.deepEqual(status.json(), { authenticated: true, username, passwordUserProvided: true })
} finally {
await f.close()
}
})
it("rejects a pending login verified against a password replaced before verification returns", async (t) => {
const f = await fixture()
let release: (() => void) | undefined
let pendingLogin: Promise<unknown> | undefined
try {
let notifyDerived!: () => void
const derived = new Promise<void>((resolve) => { notifyDerived = resolve })
const scrypt = crypto.scrypt.bind(crypto)
let held = false
// Run real scrypt, but hold the first old-password callback until the new
// password is durably committed. No scheduler or elapsed-time assumptions.
const scryptMock = t.mock.method(crypto, "scrypt", (
input: crypto.BinaryLike, salt: crypto.BinaryLike, keyLength: number,
options: crypto.ScryptOptions, callback: (error: Error | null, key: Buffer) => void,
) => scrypt(input, salt, keyLength, options, (error, key) => {
if (input === password && !held) {
held = true
release = () => { release = undefined; callback(error, key) }
notifyDerived()
} else {
callback(error, key)
}
}))
const login = f.app.inject({ method: "POST", url: "/api/auth/login", payload: { username, password } })
.then((response) => response)
pendingLogin = login
await derived
const session = f.manager.createSession(username)
const changed = await f.app.inject({
method: "POST", url: "/api/auth/password", payload: { password: "replacement-password" },
headers: { cookie: `${f.manager.getCookieName()}=${session.id}` },
})
assert.equal(changed.statusCode, 200)
assert.equal(await f.manager.validateLogin(username, "replacement-password"), true)
release!()
const stale = await login
assert.equal(stale.statusCode, 401, "a successful derivation of superseded credentials must not authorize a login")
assert.deepEqual(stale.json(), { error: "Invalid credentials" })
assert.equal(stale.headers["set-cookie"], undefined)
assert.equal(scryptMock.mock.callCount(), 3, "do not replay the old login against the replacement password")
assert.equal(await f.manager.validateLogin(username, password), false)
} finally {
release?.()
await pendingLogin
t.mock.restoreAll()
await f.close()
}
})
it("preserves unauthenticated, runtime-override and disabled-auth password errors", async () => {
for (const options of [{}, { password }, { dangerouslySkipAuth: true }]) {
const f = await fixture(options)
try {
if (!options.dangerouslySkipAuth) {
const unauthorized = await f.app.inject({ method: "POST", url: "/api/auth/password", payload: { password } })
assert.equal(unauthorized.statusCode, 401)
assert.deepEqual(unauthorized.json(), { error: "Unauthorized" })
}
if (options.password || options.dangerouslySkipAuth) {
const session = f.manager.createSession(username)
const response = await f.app.inject({
method: "POST", url: "/api/auth/password", payload: { password: "next-password" },
headers: { cookie: `${f.manager.getCookieName()}=${session.id}` },
})
assert.equal(response.statusCode, 409)
assert.match(response.body, options.password ? /provided via CLI\/env/ : /authentication is disabled/)
assert.match(String(response.headers["content-type"]), /^text\/plain/)
}
if (options.password) assert.equal(await f.manager.validateLogin(username, password), true)
if (options.dangerouslySkipAuth) assert.equal(await f.manager.validateLogin("any", "anything"), true)
} finally {
await f.close()
}
}
})
it("keeps bootstrap password changes ordered and recovers the queue after a persistence failure", async () => {
const f = await fixture({ persist: false, generateToken: true })
try {
assert.equal(await f.manager.validateLogin(username, password), false)
assert.deepEqual(f.manager.getStatus(), { username, passwordUserProvided: false })
const authPath = path.join(f.directory, "auth.json")
await fs.mkdir(authPath)
const session = f.manager.createSession(username)
const failed = await f.app.inject({
method: "POST", url: "/api/auth/password", payload: { password: "failed-password" },
headers: { cookie: `${f.manager.getCookieName()}=${session.id}` },
})
assert.equal(failed.statusCode, 409)
assert.match(String(failed.headers["content-type"]), /^text\/plain/)
assert.deepEqual(f.manager.getStatus(), { username, passwordUserProvided: false })
await fs.rmdir(authPath)
const changes = ["first-password", "second-password", "final-password"]
await Promise.all(changes.map((next) => f.manager.setPassword(next)))
assert.equal(await f.manager.validateLogin(username, changes[0]), false)
assert.equal(await f.manager.validateLogin(username, changes[2]), true)
const persisted = JSON.parse(await fs.readFile(authPath, "utf8"))
assert.equal(persisted.username, username)
assert.equal(persisted.userProvided, true)
// Independent synchronous verification proves persisted format compatibility.
const record = persisted.password
assert.equal(crypto.scryptSync(changes[2], Buffer.from(record.saltBase64, "base64"), record.keyLength, record.params)
.toString("base64"), record.hashBase64)
} finally {
await f.close()
}
})
it("services a witness endpoint during password derivation and awaits persistence", async (t) => {
const f = await fixture()
try {
const session = f.manager.createSession(username)
const cookie = `${f.manager.getCookieName()}=${session.id}`
const state = { pending: 0, completed: 0, maxCallMs: 0 }
f.app.get("/fixture-heartbeat", async () => ({ pending: state.pending, completed: state.completed }))
const setPassword = f.manager.setPassword.bind(f.manager)
let probe: Promise<{ pending: number; completed: number }> | undefined
let probeMs = 0
f.manager.setPassword = (...args) => {
if (!probe) {
const start = performance.now()
probe = new Promise((resolve, reject) => setImmediate(() => {
f.app.inject({ method: "GET", url: "/fixture-heartbeat" }).then((response) => {
probeMs = performance.now() - start
resolve(response.json())
}, reject)
}))
}
return observeWork(() => setPassword(...args), state)
}
const start = performance.now()
const responses = await Promise.all(Array.from({ length: 4 }, (_, index) => f.app.inject({
method: "POST", url: "/api/auth/password", headers: { cookie },
payload: { password: `new-password-${index}` },
})))
const witness = await probe!
t.diagnostic(JSON.stringify({ operation: "password", totalMs: performance.now() - start, probeMs, ...state, witness }))
for (const response of responses) {
assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), { ok: true, username, passwordUserProvided: true })
}
assert.equal(await f.manager.validateLogin(username, "new-password-3"), true)
const persisted = JSON.parse(await fs.readFile(path.join(f.directory, "auth.json"), "utf8"))
assert.equal(persisted.version, 1)
assert.equal(persisted.password.algorithm, "scrypt")
assert.ok(witness.pending > 0, "the HTTP witness must run before all expensive password changes finish")
} finally {
await f.close()
}
})

View file

@ -98,7 +98,7 @@ export function registerAuthRoutes(app: FastifyInstance, deps: RouteDeps) {
app.post("/api/auth/login", async (request, reply) => {
const body = LoginSchema.parse(request.body ?? {})
const ok = deps.authManager.validateLogin(body.username, body.password)
const ok = await deps.authManager.validateLogin(body.username, body.password)
if (!ok) {
reply.code(401).send({ error: "Invalid credentials" })
return
@ -147,7 +147,7 @@ export function registerAuthRoutes(app: FastifyInstance, deps: RouteDeps) {
const body = PasswordSchema.parse(request.body ?? {})
try {
const status = deps.authManager.setPassword(body.password)
const status = await deps.authManager.setPassword(body.password)
reply.send({ ok: true, ...status })
} catch (error) {
const message = error instanceof Error ? error.message : String(error)

View file

@ -267,10 +267,12 @@ export class WorkspaceManager {
return Boolean(hostDirectory && await this.ownsHostDirectory(record, hostDirectory))
}
async ownsLocation(id: string, location: LocationRef, client?: OpenCodeClient): Promise<boolean> {
async ownsLocation(id: string, location: LocationRef, client?: OpenCodeClient, signal?: AbortSignal): Promise<boolean> {
signal?.throwIfAborted()
const record = this.workspaces.get(id)
if (!record?.[WORKSPACE_STATE].published) return false
const directory = await this.getServiceDirectoryForPath(id, location.directory)
signal?.throwIfAborted()
if (!directory) return false
if (location.workspaceID === undefined) return true
// The directory fence alone cannot authorize a legacy native workspace.
@ -279,8 +281,8 @@ export class WorkspaceManager {
try {
const requested = readLocationRef({ ...location, directory })
const resolved = readLocationRef(client
? await client.location.get({ location: { directory } }, locationRequestOptions(requested))
: await this.sharedService.validateLocation(requested, undefined, record[WORKSPACE_STATE].serviceOptions))
? await client.location.get({ location: { directory } }, { ...locationRequestOptions(requested), signal })
: await this.sharedService.validateLocation(requested, { signal }, record[WORKSPACE_STATE].serviceOptions))
return sameLocation(requested, resolved)
} catch {
return false

View file

@ -127,7 +127,7 @@ export class OpenCodeSharedService {
try {
connection = await this.connect(serviceOptions, requestOptions?.deadlineAt)
const nativeRequestOptions = requestOptions?.signal ? { signal: requestOptions.signal } : undefined
return this.invalidateAfterStream(connection.client.event.subscribe(nativeRequestOptions), connection)
return this.invalidateAfterStream(connection.client.event.subscribe(nativeRequestOptions), connection, requestOptions?.signal)
} catch (error) {
if (connection && !requestOptions?.signal?.aborted) this.invalidateConnection(connection)
throw error
@ -291,14 +291,28 @@ export class OpenCodeSharedService {
}
}
private async *invalidateAfterStream(events: AsyncIterable<OpenCodeEvent>, connection: ServiceConnection) {
private async *invalidateAfterStream(events: AsyncIterable<OpenCodeEvent>, connection: ServiceConnection, signal?: AbortSignal) {
const iterator = events[Symbol.asyncIterator]()
try {
for await (const event of events) {
while (true) {
let result: IteratorResult<OpenCodeEvent>
try {
result = await iterator.next()
} catch (error) {
if (!signal?.aborted) this.invalidateConnection(connection)
throw error
}
if (result.done) {
if (!signal?.aborted) this.invalidateConnection(connection)
return
}
connection.assertCurrent()
yield event
// Consumer return/throw and subscriber-local abort are not failures of
// the shared source. Uncancelled iterator EOF/errors still invalidate.
yield result.value
}
} finally {
this.invalidateConnection(connection)
await iterator.return?.()
}
}

View file

@ -0,0 +1,116 @@
import assert from "node:assert/strict"
import { test, type TestContext } from "node:test"
import type { ServerResponse } from "node:http"
import Fastify from "fastify"
import type { OpenCodeEvent } from "@opencode/client"
import { OpenCodeSharedService } from "./opencode-service"
async function transport(t: TestContext) {
const upstream = Fastify()
const streams: ServerResponse[] = []
upstream.get("/api/event", (_request, reply) => {
reply.raw.setHeader("Content-Type", "text/event-stream")
reply.raw.flushHeaders()
reply.hijack()
streams.push(reply.raw)
reply.raw.write(`data: ${JSON.stringify({ id: "connected", created: Date.now(), type: "server.connected", data: {} })}\n\n`)
})
upstream.get("/api/info", async () => ({ version: "fixture" }))
await upstream.listen({ host: "127.0.0.1", port: 0 })
const endpoint = { url: `http://127.0.0.1:${(upstream.server.address() as { port: number }).port}` }
const service = new OpenCodeSharedService()
// Only CLI lifecycle/upstream are fixtures. SDK sharing, cancellation, SSE
// parsing and the CodeNomad connection fences remain real.
const client = await service.client({ kind: "lifecycle", identity: "isolated-event-transport",
lifecycle: { discover: async () => endpoint, ensure: async () => endpoint },
})
t.after(async () => {
await service.shutdown()
for (const stream of streams) stream.destroy()
await upstream.close()
})
return { service, client, streams,
send(event: OpenCodeEvent) { streams.at(-1)!.write(`data: ${JSON.stringify(event)}\n\n`) },
}
}
for (const mode of ["abort", "return", "throw"] as const) {
test(`one subscriber's ${mode} cannot invalidate another subscriber or the healthy client`, async t => {
const f = await transport(t)
const controller = new AbortController()
const other = f.client.event.subscribe({ signal: controller.signal })[Symbol.asyncIterator]()
t.after(async () => { controller.abort(); await other.return?.() })
assert.equal((await other.next()).value?.type, "server.connected")
const local = new AbortController()
const wrapped = (await f.service.subscribe({ signal: local.signal }))[Symbol.asyncIterator]()
t.after(async () => { local.abort(); await wrapped.return?.() })
assert.equal((await wrapped.next()).value?.type, "server.connected")
assert.equal(f.streams.length, 1, "both consumers must share one physical HTTP stream")
if (mode === "abort") {
const waiting = wrapped.next()
local.abort()
assert.equal((await waiting).done, true)
} else if (mode === "throw") {
const failure = new Error("consumer failure")
await assert.rejects(wrapped.throw!(failure), error => error === failure)
} else await wrapped.return?.()
assert.equal((await f.client.server.info()).version, "fixture")
assert.equal((await f.service.acquire()).client, f.client)
const next = other.next()
const renamed = { id: "after-local-stop", created: Date.now(), type: "session.renamed" as const,
durable: { aggregateID: "session", seq: 1, version: 1 as const },
data: { sessionID: "session", title: "still shared" },
}
f.send(renamed)
assert.deepEqual((await next).value, renamed)
})
}
test("an already-aborted subscriber never opens a stream or invalidates the healthy connection", async t => {
const f = await transport(t)
const controller = new AbortController()
controller.abort()
const wrapped = (await f.service.subscribe({ signal: controller.signal }))[Symbol.asyncIterator]()
assert.equal((await wrapped.next()).done, true)
assert.equal((await f.service.acquire()).client, f.client)
assert.equal(f.streams.length, 0)
})
test("genuine upstream EOF invalidates its connection but late cleanup cannot invalidate its replacement", async t => {
const f = await transport(t)
const controller = new AbortController()
const wrapped = (await f.service.subscribe({ signal: controller.signal }))[Symbol.asyncIterator]()
t.after(async () => { controller.abort(); await wrapped.return?.() })
assert.equal((await wrapped.next()).value?.type, "server.connected")
const ended = wrapped.next()
f.streams[0].end()
assert.equal((await ended).done, true)
const replacement = await f.service.acquire()
assert.notEqual(replacement.client, f.client)
await wrapped.return?.()
assert.equal(await f.service.acquire(), replacement)
})
test("abrupt upstream transport failure still invalidates its connection", async t => {
const f = await transport(t)
const controller = new AbortController()
const wrapped = (await f.service.subscribe({ signal: controller.signal }))[Symbol.asyncIterator]()
t.after(async () => { controller.abort(); await wrapped.return?.() })
assert.equal((await wrapped.next()).value?.type, "server.connected")
const failed = wrapped.next()
f.streams[0].destroy()
await assert.rejects(failed)
assert.notEqual((await f.service.acquire()).client, f.client)
})
test("an obsolete iterator cannot invalidate its replacement connection or publish queued events", async t => {
const f = await transport(t)
const controller = new AbortController()
const wrapped = (await f.service.subscribe({ signal: controller.signal }))[Symbol.asyncIterator]()
t.after(async () => { controller.abort(); await wrapped.return?.() })
assert.equal((await wrapped.next()).value?.type, "server.connected")
f.service.invalidate()
const replacement = await f.service.acquire()
await assert.rejects(wrapped.next())
assert.equal(await f.service.acquire(), replacement)
})

View file

@ -912,10 +912,6 @@ export default function PromptInput(props: PromptInputProps) {
mode={pickerMode()}
onClose={handlePickerClose}
onSelect={handlePickerSelect}
onSubmitWithoutSelection={() => {
handlePickerClose()
void handleSend()
}}
agents={instanceAgents()}
commands={promptCommands()}
searchQuery={searchQuery()}

View file

@ -3,7 +3,7 @@ import { describe, it } from "node:test"
import { usePromptKeyDown } from "./usePromptKeyDown.ts"
function setup(submitOnEnter = true) {
function setup(submitOnEnter = true, pickerOpen = false) {
let prompt = "hello"
const sends: Array<boolean | undefined> = []
let backgrounds = 0
@ -23,8 +23,8 @@ function setup(submitOnEnter = true) {
setPrompt: (value) => { prompt = value },
mode: () => "normal",
setMode: () => {},
isPickerOpen: () => false,
closePicker: () => {},
isPickerOpen: () => pickerOpen,
closePicker: () => { pickerOpen = false },
ignoredAtPositions: () => new Set(),
setIgnoredAtPositions: () => {},
getAttachments: () => [],
@ -38,16 +38,42 @@ function setup(submitOnEnter = true) {
selectPreviousHistory: () => false,
selectNextHistory: () => false,
})
const press = (input: Partial<KeyboardEvent>) => handler({
key: "Enter",
preventDefault() {},
stopPropagation() {},
...input,
} as KeyboardEvent)
const press = (input: Partial<KeyboardEvent>) => {
let prevented = false, stopped = false
handler({
key: "Enter",
preventDefault() { prevented = true },
stopPropagation() { stopped = true },
...input,
} as KeyboardEvent)
return { prevented, stopped }
}
return { press, sends, backgrounds: () => backgrounds, prompt: () => prompt }
}
describe("prompt submit shortcuts", () => {
it("does not send a key already consumed by a picker that closed during dispatch", () => {
const input = setup()
input.press({ defaultPrevented: true })
input.press({ defaultPrevented: true, ctrlKey: true, shiftKey: true })
assert.deepEqual(input.sends, [])
assert.equal(input.prompt(), "hello")
})
for (const submitOnEnter of [true, false]) {
it(`reserves Enter for an open picker with submitOnEnter=${submitOnEnter}`, () => {
const input = setup(submitOnEnter, true)
for (const modifiers of [{}, { shiftKey: true }, { ctrlKey: true }, { metaKey: true },
{ ctrlKey: true, shiftKey: true }, { metaKey: true, shiftKey: true }]) {
const handled = input.press(modifiers)
assert.equal(handled.prevented, true, "an empty or not-yet-mounted picker cannot insert a newline")
assert.equal(handled.stopped, Boolean(modifiers.ctrlKey || modifiers.metaKey))
assert.deepEqual(input.sends, [], "picker Enter cannot send or queue the draft")
assert.equal(input.prompt(), "hello")
}
})
}
it("sends normally on Enter and queues only on Mod+Shift+Enter", () => {
const input = setup()
input.press({})

View file

@ -240,10 +240,17 @@ export function usePromptKeyDown(options: UsePromptKeyDownOptions) {
}
if (e.key === "Enter") {
// A picker listener may already have selected an item and closed itself
// before this delegated handler runs. Never submit the consumed key.
if (e.defaultPrevented) return
const isModified = e.metaKey || e.ctrlKey
// If the picker is open, Enter should select from it.
if (!isModified && options.isPickerOpen()) {
// The picker owns Enter even when loading, empty or not yet mounted.
// Unmodified Enter (including Shift) bubbles to its selection handler;
// submit/queue modifiers are swallowed rather than sending the draft.
if (options.isPickerOpen()) {
e.preventDefault()
if (isModified) e.stopPropagation()
return
}
@ -263,10 +270,6 @@ export function usePromptKeyDown(options: UsePromptKeyDownOptions) {
}
if (e.shiftKey) {
// If the picker is open, avoid selecting an item on Enter.
if (options.isPickerOpen()) {
e.stopPropagation()
}
return
}
@ -278,9 +281,6 @@ export function usePromptKeyDown(options: UsePromptKeyDownOptions) {
// Default: Cmd/Ctrl+Enter submits.
if (isModified) {
e.preventDefault()
if (options.isPickerOpen()) {
options.closePicker()
}
options.onSend(e.shiftKey)
return
}

View file

@ -1,4 +1,4 @@
import { For, Index, Show, createEffect, createMemo, createSignal, onCleanup, untrack } from "solid-js"
import { For, Show, createEffect, createMemo, createSignal, onCleanup, untrack } from "solid-js"
import { Dynamic } from "solid-js/web"
import { Copy } from "lucide-solid"
import type { ToolState } from "../../../types/tool-state"
@ -82,20 +82,23 @@ function TaskToolCallRow(props: {
const messageVersion = createMemo(() => record()?.revision ?? 0)
const partVersion = createMemo(() => partEntry()?.revision ?? 0)
const rendered = createMemo(() => {
const part = toolPart()
if (!part) return null
return props.renderToolCall({
toolCall: part as any,
messageId: messageId(),
messageVersion: messageVersion(),
partVersion: partVersion(),
sessionId: props.sessionId,
// Keep one shell for this key. Native page reprojections and unrelated text
// revisions must update its props, not discard disclosure/scroll state.
// Rows follow their tool identity, not their position in the bounded window.
// A new identity still needs a fresh shell with its default disclosure.
// Nonzero-arity Show callbacks mount untracked; getters update the child props.
return <Show when={props.toolKey} keyed>{(_key) => (
<Show when={toolPart()}>{(_part) => props.renderToolCall({
get toolCall() { partVersion(); return toolPart()! },
get messageId() { return messageId() },
get messageVersion() { return messageVersion() },
// Native page hydration resets the per-part revision to zero. Let
// Markdown use its content hash instead of pinning changed output to 0;
// the getter above still fences versioned in-place snapshot updates.
get sessionId() { return props.sessionId },
forceCollapsed: true,
})
})
return <>{rendered()}</>
})}</Show>
)}</Show>
}
function normalizeStatus(status?: string | null): ToolState["status"] | undefined {
@ -229,15 +232,9 @@ export const taskRenderer: ToolRenderer = {
const [childToolsTruncated, setChildToolsTruncated] = createSignal(false)
let indexedSessionId = ""
let indexedMessageCount = 0
let indexedMessageTail = ""
const indexedPartCounts = new Map<string, number>()
function resetChildToolIndex(nextSessionId: string) {
indexedSessionId = nextSessionId
indexedMessageCount = 0
indexedMessageTail = ""
indexedPartCounts.clear()
setChildToolKeys([])
setChildToolsTruncated(false)
}
@ -265,15 +262,11 @@ export const taskRenderer: ToolRenderer = {
keys.unshift(`${messageId}::${partId}`)
}
if (idx >= oldestScannedIndex) setChildToolsTruncated(true)
indexedPartCounts.set(messageId, partIds.length)
return keys
}
function fullRescanChildTools(sessionId: string, messageIds: string[]) {
indexedSessionId = sessionId
indexedMessageCount = messageIds.length
indexedMessageTail = messageIds[messageIds.length - 1] ?? ""
indexedPartCounts.clear()
setChildToolsTruncated(false)
const nextKeys: string[] = []
@ -284,93 +277,35 @@ export const taskRenderer: ToolRenderer = {
const keys = scanMessageToolParts(messageIds[index], 0, scanLimit - nextKeys.length, budget)
for (let keyIndex = keys.length - 1; keyIndex >= 0; keyIndex -= 1) nextKeys.unshift(keys[keyIndex])
}
for (let index = oldestScannedIndex; index < messageIds.length; index += 1) {
const messageId = messageIds[index]
if (!indexedPartCounts.has(messageId)) indexedPartCounts.set(messageId, store.getMessage(messageId)?.partIds.length ?? 0)
}
setChildToolsTruncated((truncated) => isTaskScanTruncated(truncated, oldestScannedIndex > 0, isTaskStepListTruncated(nextKeys.length)))
setChildToolKeys(nextKeys.slice(-TASK_STEP_RENDER_LIMIT))
const keys = nextKeys.slice(-TASK_STEP_RENDER_LIMIT)
setChildToolKeys(previous => previous.length === keys.length && previous.every((key, index) => key === keys[index]) ? previous : keys)
}
createEffect(() => {
const id = childSessionId()
const loaded = childSessionLoaded()
if (!id || !loaded) {
if (indexedSessionId) {
resetChildToolIndex("")
}
if (!id || (indexedSessionId && indexedSessionId !== id)) {
resetChildToolIndex("")
}
if (!id) return
if (!loaded) {
// Invalidation requests a fresh page, but its resident display snapshot
// may still be valid. Keep its shells while that read is pending, but
// clear membership and truncation when deletion/eviction removed it.
if (store.getSessionMessageIds(id).length === 0) resetChildToolIndex("")
// Rebuild resident membership after the authoritative page arrives.
return
}
// We use the session revision as the reactive change point, but avoid
// rescanning the entire session on every update.
// Authoritative pages can remove, replace or reorder parts without changing
// message counts. Revalidate only bounded structural identities (not output
// payloads); unchanged keys retain their array and keyed tool shells.
store.getSessionRevision(id)
untrack(() => {
const messageIds = store.getSessionMessageIds(id)
if (!indexedSessionId || indexedSessionId !== id) {
fullRescanChildTools(id, messageIds)
return
}
// Detect structural changes (reorder/shrink) and fall back to a full rescan.
if (messageIds.length < indexedMessageCount) {
fullRescanChildTools(id, messageIds)
return
}
if (indexedMessageCount > 0) {
const expectedTailIndex = indexedMessageCount - 1
if (expectedTailIndex >= 0 && messageIds[expectedTailIndex] !== indexedMessageTail) {
fullRescanChildTools(id, messageIds)
return
}
}
const appendedKeys: string[] = []
const budget = { remaining: TASK_MESSAGE_SCAN_LIMIT }
// Scan any new messages appended since last index.
const appendedStart = Math.max(indexedMessageCount, messageIds.length - TASK_MESSAGE_SCAN_LIMIT)
if (appendedStart > indexedMessageCount) setChildToolsTruncated(true)
for (let idx = appendedStart; idx < messageIds.length && budget.remaining > 0; idx += 1) {
const messageId = messageIds[idx]
appendedKeys.push(...scanMessageToolParts(messageId, 0, TASK_STEP_RENDER_LIMIT, budget))
if (appendedKeys.length > TASK_STEP_RENDER_LIMIT) {
setChildToolsTruncated(true)
appendedKeys.splice(0, appendedKeys.length - TASK_STEP_RENDER_LIMIT)
}
}
// Scan the bounded indexed window so out-of-order updates are not missed.
const existingCount = Math.min(indexedMessageCount, messageIds.length)
const windowStart = Math.max(0, existingCount - TASK_MESSAGE_SCAN_LIMIT)
for (let idx = windowStart; idx < existingCount && budget.remaining > 0; idx += 1) {
const messageId = messageIds[idx]
const previousPartCount = indexedPartCounts.get(messageId) ?? 0
const record = store.getMessage(messageId)
const nextPartCount = record?.partIds.length ?? 0
if (nextPartCount > previousPartCount) {
appendedKeys.push(...scanMessageToolParts(messageId, previousPartCount, TASK_STEP_RENDER_LIMIT, budget))
if (appendedKeys.length > TASK_STEP_RENDER_LIMIT) {
setChildToolsTruncated(true)
appendedKeys.splice(0, appendedKeys.length - TASK_STEP_RENDER_LIMIT)
}
}
}
indexedMessageCount = messageIds.length
indexedMessageTail = messageIds[messageIds.length - 1] ?? ""
if (indexedPartCounts.size > TASK_MESSAGE_SCAN_LIMIT) {
const retainedIds = new Set(messageIds.slice(-TASK_MESSAGE_SCAN_LIMIT))
for (const messageId of indexedPartCounts.keys()) if (!retainedIds.has(messageId)) indexedPartCounts.delete(messageId)
}
if (appendedKeys.length > 0) {
if (childToolKeys().length + appendedKeys.length > TASK_STEP_RENDER_LIMIT) setChildToolsTruncated(true)
setChildToolKeys((prev) => [...prev, ...appendedKeys].slice(-TASK_STEP_RENDER_LIMIT))
}
fullRescanChildTools(id, store.getSessionMessageIds(id))
})
})
const promptContent = createMemo(() => {
@ -591,12 +526,12 @@ export const taskRenderer: ToolRenderer = {
}
>
<div class="tool-call-task-summary">
<Index each={childToolKeys()}>
<For each={childToolKeys()}>
{(key) => (
<Show when={renderToolCall}>
{(render) => (
<TaskToolCallRow
toolKey={key()}
toolKey={key}
store={store}
sessionId={childSessionId()}
renderToolCall={render()}
@ -604,7 +539,7 @@ export const taskRenderer: ToolRenderer = {
)}
</Show>
)}
</Index>
</For>
</div>
{scrollHelpers?.renderSentinel?.()}
</div>

View file

@ -79,7 +79,6 @@ interface UnifiedPickerProps {
mode?: "mention" | "command"
onSelect: (item: PickerItem, action: PickerSelectAction) => void
onClose: () => void
onSubmitWithoutSelection?: () => void
agents: Agent[]
commands?: CommandInfo[]
searchQuery: string
@ -414,12 +413,13 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
} else if (e.key === "Enter" || e.key === "Tab") {
e.preventDefault()
e.stopPropagation()
// Solid delegates textarea key handlers at document too, so propagation
// alone cannot keep modified submit shortcuts out of this listener.
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) return
const selected = items[selectedIndex()]
if (selected) {
const action: PickerSelectAction = e.key === "Tab" ? "tab" : e.shiftKey ? "shiftEnter" : "enter"
props.onSelect(selected, action)
} else if (e.key === "Enter" && mode() === "mention") {
props.onSubmitWithoutSelection?.()
}
} else if (e.key === "Escape") {
e.preventDefault()

View file

@ -14,6 +14,23 @@ const logger = {
}
describe("attachEventSourceHandlers", () => {
it("separates malformed data from callback failures without reconnect or replay", () => {
const source = new FakeEventSource()
const errors: string[] = []
const received: unknown[] = []
let reconnects = 0
attachEventSourceHandlers(source as unknown as EventSource, {
onEvent(event) { received.push(event); throw new Error("consumer failed") },
onError() { reconnects++ },
logger: { warn() {}, error(message) { errors.push(message) } },
})
source.onmessage!(new MessageEvent("message", { data: "malformed" }))
source.onmessage!(new MessageEvent("message", { data: '{"type":"fixture"}' }))
assert.deepEqual(errors, ["Failed to parse event", "Failed to dispatch event"])
assert.deepEqual(received, [{ type: "fixture" }])
assert.equal(reconnects, 0)
})
it("requests reconnect when EventSource emits close", () => {
const source = new FakeEventSource()
let reconnects = 0

View file

@ -20,11 +20,19 @@ export function attachEventSourceHandlers(source: EventSource, options: EventSou
let disconnected = false
source.onmessage = (event) => {
let payload: WorkspaceEventPayload
try {
const payload = JSON.parse(event.data) as WorkspaceEventPayload
options.onEvent(payload)
payload = JSON.parse(event.data) as WorkspaceEventPayload
} catch (error) {
options.logger.error("Failed to parse event", error)
return
}
try {
options.onEvent(payload)
} catch (error) {
// Deferred reactive flush errors occur after individual subscribers have
// returned. They are application failures, not malformed transport data.
options.logger.error("Failed to dispatch event", error)
}
}

View file

@ -0,0 +1,68 @@
import assert from "node:assert/strict"
import { spawn, spawnSync } from "node:child_process"
import { test } from "node:test"
import { fileURLToPath } from "node:url"
const root = fileURLToPath(new URL("../../../../", import.meta.url))
const deadlineMs = 30_000
// Supervise only these private subprocesses. A deadline is a test failure, not
// successful cleanup: the imported application must release its own resources.
function run(args: string[]): Promise<{ code: number | null; signal: string | null; timedOut: boolean; output: string }> {
return new Promise((resolve, reject) => {
// Node's parent test-child marker otherwise suppresses a nested --test
// runner. This child owns its own runner; require its complete test count.
const env = { ...process.env }
delete env.NODE_TEST_CONTEXT
const child = spawn(process.execPath, args, {
cwd: root,
env,
stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
})
let output = "", timedOut = false
child.stdout.on("data", chunk => { output += chunk })
child.stderr.on("data", chunk => { output += chunk })
const timeout = setTimeout(() => {
timedOut = true
if (!child.pid) return
if (process.platform === "win32") {
spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" })
} else {
process.kill(-child.pid, "SIGKILL")
}
}, deadlineMs)
child.once("error", error => { clearTimeout(timeout); reject(error) })
child.once("close", (code, signal) => {
clearTimeout(timeout)
resolve({ code, signal, timedOut, output })
})
})
}
function assertNaturalExit(result: Awaited<ReturnType<typeof run>>): void {
assert.equal(result.timedOut, false, `private subprocess exceeded ${deadlineMs}ms; not a natural exit:\n${result.output}`)
assert.equal(result.signal, null, result.output)
assert.equal(result.code, 0, result.output)
}
test("importing the actual serverEvents singleton without a browser exits naturally", async () => {
const module = new URL("./server-events.ts", import.meta.url).href
const result = await run(["--conditions=browser", "--import", "tsx", "--input-type=module", "--eval",
`await import(${JSON.stringify(module)}); console.log("serverEvents import complete")`,
])
assert.match(result.output, /serverEvents import complete/)
assertNaturalExit(result)
})
test("the actual eleven compaction store tests pass and exit naturally without force-exit", async () => {
const result = await run(["--conditions=browser", "--import", "tsx", "--test", "--test-reporter=tap",
"packages/ui/src/stores/opencode-compaction.test.ts",
])
assertNaturalExit(result)
assert.match(result.output, /^# tests 11$/m)
assert.match(result.output, /^# pass 11$/m)
assert.match(result.output, /^# fail 0$/m)
assert.match(result.output, /^# cancelled 0$/m)
assert.match(result.output, /^# skipped 0$/m)
})

View file

@ -34,7 +34,7 @@ class ServerEvents {
constructor() {
authRecovery.onRestored(() => this.restart("authentication restored"))
void this.connect()
if (typeof window !== "undefined") void this.connect()
}
private async connect() {
@ -72,7 +72,7 @@ class ServerEvents {
}
logSse("Events stream connected")
this.retryDelay = RETRY_BASE_DELAY
this.openHandlers.forEach((handler) => handler())
this.notify(this.openHandlers, undefined, "open")
},
onPing: (payload) => {
if (generation !== this.connectGeneration) {
@ -149,8 +149,20 @@ class ServerEvents {
}
private dispatch(event: WorkspaceEventPayload) {
this.handlers.get("*")?.forEach((handler) => handler(event))
this.handlers.get(event.type)?.forEach((handler) => handler(event))
this.notify(this.handlers.get("*"), event, event.type)
this.notify(this.handlers.get(event.type), event, event.type)
}
private notify<T>(handlers: Set<(value: T) => void> | undefined, value: T, kind: string): void {
handlers?.forEach((handler) => {
try {
handler(value)
} catch (error) {
// A feature failure must not discard another feature's native event or
// interrupt the connection lifecycle. Do not replay failed handlers.
log.error("Backend event subscriber failed", { kind, error })
}
})
}
private dispatchBatch(events: WorkspaceEventPayload[]) {
@ -167,7 +179,7 @@ class ServerEvents {
}
private emitTransportStatus(status: WorkspaceEventTransportStatus) {
this.statusHandlers.forEach((handler) => handler(status))
this.notify(this.statusHandlers, status, "transport-status")
}
on(type: WorkspaceEventType | "*", handler: (event: WorkspaceEventPayload) => void): () => void {

View file

@ -0,0 +1,56 @@
import type { OpenCodeEvent } from "@opencode/client"
type CompactionDelta = Extract<OpenCodeEvent, { type: "session.compaction.delta" }>
const INTERVAL_MS = 250
// Only payload aggregation lives here. Revision/read authority remains with the
// native reducer owner, which cancels or flushes this buffer at its boundaries.
export function createCompactionDeltaBuffer<Context>(reduce: (event: CompactionDelta, context: Context) => void) {
const pending = new Map<string, {
event: CompactionDelta
context: Context
timeout: ReturnType<typeof setTimeout>
}>()
const keyFor = (instanceId: string, sessionId: string) => `${instanceId}\0${sessionId}`
const flush = (instanceId: string, sessionId: string) => {
const key = keyFor(instanceId, sessionId)
const item = pending.get(key)
if (!item) return
clearTimeout(item.timeout)
pending.delete(key)
reduce(item.event, item.context)
}
return {
flush,
push(instanceId: string, sessionId: string, event: CompactionDelta, context: Context) {
const key = keyFor(instanceId, sessionId)
const item = pending.get(key)
if (item) {
item.event = { ...event, data: { ...event.data, text: item.event.data.text + event.data.text } }
item.context = context
return
}
pending.set(key, {
event,
context,
timeout: setTimeout(() => flush(instanceId, sessionId), INTERVAL_MS),
})
},
cancel(instanceId: string, sessionId: string) {
const key = keyFor(instanceId, sessionId)
const item = pending.get(key)
if (!item) return
clearTimeout(item.timeout)
pending.delete(key)
},
clear(instanceId: string) {
const prefix = `${instanceId}\0`
for (const [key, item] of pending) {
if (!key.startsWith(prefix)) continue
clearTimeout(item.timeout)
pending.delete(key)
}
},
}
}

View file

@ -139,51 +139,6 @@ const [activeInstanceId, setActiveInstanceId] = createSignal<string | null>(null
const [instanceLogs, setInstanceLogs] = createSignal<Map<string, LogEntry[]>>(new Map())
const [logStreamingState, setLogStreamingState] = createSignal<Map<string, boolean>>(new Map())
const COMPACTION_PROJECTION_INTERVAL_MS = 250
const pendingCompactionProjections = new Map<string, {
timeout: ReturnType<typeof setTimeout>
project: () => void
}>()
function compactionProjectionKey(instanceId: string, sessionId: string): string {
return `${instanceId}\0${sessionId}`
}
function scheduleCompactionProjection(instanceId: string, sessionId: string, project: () => void): void {
const key = compactionProjectionKey(instanceId, sessionId)
const pending = pendingCompactionProjections.get(key)
if (pending) {
pending.project = project
return
}
const next = {
project,
timeout: setTimeout(() => {
pendingCompactionProjections.delete(key)
next.project()
}, COMPACTION_PROJECTION_INTERVAL_MS),
}
pendingCompactionProjections.set(key, next)
}
function cancelCompactionProjection(instanceId: string, sessionId: string): void {
const key = compactionProjectionKey(instanceId, sessionId)
const pending = pendingCompactionProjections.get(key)
if (!pending) return
clearTimeout(pending.timeout)
pendingCompactionProjections.delete(key)
}
function clearCompactionProjections(instanceId: string): void {
const prefix = `${instanceId}\0`
for (const [key, pending] of pendingCompactionProjections) {
if (!key.startsWith(prefix)) continue
clearTimeout(pending.timeout)
pendingCompactionProjections.delete(key)
}
}
// Interruption queues per instance
const [permissionQueues, setPermissionQueues] = createSignal<Map<string, PermissionRequest[]>>(new Map())
const [activePermissionId, setActivePermissionId] = createSignal<Map<string, string | null>>(new Map())
@ -1218,7 +1173,7 @@ function updateInstance(id: string, updates: Partial<Instance>) {
clearSessionCatalogState(id)
clearCommands(id)
clearInstanceMetadata(id)
clearCompactionProjections(id)
destroyOpenCodeData(id)
volatileInstanceRefreshes.delete(id)
for (const sessionId of sessions().get(id)?.keys() ?? []) invalidateSessionMessageLoad(id, sessionId)
}
@ -1282,7 +1237,7 @@ function removeInstance(id: string, options: { authoritative?: boolean } = {}) {
clearSettledForms(id)
clearPendingFormQueue(id)
clearInstanceMetadata(id)
clearCompactionProjections(id)
destroyOpenCodeData(id)
clearPermissionAutoAcceptForInstance(id)
clearSyncedYoloSessionsForInstance(id)
initialHydrations.delete(id)
@ -1953,10 +1908,6 @@ function handleInstanceInvalidation(instanceId: string, event: Parameters<NonNul
? event.data.form.sessionID
: undefined
const isCompactionDelta = event.type === "session.compaction.delta"
if (event.type === "server.connected") clearCompactionProjections(instanceId)
if (sessionId && (event.type === "session.compaction.ended" || event.type === "session.compaction.failed")) {
cancelCompactionProjection(instanceId, sessionId)
}
const projectMessages = (data: ReturnType<typeof applyOpenCodeDataEvent>, preserveOmitted = true, force = false) => {
if (sessionId && event.type.startsWith("session.")
&& activeSessionId().get(instanceId) !== sessionId
@ -1964,7 +1915,6 @@ function handleInstanceInvalidation(instanceId: string, event: Parameters<NonNul
invalidateSessionMessageLoad(instanceId, sessionId)
}
if (isCompactionDelta && !force) {
if (sessionId) scheduleCompactionProjection(instanceId, sessionId, () => projectMessages(data, preserveOmitted, true))
return
}
if (sessionId && (force || event.type.startsWith("session.")) && (
@ -1992,10 +1942,14 @@ function handleInstanceInvalidation(instanceId: string, event: Parameters<NonNul
}
}
}
const data = applyOpenCodeDataEvent(instanceId, instance.folder, event, project, (next) => {
const data = applyOpenCodeDataEvent(instanceId, instance.folder, event, (next) => {
if (isCompactionDelta) projectMessages(next, true, true)
else project(next)
}, (next) => {
projectMessages(next, false, true)
schedulePendingRequestReconciliation(instanceId)
})
}, Boolean(sessionId && activeSessionId().get(instanceId) !== sessionId
&& !messageStoreBus.getInstance(instanceId)?.getSessionMessageIds(sessionId).length))
project(data)
if (sessionId && (event.type === "permission.asked" || event.type === "permission.replied")) {
const current = data.session.permission.list(sessionId) ?? []

View file

@ -0,0 +1,171 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { sdkManager } from "../lib/sdk-manager.ts"
import { sseManager } from "../lib/sse-manager.ts"
import {
applyOpenCodeDataEvent, destroyOpenCodeData, finishOpenCodeDataEvent,
getOpenCodeMessageRevision, invalidateOpenCodeSessionContent,
} from "./opencode-data.ts"
const model = { providerID: "fixture", id: "fixture" }
function fixture(instanceId: string) {
let sequence = 0, projections = 0
const client: any = {
session: { active: async () => ({}), get: async () => ({ id: "s" }),
inbox: { list: async () => [] }, form: { list: async () => [] } },
permission: { list: async () => [] },
message: { list: async () => ({ data: [], cursor: {} }) },
}
;(sdkManager as any).clients.set(`${instanceId}:/workspaces/${instanceId}/instance`, client)
let data!: ReturnType<typeof applyOpenCodeDataEvent>
const emit = (type: string, payload: any = {}) => {
data = applyOpenCodeDataEvent(instanceId, "/fixture", {
type, id: `event-${++sequence}`, created: sequence,
data: { sessionID: "s", ...payload }, location: { directory: "/fixture" },
} as any, () => { projections++ })
return data
}
return {
emit, client, projections: () => projections,
data: () => data,
start: (id = "compact") => emit("session.compaction.started", { inputID: id, reason: "manual", recent: "" }),
delta: (text: string) => emit("session.compaction.delta", { text }),
summary: (id = "compact") => (data.session.message.get("s", id) as any)?.summary,
cleanup: () => { destroyOpenCodeData(instanceId); sdkManager.destroyClientsForInstance(instanceId) },
}
}
test("compaction chunks are fenced immediately, joined once, and preserve same-session order", ctx => {
ctx.mock.timers.enable({ apis: ["setTimeout"] })
const f = fixture("compaction-order")
try {
f.start()
const revision = getOpenCodeMessageRevision("compaction-order", "s")
for (let index = 0; index < 128; index++) f.delta(String(index))
assert.equal(getOpenCodeMessageRevision("compaction-order", "s"), revision + 128)
assert.equal(f.summary(), "")
assert.equal(f.projections(), 0)
ctx.mock.timers.tick(250)
assert.equal(f.summary(), Array.from({ length: 128 }, (_, i) => String(i)).join(""))
assert.equal(f.projections(), 1)
f.delta("prefix")
f.start("second")
assert(f.summary()!.endsWith("prefix"), "second start flushes preceding running compaction")
f.delta("second text")
ctx.mock.timers.tick(250)
assert.equal(f.summary("second"), "second text")
assert.equal(f.projections(), 3)
f.delta("pending")
f.emit("session.compaction.ended", { reason: "manual", text: "final", recent: "" })
ctx.mock.timers.tick(500)
assert.equal(f.summary("second"), "final")
assert.equal(f.projections(), 3, "ended supersedes buffered fragments without replay")
} finally { f.cleanup() }
})
for (const boundary of ["failed", "delete", "reconnect", "prune", "revert", "dispose"] as const) {
test(`pending compaction text cannot survive ${boundary}`, ctx => {
ctx.mock.timers.enable({ apis: ["setTimeout"] })
const instanceId = `compaction-${boundary}`
const f = fixture(instanceId)
try {
f.start()
f.delta("must not reappear")
if (boundary === "failed") f.emit("session.compaction.failed", { reason: "manual", error: { type: "fixture", message: "failed" } })
if (boundary === "delete") f.emit("session.deleted")
if (boundary === "reconnect") f.emit("server.connected")
if (boundary === "prune") invalidateOpenCodeSessionContent(instanceId, "s")
if (boundary === "revert") f.emit("session.revert.committed", { to: "compact" })
if (boundary === "dispose") destroyOpenCodeData(instanceId)
ctx.mock.timers.tick(500)
assert.equal(f.projections(), 0)
if (boundary === "revert" || boundary === "delete") assert.equal(f.summary(), undefined)
f.start("fresh")
f.delta("new")
ctx.mock.timers.tick(250)
if (boundary !== "prune") assert.equal(f.summary("fresh"), "new")
} finally { f.cleanup() }
})
}
test("idle flushes pending compaction before retiring its reducer", ctx => {
ctx.mock.timers.enable({ apis: ["setTimeout"] })
const f = fixture("compaction-idle")
try {
f.start()
f.delta("last text")
f.emit("session.idle")
assert.equal(f.summary(), "last text")
finishOpenCodeDataEvent("compaction-idle", { type: "session.idle", data: { sessionID: "s" } } as any)
ctx.mock.timers.tick(500)
assert.equal(f.projections(), 1)
} finally { f.cleanup() }
})
test("same session IDs in distinct instances keep separate buffers and flush authority", ctx => {
ctx.mock.timers.enable({ apis: ["setTimeout"] })
const first = fixture("compaction-instance-first"), second = fixture("compaction-instance-second")
try {
first.start(); second.start()
first.delta("A"); second.delta("B")
first.emit("session.usage.updated", { cost: 0, tokens: {} })
assert.equal(first.summary(), "A")
assert.equal(second.summary(), "")
ctx.mock.timers.tick(250)
assert.equal(second.summary(), "B")
assert.equal(first.projections(), 1)
assert.equal(second.projections(), 1)
} finally { first.cleanup(); second.cleanup() }
})
test("SDK history reads flush pending chunks and reject a pre-delta page without duplication", async () => {
const instanceId = "compaction-native-read"
const previous = sseManager.getStatuses
sseManager.getStatuses = () => new Map([[instanceId, "connected"]])
const f = fixture(instanceId)
const replies: Array<(value: any) => void> = []
const page = (summary: string) => ({ data: [{ id: "compact", type: "compaction", status: "running",
reason: "manual", summary, recent: "", model, time: { created: 1 } }], cursor: {} })
f.client.message.list = () => new Promise(resolve => replies.push(resolve))
try {
const data = f.start()
const read = data.session.message.sync("s")
await new Promise(resolve => setImmediate(resolve))
assert.equal(replies.length, 1)
f.delta("A")
replies[0](page(""))
await new Promise(resolve => setImmediate(resolve))
assert.equal(replies.length, 2, "admitted buffered delta fences stale SDK read")
replies[1](page("A"))
await read
assert.equal(f.summary(), "A", "authoritative page must not double-append buffered text")
f.delta("B")
data.session.message.invalidate("s")
const nextRead = data.session.message.sync("s")
await new Promise(resolve => setImmediate(resolve))
assert.equal(f.summary(), "AB", "pending text is reduced before a new native read")
replies[2](page("AB"))
await nextRead
assert.equal(f.summary(), "AB")
assert.equal(f.projections(), 2)
} finally { f.cleanup(); sseManager.getStatuses = previous }
})
test("rotation at the 200-message boundary retains exactly one running compaction and its pending chunks", async () => {
const f = fixture("compaction-rotation")
try {
for (let index = 0; index < 200; index++) {
f.emit("session.step.started", { assistantMessageID: `m${index}`, agent: "build", model })
f.emit("session.step.ended", { assistantMessageID: `m${index}`, finish: "stop" })
}
f.start()
f.delta("A"); f.delta("B")
// Same-session terminal delivery forces native order while the start is
// still queued behind the bounded SDK snapshot rotation.
f.emit("session.usage.updated", { cost: 0, tokens: {} })
await new Promise(resolve => setImmediate(resolve))
assert.equal(f.summary(), "AB")
assert.equal(f.data().session.message.list("s").length, 200)
assert.equal(f.data().session.message.list("s").filter(message => message.type === "compaction").length, 1)
} finally { f.cleanup() }
})

View file

@ -8,12 +8,14 @@ import { MESSAGE_WINDOW_PAGE_SIZE } from "./message-v2/message-window"
import { messageStoreBus } from "./message-v2/bus"
import { sseManager } from "../lib/sse-manager"
import { getLogger } from "../lib/logger"
import { createCompactionDeltaBuffer } from "./compaction-delta-buffer"
const log = getLogger("session")
type DataEntry = {
data: Data
emit: (event: OpenCodeEvent) => void
fencePendingReads: (sessionId: string) => void
syncMessages: (sessionId: string, messages: SessionMessageInfo[], isCurrent: () => boolean) => Promise<boolean>
syncAuthoritative: (sessionId: string, isCurrent: () => boolean) => Promise<boolean>
dispose: () => void
@ -57,6 +59,18 @@ const messageRevisions = new Map<string, number>()
const fullDataRevisions = new Map<string, number>()
const instanceGenerations = new Map<string, number>()
const instanceDataRevisions = new Map<string, ReturnType<typeof createSignal<number>>>()
const unobservedCompactions = new Set<string>()
type CompactionDeltaContext = {
instanceId: string
directory: string
onDeferred?: (data: Data) => void
onResynced?: (data: Data) => void
}
const coalescedCompactionEvents = new WeakSet<object>()
const compactionDeltas = createCompactionDeltaBuffer<CompactionDeltaContext>((event, context) => {
coalescedCompactionEvents.add(event)
applyOpenCodeDataEvent(context.instanceId, context.directory, event, context.onDeferred, context.onResynced)
})
let nextInstanceGeneration = 0
function messageRevisionKey(instanceId: string, sessionId: string): string {
@ -130,6 +144,10 @@ function createDataEntry(instanceId: string, directory: string): DataEntry {
if (disposed || getOpenCodeInstanceGeneration(instanceId) !== instanceGeneration) {
throw new Error("Stale read from disposed OpenCode projection")
}
// A current native page can already contain buffered text.
// Reduce it before taking read authority, never append it
// again to a page that was fetched after delta admission.
compactionDeltas.flush(instanceId, input.sessionID)
const snapshot = messageSnapshots.get(input.sessionID)
if (snapshot) return { data: [...snapshot].reverse(), cursor: {} }
const revision = eventRevisions.get(input.sessionID) ?? 0
@ -137,6 +155,7 @@ function createDataEntry(instanceId: string, directory: string): DataEntry {
if (disposed || getOpenCodeInstanceGeneration(instanceId) !== instanceGeneration) {
throw new Error("Stale read from disposed OpenCode projection")
}
compactionDeltas.flush(instanceId, input.sessionID)
if ((eventRevisions.get(input.sessionID) ?? 0) !== revision) continue
return response
}
@ -167,6 +186,9 @@ function createDataEntry(instanceId: string, directory: string): DataEntry {
emit(details: OpenCodeEvent) {
emit(details)
},
fencePendingReads(sessionId: string) {
eventRevisions.set(sessionId, (eventRevisions.get(sessionId) ?? 0) + 1)
},
async syncMessages(sessionId: string, messages: SessionMessageInfo[], isCurrent: () => boolean) {
messageSnapshots.set(sessionId, messages)
try {
@ -573,6 +595,7 @@ export function applyOpenCodeDataEvent(
event: OpenCodeEvent,
onDeferred?: (data: Data) => void,
onResynced?: (data: Data) => void,
skipUnobservedCompaction = false,
): Data {
if (event.type === "server.connected") destroyOpenCodeData(instanceId)
const primary = ensureData(instanceId, directory)
@ -581,6 +604,20 @@ export function applyOpenCodeDataEvent(
// with sync:false, feeding these events to createData starts HTTP refreshes.
if (!/^(session|permission|form)\./.test(event.type)) return primary.data
const sessionId = eventSessionId(event)
const coalesced = coalescedCompactionEvents.delete(event)
if (typeof sessionId === "string" && !coalesced) {
if (event.type === "session.compaction.ended" || event.type === "session.compaction.failed"
|| event.type === "session.deleted" || event.type === "session.revert.committed") {
// Terminal events replace summary text. Destructive events must not let
// a delayed fragment resurrect a removed or invalidated compaction.
compactionDeltas.cancel(instanceId, sessionId)
unobservedCompactions.delete(messageRevisionKey(instanceId, sessionId))
} else if (event.type !== "session.compaction.delta") {
// A second start or another same-session event observes preceding text
// in native order; unrelated sessions never force a flush.
compactionDeltas.flush(instanceId, sessionId)
}
}
if (event.type === "session.deleted" && typeof sessionId === "string") {
const key = messageRevisionKey(instanceId, sessionId)
const transcript = transcriptEntries.get(key)
@ -595,24 +632,49 @@ export function applyOpenCodeDataEvent(
fullDataRevisions.delete(key)
return transcript?.entry.data ?? primary.data
}
if (typeof sessionId === "string" && !coalesced) {
const key = messageRevisionKey(instanceId, sessionId)
fullDataRevisions.set(key, (fullDataRevisions.get(key) ?? 0) + 1)
if (eventAffectsMessages(event)) messageRevisions.set(key, (messageRevisions.get(key) ?? 0) + 1)
if (event.type === "session.inbox.cancelled" || event.type === "session.revert.committed") bumpMutationRevision(key)
if (skipUnobservedCompaction && event.type.startsWith("session.compaction.") && !transcriptEntries.has(key)) {
// No native reducer/visible transcript is observing this session. The
// next activation loads authoritative messages instead of storing deltas.
if (event.type === "session.compaction.started") unobservedCompactions.add(key)
if (event.type === "session.compaction.ended" || event.type === "session.compaction.failed") unobservedCompactions.delete(key)
return primary.data
}
}
const transcript = typeof sessionId === "string"
? ensureTranscript(instanceId, sessionId, directory)
: undefined
const entry = transcript?.entry ?? primary
if (transcript && typeof sessionId === "string") {
if (event.type === "session.compaction.started") unobservedCompactions.delete(messageRevisionKey(instanceId, sessionId))
// A new execution must not inherit a preceding idle event's deferred cleanup.
if (event.type === "session.execution.started" || eventMayAppendMessage(event)) transcript.retireWhenDrained = false
const key = messageRevisionKey(instanceId, sessionId)
fullDataRevisions.set(key, (fullDataRevisions.get(key) ?? 0) + 1)
if (eventAffectsMessages(event)) messageRevisions.set(key, (messageRevisions.get(key) ?? 0) + 1)
if (event.type === "session.inbox.cancelled" || event.type === "session.revert.committed") bumpMutationRevision(key)
}
if (transcript && typeof sessionId === "string") {
if (onResynced) transcript.onResynced = onResynced
if (event.type === "session.compaction.delta" && unobservedCompactions.delete(messageRevisionKey(instanceId, sessionId))) {
// Activation during a previously unobserved compaction has no start row
// in the SDK reducer. Recover its exact native summary instead of making
// up a start or appending a delta to a REST page that already includes it.
transcript.preserveNativePageOnResync = true
collapseTranscriptQueue(instanceId, sessionId, transcript)
return transcript.entry.data
}
if (transcript.needsAuthoritativeResync || transcript.resyncing) {
collapseTranscriptQueue(instanceId, sessionId, transcript)
return transcript.entry.data
}
if (event.type === "session.compaction.delta" && !coalesced) {
// Admission fences both CodeNomad and SDK reads immediately, even though
// the reactive payload is reduced only once per interval.
entry.fencePendingReads(sessionId)
compactionDeltas.push(instanceId, sessionId, event, { instanceId, directory, onDeferred, onResynced })
return entry.data
}
if (transcript.rotating
|| (eventMayAppendMessage(event, transcript.entry.data.session.message.list(sessionId)
.filter((message) => message.type === "compaction" && message.status === "running").length)
@ -623,6 +685,7 @@ export function applyOpenCodeDataEvent(
}
}
entry.emit(event)
if (coalesced) onDeferred?.(entry.data)
return entry.data
}
@ -631,6 +694,8 @@ export function getOpenCodeMessageRevision(instanceId: string, sessionId: string
}
export function invalidateOpenCodeSessionContent(instanceId: string, sessionId: string): void {
compactionDeltas.cancel(instanceId, sessionId)
unobservedCompactions.delete(messageRevisionKey(instanceId, sessionId))
const key = messageRevisionKey(instanceId, sessionId)
bumpMutationRevision(key)
messageRevisions.set(key, (messageRevisions.get(key) ?? 0) + 1)
@ -720,6 +785,8 @@ export function finishOpenCodeDataEvent(instanceId: string, event: OpenCodeEvent
export function destroyOpenCodeData(instanceId: string, sessionId?: string): void {
if (sessionId !== undefined) {
compactionDeltas.cancel(instanceId, sessionId)
unobservedCompactions.delete(messageRevisionKey(instanceId, sessionId))
const key = messageRevisionKey(instanceId, sessionId)
const transcript = transcriptEntries.get(key)
if (transcript) {
@ -734,6 +801,10 @@ export function destroyOpenCodeData(instanceId: string, sessionId?: string): voi
instanceDataRevision(instanceId)[1]((current) => current + 1)
return
}
compactionDeltas.clear(instanceId)
for (const key of unobservedCompactions) {
if (key.startsWith(`${instanceId}\0`)) unobservedCompactions.delete(key)
}
instanceGenerations.set(instanceId, ++nextInstanceGeneration)
entries.get(instanceId)?.dispose()
entries.delete(instanceId)

View file

@ -13,9 +13,22 @@ test("renderer SSE reconnect invalidates every cached worktree without reading h
sources.push(source)
return source
})
// This fixture models a web renderer, not the intentionally passive Node
// import. No native bridge or network globals are needed by the mock above.
const windowDescriptor = Object.getOwnPropertyDescriptor(globalThis, "window")
t.after(() => {
sources.forEach((source) => source.close())
if (windowDescriptor) Object.defineProperty(globalThis, "window", windowDescriptor)
else Reflect.deleteProperty(globalThis, "window")
assert.deepEqual(Object.getOwnPropertyDescriptor(globalThis, "window"), windowDescriptor)
})
Object.defineProperty(globalThis, "window", { configurable: true, value: {} })
const { serverEvents } = await import("../lib/server-events")
await import("./plugin-controls-events")
await tick()
assert.equal(sources.length, 1, "the renderer auto-connects through the mocked browser transport")
assert.equal(sources[0].closed, false)
assert.equal(typeof sources[0].onopen, "function")
sources[0].onopen!()
let nativeStatuses = 0
t.after(serverEvents.on("instance.eventStatus", () => { nativeStatuses++ }))

View file

@ -0,0 +1,145 @@
import assert from "node:assert/strict"
import { after, before, test } from "node:test"
import { fileURLToPath } from "node:url"
import { readFile } from "node:fs/promises"
import { chromium, type Browser, type Page } from "playwright"
import { createServer, type ViteDevServer } from "vite"
import solid from "vite-plugin-solid"
let server: ViteDevServer, browser: Browser, url: string
before(async () => {
server = await createServer({ configFile: false, root: fileURLToPath(new URL("../..", import.meta.url)), logLevel: "error",
plugins: [solid(), {
name: "compaction-counts",
enforce: "pre",
async transform(source, id) {
if (id.endsWith("/stores/instances.ts") && process.env.CODENOMAD_COMPACTION_BASELINE) {
return readFile(`${process.env.CODENOMAD_COMPACTION_BASELINE}/instances.ts`, "utf8")
}
if (!id.endsWith("/stores/opencode-data.ts")) return
if (process.env.CODENOMAD_COMPACTION_BASELINE) source = await readFile(`${process.env.CODENOMAD_COMPACTION_BASELINE}/opencode-data.ts`, "utf8")
// Observe the actual native reducer boundary, not a reimplemented reducer.
return source.replace("for (const listener of listeners)", `
if (details.type === "session.compaction.delta") {
const counts = (window as any).compactionCounts?.deltas
if (counts) counts[details.data.sessionID] = (counts[details.data.sessionID] ?? 0) + 1
}
for (const listener of listeners)`)
.replace("const source = data.session.message.list(sessionId)", `
const counts = (window as any).compactionCounts?.projections
if (counts) counts[sessionId] = (counts[sessionId] ?? 0) + 1
const source = data.session.message.list(sessionId)`)
},
configureServer(vite) {
vite.middlewares.use("/compaction", async (_req, res) => {
res.setHeader("Content-Type", "text/html")
res.end(await vite.transformIndexHtml("/compaction", '<html><body><div id="root" style="display:flex;width:1100px;height:740px"></div><script type="module" src="/tests/browser/fixtures/compaction-responsiveness.tsx"></script></body></html>'))
})
},
}], resolve: { dedupe: ["solid-js"] }, optimizeDeps: { exclude: ["lucide-solid"] },
server: { host: "127.0.0.1", port: 0, hmr: false, watch: null },
})
await server.listen()
url = `http://127.0.0.1:${(server.httpServer!.address() as { port: number }).port}/compaction`
browser = await chromium.launch({ executablePath: process.env.CODENOMAD_BROWSER_PATH || undefined })
})
after(async () => { await browser?.close(); await server?.close() })
async function fixture(run: (page: Page) => Promise<void>) {
const page = await browser.newPage({ viewport: { width: 1200, height: 800 } })
const errors: string[] = []
page.on("pageerror", error => errors.push(error.message))
await page.route("**/*", route => {
const request = new URL(route.request().url())
if (request.hostname !== "127.0.0.1") return route.abort()
if (request.pathname.startsWith("/api/")) return route.fulfill({ json: {} })
return route.continue()
})
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30_000 })
await page.waitForFunction(() => Boolean((window as any).compactionFixture), undefined, { timeout: 30_000 })
await page.waitForTimeout(300)
await run(page)
assert.deepEqual(errors, [])
} finally { await page.close() }
}
const snapshot = (page: Page) => page.evaluate(() => (window as any).compactionFixture.snapshot())
const joined = (count: number) => Array.from({ length: count }, (_, index) => `[${index}]`).join("")
test("128 active deltas reduce and project once, preserve immediate revision, and end authoritatively", () => fixture(async page => {
await page.evaluate(() => { const f = (window as any).compactionFixture; f.start(); f.resetCounts() })
const before = await snapshot(page)
const dispatchMs = await page.evaluate(() => (window as any).compactionFixture.burst("active"))
const pending = await snapshot(page)
console.log(JSON.stringify({ scenario: "active128-ingress", dispatchMs, counts: pending.counts }))
assert.equal(pending.revision - before.revision, 128)
assert.equal(pending.counts.deltas.active ?? 0, 0)
assert.equal(pending.counts.projections.active ?? 0, 0)
await page.waitForFunction(() => (window as any).compactionFixture.snapshot().active?.length > 0)
const flushed = await snapshot(page)
assert.equal(flushed.active, joined(128))
assert.equal(flushed.counts.deltas.active, 1)
assert.equal(flushed.counts.projections.active, 1)
assert.equal(flushed.messageLists, 0)
console.log(JSON.stringify({ scenario: "active128", dispatchMs, counts: flushed.counts }))
await page.evaluate(() => { const f = (window as any).compactionFixture; f.burst("active", 4); f.end("active", "authoritative final") })
await page.waitForTimeout(300)
const final = await snapshot(page)
assert.equal(final.active, "authoritative final")
assert.equal(final.counts.deltas.active, 1, "terminal final text cancels pending fragments")
}))
test("spaced native events coalesce per interval without losing chunks or affecting the other session", () => fixture(async page => {
await page.evaluate(() => { const f = (window as any).compactionFixture; f.start(); f.resetCounts() })
for (let phase = 1; phase <= 2; phase++) {
await page.evaluate(() => (window as any).compactionFixture.phase("active"))
await page.waitForFunction(phase => (window as any).compactionFixture.snapshot().active ===
Array.from({ length: 8 }, (_, index) => `[${index}]`).join("").repeat(phase), phase)
const state = await snapshot(page)
assert.equal(state.counts.deltas.active, phase)
assert.equal(state.counts.projections.active, phase)
assert.equal(state.messageLists, 0)
}
}))
test("never-loaded inactive compaction allocates no reducer payload; activation and remount hydrate exact final text", () => fixture(async page => {
await page.evaluate(() => { const f = (window as any).compactionFixture; f.resetCounts(); f.start("inactive"); f.burst("inactive"); f.end("inactive", "saved summary") })
await page.waitForTimeout(300)
const inactive = await snapshot(page)
assert.deepEqual(inactive.counts, { deltas: {}, projections: {} })
assert.equal(inactive.inactive, undefined)
assert.equal(inactive.messageLists, 0)
await page.evaluate(() => (window as any).compactionFixture.activate("inactive"))
await page.waitForFunction(() => (window as any).compactionFixture.snapshot().inactive === "saved summary")
await page.evaluate(() => (window as any).compactionFixture.unmount())
await page.evaluate(() => (window as any).compactionFixture.remount())
await page.waitForFunction(() => (window as any).compactionFixture.snapshot().inactive === "saved summary")
assert.equal((await snapshot(page)).active, undefined)
}))
test("activation during an unobserved compaction hydrates then resumes exact streaming summary", () => fixture(async page => {
await page.evaluate(() => { const f = (window as any).compactionFixture; f.seed("inactive", 199); f.start("inactive"); f.burst("inactive", 4); f.activate("inactive") })
await page.waitForFunction(() => (window as any).compactionFixture.snapshot().inactive === "[0][1][2][3]")
await page.evaluate(() => (window as any).compactionFixture.burst("inactive", 2))
await page.waitForFunction(() => (window as any).compactionFixture.snapshot().inactive === "[0][1][2][3][0][1]")
await page.evaluate(() => { const f = (window as any).compactionFixture; f.resetCounts(); f.burst("inactive", 2) })
await page.waitForFunction(() => (window as any).compactionFixture.snapshot().inactive === "[0][1][2][3][0][1][0][1]")
const resumed = await snapshot(page)
assert.equal(resumed.counts.deltas.inactive, 1)
assert.equal(resumed.counts.projections.inactive, 1)
assert.equal(resumed.messageLists, 0)
assert.equal(resumed.ids.length, 200, "SDK's 20-row recovery must retain the hydrated native 200-row window")
}))
test("a loaded but inactive reducer batches text, does not hydrate its hidden view, and returns without duplication", () => fixture(async page => {
await page.evaluate(() => { const f = (window as any).compactionFixture; f.start(); f.activate("inactive"); f.resetCounts(); f.burst("active", 4) })
await page.waitForTimeout(300)
const hidden = await snapshot(page)
assert.equal(hidden.counts.deltas.active, 1)
assert.equal(hidden.counts.projections.active ?? 0, 0)
assert.equal(hidden.active, "", "hidden visible snapshot is revalidated, not hydrated by every delta")
await page.evaluate(() => (window as any).compactionFixture.activate("active"))
await page.waitForFunction(() => (window as any).compactionFixture.snapshot().active === "[0][1][2][3]")
await page.evaluate(() => (window as any).compactionFixture.burst("active", 2))
await page.waitForFunction(() => (window as any).compactionFixture.snapshot().active === "[0][1][2][3][0][1]")
}))

View file

@ -0,0 +1,172 @@
import assert from "node:assert/strict"
import { after, before, test } from "node:test"
import { chromium, type Browser, type Route } from "playwright"
import type { ViteDevServer } from "vite"
import { startDeviceUploadFixture } from "./fixtures/device-upload-server.mjs"
let server: ViteDevServer, browser: Browser, url: string
before(async () => {
;({ server, url } = await startDeviceUploadFixture())
browser = await chromium.launch({ executablePath: process.env.CODENOMAD_BROWSER_PATH || undefined })
})
after(async () => { await browser?.close(); await server?.close() })
async function setup(submitOnEnter = true) {
const page = await browser.newPage({ viewport: { width: 1100, height: 800 }, locale: "en-US",
permissions: ["clipboard-read", "clipboard-write"] })
const prompts: unknown[] = [], searches: Route[] = [], errors: string[] = []
page.on("pageerror", error => errors.push(error.message))
await page.addInitScript(() => {
;(window as any).__CODENOMAD_RUNTIME_HOST__ = "tauri"
;(window as any).__CODENOMAD_WINDOW_CONTEXT__ = "local"
})
await page.route("**/api/**", route => {
const target = new URL(route.request().url())
if (target.pathname.endsWith("/prompt")) {
prompts.push(route.request().postDataJSON())
return route.fulfill({ json: { data: {} } })
}
if (/\/api\/workspaces\/[^/]+\/files(?:\/search)?$/.test(target.pathname)) {
searches.push(route)
return
}
return route.fulfill({ json: {} })
})
await page.goto(url)
await page.waitForFunction(() => Boolean((window as any).fixture))
await page.evaluate(value => (window as any).fixture.submitOnEnter(value), submitOnEnter)
const input = page.locator("textarea:visible").first()
const picker = page.locator(".dropdown-surface")
const waitSearch = async (count: number) => {
const deadline = Date.now() + 5000
while (searches.length < count && Date.now() < deadline) await page.waitForTimeout(10)
assert.equal(searches.length, count)
}
const assertDraft = async (draft: string) => {
await page.waitForTimeout(50)
assert.equal(await input.inputValue(), draft)
assert.deepEqual(prompts, [], "no native prompt may be dispatched by picker keys")
assert.deepEqual(errors, [])
}
return { page, input, picker, searches, prompts, errors, waitSearch, assertDraft }
}
for (const submitOnEnter of [true, false]) {
test(`empty and pending mention results never send, including modifiers (submitOnEnter=${submitOnEnter})`, async () => {
const f = await setup(submitOnEnter)
try {
await f.input.fill("@missing/path")
await f.picker.waitFor()
await f.waitSearch(1)
await f.input.press("Enter")
await f.assertDraft("@missing/path")
await f.searches[0].fulfill({ json: [] })
await f.page.getByText("No results found", { exact: true }).waitFor()
for (const key of ["Enter", "Shift+Enter", "Control+Enter", "Meta+Enter", "Control+Shift+Enter", "Meta+Shift+Enter"]) {
await f.input.press(key)
await f.assertDraft("@missing/path")
assert.equal(await f.picker.count(), 1)
}
} finally { await f.page.close() }
})
}
test("Ctrl+V extends an ignored picker query without sending; empty-result Enter still cannot send", async () => {
const f = await setup()
try {
await f.input.fill("@missing")
await f.picker.waitFor(); await f.waitSearch(1)
await f.searches[0].fulfill({ json: [] })
await f.page.evaluate(() => navigator.clipboard.writeText("/pasted/path"))
await f.input.press("Control+v")
await f.waitSearch(2)
await f.assertDraft("@missing/pasted/path")
await f.searches[1].fulfill({ json: [] })
await f.input.press("Enter")
await f.assertDraft("@missing/pasted/path")
await f.input.press("Escape")
await f.picker.waitFor({ state: "detached" })
await f.assertDraft("@missing/pasted/path")
await f.input.press("Enter")
await f.page.waitForFunction(() => document.querySelector<HTMLTextAreaElement>("textarea")?.value === "")
assert.equal(f.prompts.length, 1, "explicit submission after dismissal remains available")
assert.deepEqual(f.errors, [])
} finally { await f.page.close() }
})
test("Tab autocomplete and Enter attach a selected file without sending; the next Enter sends", async () => {
const f = await setup()
try {
await f.input.fill("@server")
await f.picker.waitFor(); await f.waitSearch(1)
await f.searches[0].fulfill({ json: [{ name: "server.txt", path: "server.txt", type: "file" }] })
await f.page.getByText("server.txt", { exact: true }).last().waitFor()
await f.input.press("Tab")
await f.assertDraft("@server.txt")
await f.waitSearch(2)
await f.searches[1].fulfill({ json: [{ name: "server.txt", path: "server.txt", type: "file" }] })
await f.input.press("Enter")
await f.picker.waitFor({ state: "detached" })
await f.assertDraft("@./server.txt ")
assert.equal(await f.page.evaluate(() => (window as any).fixture.attachments().length), 1)
await f.input.press("Enter")
await f.page.waitForFunction(() => (window as any).fixture.attachments().length === 0)
assert.equal(f.prompts.length, 1)
assert.deepEqual(f.errors, [])
} finally { await f.page.close() }
})
test("an empty command picker swallows Enter rather than submitting the command text", async () => {
const f = await setup()
try {
await f.input.fill("/unknown-command")
await f.picker.waitFor()
await f.input.press("Enter")
await f.assertDraft("/unknown-command")
await f.input.press("Control+Shift+Enter")
await f.assertDraft("/unknown-command")
} finally { await f.page.close() }
})
test("modified Enter cannot send a highlighted file; Shift+Enter retains path-only selection", async () => {
const f = await setup(false)
try {
await f.input.fill("@server")
await f.picker.waitFor(); await f.waitSearch(1)
await f.searches[0].fulfill({ json: [{ name: "server.txt", path: "server.txt", type: "file" }] })
await f.page.getByText("server.txt", { exact: true }).last().waitFor()
for (const key of ["Control+Enter", "Control+Shift+Enter", "Meta+Enter", "Meta+Shift+Enter"]) {
await f.input.press(key)
await f.assertDraft("@server")
assert.equal(await f.page.evaluate(() => (window as any).fixture.attachments().length), 0)
}
await f.input.press("Shift+Enter")
await f.picker.waitFor({ state: "detached" })
await f.assertDraft("@./server.txt ")
assert.deepEqual(await f.page.evaluate(() => (window as any).fixture.attachments().map((a: any) => a.source)),
[{ type: "text", value: "./server.txt" }])
} finally { await f.page.close() }
})
test("directory Tab navigation and arrow selection remain available without sending", async () => {
const f = await setup()
try {
await f.input.fill("@docs")
await f.picker.waitFor(); await f.waitSearch(1)
await f.searches[0].fulfill({ json: [{ name: "docs", path: "docs", type: "directory" }] })
await f.page.locator('[data-picker-selected="true"]').waitFor()
await f.input.press("Tab")
await f.assertDraft("@docs/")
await f.waitSearch(2)
await f.searches[1].fulfill({ json: [
{ name: "a.txt", path: "docs/a.txt", type: "file" },
{ name: "b.txt", path: "docs/b.txt", type: "file" },
] })
await f.page.getByText("b.txt", { exact: true }).last().waitFor()
await f.input.press("ArrowDown")
await f.input.press("Enter")
await f.picker.waitFor({ state: "detached" })
await f.assertDraft("@./docs/b.txt ")
assert.equal(await f.page.evaluate(() => (window as any).fixture.attachments()[0].filename), "b.txt")
} finally { await f.page.close() }
})

View file

@ -0,0 +1,140 @@
import assert from "node:assert/strict"
import { after, before, test } from "node:test"
import { fileURLToPath } from "node:url"
import Fastify, { type FastifyInstance } from "fastify"
import pino from "pino"
import { chromium, type Browser, type Page } from "playwright"
import { createServer, type ViteDevServer } from "vite"
import solid from "vite-plugin-solid"
import { EventBus } from "../../../server/src/events/bus"
import { ClientConnectionManager } from "../../../server/src/clients/connection-manager"
import { registerEventRoutes } from "../../../server/src/server/routes/events"
let backend: FastifyInstance, server: ViteDevServer, browser: Browser, connections: ClientConnectionManager, url: string
const bus = new EventBus()
const clients = new Set<() => void>()
before(async () => {
const logger = pino({ level: "silent" })
// Send a body frame immediately so the Vite proxy forwards idle stream headers.
bus.publish({ type: "instance.eventStatus", instanceId: "fixture", status: "connected", generation: 1 })
connections = new ClientConnectionManager(logger)
backend = Fastify()
registerEventRoutes(backend, { eventBus: bus, logger, connectionManager: connections,
registerClient: close => { clients.add(close); return () => { clients.delete(close) } },
})
backend.get("/api/auth/status", async () => ({ authenticated: true }))
await backend.listen({ host: "127.0.0.1", port: 0 })
const target = `http://127.0.0.1:${(backend.server.address() as { port: number }).port}`
server = await createServer({ configFile: false, root: fileURLToPath(new URL("../..", import.meta.url)), logLevel: "error",
plugins: [solid(), { name: "event-subscriber-isolation", configureServer(s) {
s.middlewares.use("/fixture", async (_request, response) => {
response.setHeader("Content-Type", "text/html")
response.end(await s.transformIndexHtml("/fixture", '<html><body><div id="root"></div><script type="module" src="/tests/browser/fixtures/event-subscriber-isolation.tsx"></script></body></html>'))
})
} }], resolve: { dedupe: ["solid-js"] },
server: { host: "127.0.0.1", port: 0, hmr: false, watch: null, proxy: { "/api": { target } } },
})
await server.listen()
url = `http://127.0.0.1:${(server.httpServer!.address() as { port: number }).port}/fixture`
browser = await chromium.launch({ executablePath: process.env.CODENOMAD_BROWSER_PATH || undefined })
})
after(async () => {
await browser?.close()
connections?.shutdown()
for (const close of clients) close()
await server?.close()
await backend?.close()
})
function emit(id: string) {
bus.publish({ type: "instance.event", instanceId: "fixture", event: {
id, created: Date.now(), type: "session.renamed", data: { sessionID: "session", title: id },
durable: { aggregateID: "session", seq: 1, version: 1 }, location: { directory: "/fixture" },
} })
}
async function setup(page: Page, mode: string) {
const pageErrors: string[] = []
page.on("pageerror", error => pageErrors.push(error.message))
await page.goto(`${url}?mode=${mode}`)
await page.locator("[data-events]").waitFor()
await page.waitForFunction(() => (window as any).fixture.snapshot().statuses.includes("connected"))
return pageErrors
}
test("a failing wildcard subscriber cannot discard a valid SSE event before native and typed subscribers", async () => {
const page = await browser.newPage()
try {
const pageErrors = await setup(page, "event")
emit("first")
emit("second")
await page.waitForFunction(() => (window as any).fixture.snapshot().faults === 2)
const state = await page.evaluate(() => (window as any).fixture.snapshot())
assert.deepEqual(state.native, ["first", "second"], "real SSEManager must receive both events despite another subscriber's error")
assert.deepEqual(state.typed, ["first", "second"])
assert.equal(state.errors.length, 2, "each failed callback is diagnosed, without replaying the event")
assert.doesNotMatch(JSON.stringify(state.errors), /Failed to parse event/)
assert.deepEqual(pageErrors, [])
} finally { await page.close() }
})
test("a deferred Solid derivation error is diagnosed as dispatch, not malformed SSE, without replay", async () => {
const page = await browser.newPage()
try {
const pageErrors = await setup(page, "derived")
emit("first")
await page.waitForFunction(() => (window as any).fixture.snapshot().errors.length === 1)
emit("second")
await page.waitForFunction(() => (window as any).fixture.snapshot().native.length === 2)
const state = await page.evaluate(() => (window as any).fixture.snapshot())
assert.deepEqual(state.native, ["first", "second"])
assert.deepEqual(state.typed, ["first", "second"])
assert.equal(state.errors[0][0], "Failed to dispatch event")
assert.doesNotMatch(JSON.stringify(state.errors), /Failed to parse event/)
assert.deepEqual(pageErrors, [])
} finally { await page.close() }
})
test("a failing open subscriber cannot suppress later reconnect reconciliation subscribers", async () => {
const page = await browser.newPage()
try {
const pageErrors = await setup(page, "open")
await page.waitForFunction(() => (window as any).fixture.snapshot().faults === 1)
assert.equal(await page.evaluate(() => (window as any).fixture.snapshot().opens), 1)
assert.equal(await page.evaluate(() => (window as any).fixture.restart()), null)
await page.waitForFunction(() => (window as any).fixture.snapshot().faults === 2)
assert.equal(await page.evaluate(() => (window as any).fixture.snapshot().opens), 2)
assert.deepEqual(pageErrors, [])
} finally { await page.close() }
})
test("a failing status subscriber cannot abort restart or prevent later transport-status notifications", async () => {
const page = await browser.newPage()
try {
const pageErrors = await setup(page, "status")
assert.equal(await page.evaluate(() => (window as any).fixture.restart()), null, "restart must still close and replace its connection")
await page.waitForFunction(() => (window as any).fixture.snapshot().opens === 2)
const state = await page.evaluate(() => (window as any).fixture.snapshot())
assert.ok(state.statuses.includes("disconnected"))
emit("after-restart")
await page.waitForFunction(() => (window as any).fixture.snapshot().native.length === 1)
assert.deepEqual(await page.evaluate(() => (window as any).fixture.snapshot().native), ["after-restart"])
assert.deepEqual(pageErrors, [])
} finally { await page.close() }
})
test("normal subscribers retain SSE FIFO and unsubscribe removes only its own listener", async () => {
const page = await browser.newPage()
try {
const pageErrors = await setup(page, "normal")
emit("first")
await page.waitForFunction(() => (window as any).fixture.snapshot().typed.length === 1)
await page.evaluate(() => (window as any).fixture.unsubscribe())
emit("second")
await page.waitForFunction(() => (window as any).fixture.snapshot().native.length === 2)
const state = await page.evaluate(() => (window as any).fixture.snapshot())
assert.deepEqual(state.native, ["first", "second"])
assert.deepEqual(state.typed, ["first"])
assert.deepEqual(state.errors, [])
assert.deepEqual(pageErrors, [])
} finally { await page.close() }
})

View file

@ -0,0 +1,94 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { runWithDiagnosticCleanup } from "./fixture-diagnostic-boundary"
async function capture(run: () => Promise<unknown>) {
let caught = false
let thrown: unknown
try {
await run()
} catch (error) {
caught = true
thrown = error
}
return { caught, thrown }
}
test("diagnostic and cleanup failures preserve the primary error identity and stack", async () => {
const primary = new Error("primary")
const stack = primary.stack
const observation = new Error("observation")
const cleanup = new Error("cleanup")
const observations: unknown[] = []
const cleanups: unknown[] = []
const result = await capture(() => runWithDiagnosticCleanup({
run: async () => { throw primary },
diagnose: async () => { throw observation },
cleanup: async () => { throw cleanup },
onObservationError: error => observations.push(error),
onCleanupError: error => cleanups.push(error),
}))
assert.equal(result.caught, true)
assert.equal(result.thrown, primary)
assert.equal((result.thrown as Error).stack, stack)
assert.deepEqual(observations, [observation])
assert.deepEqual(cleanups, [cleanup])
})
test("a successful snapshot and failed cleanup still preserve the primary error", async () => {
const primary = new Error("primary")
const cleanup = new Error("cleanup")
let diagnosed = false
const cleanups: unknown[] = []
const result = await capture(() => runWithDiagnosticCleanup({
run: async () => { throw primary },
diagnose: async () => { diagnosed = true },
cleanup: async () => { throw cleanup },
onObservationError: () => assert.fail("unexpected observation error"),
onCleanupError: error => cleanups.push(error),
}))
assert.equal(diagnosed, true)
assert.equal(result.thrown, primary)
assert.deepEqual(cleanups, [cleanup])
})
test("falsy primary errors are preserved when cleanup fails", async () => {
for (const primary of [undefined, null]) {
const cleanup = new Error("cleanup")
const cleanups: unknown[] = []
const result = await capture(() => runWithDiagnosticCleanup({
run: async () => { throw primary },
diagnose: async () => {},
cleanup: async () => { throw cleanup },
onObservationError: () => assert.fail("unexpected observation error"),
onCleanupError: error => cleanups.push(error),
}))
assert.equal(result.caught, true)
assert.equal(result.thrown, primary)
assert.deepEqual(cleanups, [cleanup])
}
})
test("cleanup failure after success remains the test failure", async () => {
const cleanup = new Error("cleanup")
const result = await capture(() => runWithDiagnosticCleanup({
run: async () => "success",
diagnose: async () => assert.fail("diagnostics must not run after success"),
cleanup: async () => { throw cleanup },
onObservationError: () => assert.fail("unexpected observation error"),
onCleanupError: () => assert.fail("cleanup must propagate after success"),
}))
assert.equal(result.caught, true)
assert.equal(result.thrown, cleanup)
})
test("success and cleanup success preserve the result", async () => {
const result = await runWithDiagnosticCleanup({
run: async () => "success",
diagnose: async () => assert.fail("diagnostics must not run after success"),
cleanup: async () => {},
onObservationError: () => assert.fail("unexpected observation error"),
onCleanupError: () => assert.fail("unexpected cleanup error"),
})
assert.equal(result, "success")
})

View file

@ -0,0 +1,29 @@
export async function runWithDiagnosticCleanup<T>(options: {
run: () => Promise<T>
diagnose: () => Promise<void>
cleanup: () => Promise<void>
onObservationError: (error: unknown) => void
onCleanupError: (error: unknown) => void
}): Promise<T> {
let primaryFailed = false
try {
try {
return await options.run()
} catch (error) {
primaryFailed = true
try {
await options.diagnose()
} catch (observationError) {
try { options.onObservationError(observationError) } catch {}
}
throw error
}
} finally {
try {
await options.cleanup()
} catch (cleanupError) {
if (!primaryFailed) throw cleanupError
try { options.onCleanupError(cleanupError) } catch {}
}
}
}

View file

@ -0,0 +1,103 @@
import { createSignal, Show } from "solid-js"
import { render } from "solid-js/web"
import SessionView from "../../../src/components/session/session-view"
import { ConfigProvider } from "../../../src/stores/preferences"
import { I18nProvider } from "../../../src/lib/i18n"
import { ThemeProvider } from "../../../src/lib/theme"
import { sdkManager } from "../../../src/lib/sdk-manager"
import { addInstance } from "../../../src/stores/instances"
import { sessions, setSessions, setActiveSession, setProviders } from "../../../src/stores/session-state"
import { messageStoreBus } from "../../../src/stores/message-v2/bus"
import { sseManager } from "../../../src/lib/sse-manager"
import { getOpenCodeMessageRevision } from "../../../src/stores/opencode-data"
import "../../../src/index.css"
const instanceId = "compaction-fixture", model = { providerID: "fixture", id: "fixture" }
let sequence = 1000, messageLists = 0
const nativeMessages = new Map<string, any[]>()
const [selected, select] = createSignal("active")
const [visible, show] = createSignal(true)
const client: any = {
session: {
active: async () => ({}), inbox: { list: async () => [] },
form: { list: async () => [] },
get: async ({ sessionID }: any) => ({ id: sessionID, title: sessionID,
location: { directory: "/fixture" }, time: { created: 1, updated: 1 } }),
instructions: { entry: { remove: async () => {}, put: async () => {} } },
},
model: { default: async () => ({ data: model }) },
permission: { list: async () => [] },
message: { list: async ({ sessionID, limit = 200 }: any) => {
messageLists++
return { data: structuredClone((nativeMessages.get(sessionID) ?? []).slice(-limit)).reverse(), cursor: {} }
} },
}
;(sdkManager as any).clients.set(`${instanceId}:/workspaces/${instanceId}/instance`, client)
addInstance({ id: instanceId, folder: "/fixture", port: 0, pid: 0, proxyPath: "", status: "ready", client })
setSessions(previous => new Map(previous).set(instanceId, new Map(["active", "inactive"].map(id => [id, {
id, instanceId, parentId: null, title: id, agent: "build", model: { providerId: "fixture", modelId: "fixture" },
status: "idle", retry: null, idleSince: null, generationRecovery: null, runtimeStatusKnown: true,
version: "1", projectID: "fixture", location: { directory: "/fixture" }, cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: 1, updated: 1 },
}]))))
setProviders(previous => new Map(previous).set(instanceId, [{ id: "fixture", name: "Fixture", models: [
{ id: "fixture", name: "Fixture", providerId: "fixture", limit: { context: 10000, output: 1000 }, cost: { input: 0, output: 0 } },
] }]))
setActiveSession(instanceId, "active")
const store = messageStoreBus.getOrCreate(instanceId)
render(() => <ConfigProvider><I18nProvider><ThemeProvider><Show when={visible()}>
<SessionView sessionId={selected()} activeSessions={sessions().get(instanceId)!} instanceId={instanceId}
instanceFolder="/fixture" escapeInDebounce={false} isActive={true} />
</Show></ThemeProvider></I18nProvider></ConfigProvider>, document.getElementById("root")!)
const emit = (sessionID: string, type: string, data: any = {}) => {
if (type === "session.compaction.started") nativeMessages.set(sessionID, [...(nativeMessages.get(sessionID) ?? []), { id: `compact-${sessionID}`,
type: "compaction", status: "running", reason: "manual", summary: "", recent: "", time: { created: sequence } }])
if (type === "session.compaction.delta") nativeMessages.get(sessionID)!.at(-1).summary += data.text
;(sseManager as any).handleEvent(instanceId, {
id: `event_${++sequence}`, type, created: sequence, location: { directory: "/fixture" }, data: { sessionID, ...data },
})
}
const text = (sessionID: string) => {
const message = store.getMessage(`compact-${sessionID}`)
return message?.partIds.map(id => (message.parts[id].data as any).text).join("")
}
;(window as any).compactionFixture = {
resetCounts: () => { (window as any).compactionCounts = { deltas: {}, projections: {} }; messageLists = 0 },
seed: (sessionID: string, count: number) => nativeMessages.set(sessionID, Array.from({ length: count }, (_, index) => ({
id: `history-${sessionID}-${String(index).padStart(4, "0")}`, type: "user", text: `History ${index}`, time: { created: index },
}))),
snapshot: () => ({ counts: (window as any).compactionCounts, messageLists,
active: text("active"), inactive: text("inactive"), ids: store.getSessionMessageIds(selected()),
revision: getOpenCodeMessageRevision(instanceId, selected()) }),
start: (sessionID = "active") => {
emit(sessionID, "session.compaction.started", { inputID: `compact-${sessionID}`, reason: "manual", recent: "" })
},
burst: (sessionID: string, count = 128) => {
const started = performance.now()
for (let index = 0; index < count; index++) emit(sessionID, "session.compaction.delta", { text: `[${index}]` })
return performance.now() - started
},
phase: async (sessionID: string) => {
for (let index = 0; index < 8; index++) {
emit(sessionID, "session.compaction.delta", { text: `[${index}]` })
await new Promise(resolve => setTimeout(resolve, 10))
}
},
end: (sessionID: string, summary: string) => {
nativeMessages.set(sessionID, [{ id: `compact-${sessionID}`, type: "compaction", status: "completed",
reason: "manual", summary, recent: "", model, time: { created: 1 } }])
emit(sessionID, "session.compaction.ended", { reason: "manual", text: summary, recent: "", model })
},
fail: (sessionID: string) => emit(sessionID, "session.compaction.failed", {
inputID: `compact-${sessionID}`, reason: "manual", error: { type: "fixture", message: "cancelled" },
}),
activate: (sessionID: string) => {
show(false)
select(sessionID)
setActiveSession(instanceId, sessionID)
show(true)
},
unmount: () => show(false),
remount: () => show(true),
}

View file

@ -55,6 +55,7 @@ await updatePreferences({ locale: (params.get("locale") || "en") as any })
switch: (id: string) => setActiveSession(instanceId, id),
active: setActive,
mounted: setMounted,
submitOnEnter: (value: boolean) => updatePreferences({ promptSubmitOnEnter: value }),
move: (directory: string) => setSessions(previous => {
const next = new Map(previous), list = new Map(next.get(instanceId))
const id = activeSessionId().get(instanceId)!

View file

@ -0,0 +1,44 @@
import { createMemo, createRoot, createSignal } from "solid-js"
import { render } from "solid-js/web"
import { serverEvents } from "../../../src/lib/server-events"
import { sseManager } from "../../../src/lib/sse-manager"
import { getLogger } from "../../../src/lib/logger"
const mode = new URLSearchParams(location.search).get("mode")
const native: string[] = [], typed: string[] = [], statuses: string[] = [], errors: unknown[][] = []
let faults = 0, opens = 0
const [version, setVersion] = createSignal(0)
const changed = () => setVersion(value => value + 1)
const fail = () => { faults++; changed(); throw new Error("synthetic subscriber failure") }
// Replace diagnostics only; dispatch, transport, native reducer entry and the
// browser's EventSource are production implementations.
getLogger("sse").error = (...args) => { errors.push(args) }
if (mode === "event") serverEvents.on("*", event => { if (event.type === "instance.event") fail() })
if (mode === "derived") createRoot(() => {
const [broken, setBroken] = createSignal(false)
createMemo(() => { if (broken()) throw new Error("synthetic deferred derivation failure") })
serverEvents.on("*", event => {
if (event.type !== "instance.event") return
faults++
changed()
setBroken(true)
})
})
if (mode === "open") serverEvents.onOpen(fail)
if (mode === "status") serverEvents.onTransportStatus(status => { if (status === "disconnected") fail() })
sseManager.onNativeSessionEvent = (_id, event) => { native.push(event.id); changed() }
const unsubscribe = serverEvents.on("instance.event", event => {
if (event.type === "instance.event") typed.push(event.event.id)
changed()
})
serverEvents.onOpen(() => { opens++; changed() })
serverEvents.onTransportStatus(status => { statuses.push(status); changed() })
;(window as any).fixture = {
snapshot: () => ({ native, typed, statuses, faults, opens, errors }),
restart: () => {
try { serverEvents.restart("isolated subscriber fixture"); return null }
catch (error) { return error instanceof Error ? error.message : String(error) }
},
unsubscribe,
}
render(() => <pre data-events>{version()}{JSON.stringify({ native, typed, statuses, faults, opens })}</pre>, document.getElementById("root")!)

View file

@ -14,6 +14,9 @@ import { setSessions, setActiveSession, setActiveParentSession, setSessionPage,
import { ensureWorktreesLoaded } from "../../../src/stores/worktrees"
import "../../../src/index.css"
// Optional, synchronous test-only marks; no observer is installed on native pages.
const bootStage = (phase: string) => (window as any).__headerFixtureBoot?.mark(phase)
bootStage("imports-complete")
const id = "header-windows", sessionId = "session"
let interrupts = 0
const session: any = { id: sessionId, instanceId: id, parentId: null, title: "Fixture conversation", location: { directory: "/repo" },
@ -37,7 +40,9 @@ setProviders(previous => new Map(previous).set(id, [{ id: "fixture", name: "Fixt
setActiveSession(id, sessionId)
setActiveParentSession(id, sessionId)
setSessionPage(id, [sessionId], false, true)
bootStage("worktrees-before")
await ensureWorktreesLoaded(id)
bootStage("worktrees-after")
let executions = 0
const escapeStates: boolean[] = []
const [menuInstance, setMenuInstance] = createSignal<string | undefined>(id)
@ -64,8 +69,12 @@ function Fixture() {
</div>
)
}
bootStage("render-before")
render(() => <ConfigProvider><I18nProvider><ThemeProvider><Fixture /></ThemeProvider></I18nProvider></ConfigProvider>, document.getElementById("root")!)
bootStage("render-after")
bootStage("preferences-before")
await updatePreferences({ locale: "en" })
bootStage("preferences-after")
;(window as any).fixture = {
viewAction: (action: string) => viewAction(action),
menuInstance: setMenuInstance,
@ -82,3 +91,4 @@ await updatePreferences({ locale: "en" })
interrupts: () => interrupts,
promptHeight: promptInputHeight,
}
bootStage("published")

View file

@ -0,0 +1,52 @@
import { OpenCode } from "@opencode/client"
import { createSignal } from "solid-js"
import { render } from "solid-js/web"
import { createInstanceFetch } from "../../../src/lib/sdk-manager"
import { serverEvents } from "../../../src/lib/server-events"
// Deliberately small transport fixture, not a replacement session store/UI.
// All API requests and SSE use the real production browser adapters.
const baseUrl = `${location.origin}/workspaces/workspace/instance/`
const client = OpenCode.make({ baseUrl, fetch: createInstanceFetch(baseUrl) })
const [selected, setSelected] = createSignal("A")
const [loaded, setLoaded] = createSignal("")
const [draft, setDraft] = createSignal("")
const [opens, setOpens] = createSignal(0)
const [events, setEvents] = createSignal(0)
const [pending, setPending] = createSignal(false)
const errors: string[] = []
let catalogueSettled = 0
serverEvents.onOpen(() => setOpens(value => value + 1))
serverEvents.on("instance.eventStatus", () => setEvents(value => value + 1))
const fixture = {
start(action: "prompt" | "compact") {
setPending(true)
const operation = action === "prompt"
? client.session.prompt({ sessionID: "A", text: "private transport fixture" })
: client.session.compact({ sessionID: "A" })
void operation.catch(error => errors.push(String(error))).finally(() => setPending(false))
},
catalogues() {
// Five distinct catalogue intents, not foreground session reads: production
// createInstanceFetch must admit only two and leave the other three queued.
// Distinct URLs avoid Chromium's same-URL cache request coalescing.
for (let index = 0; index < 5; index++) {
void client.agent.get({ agentID: `fixture-${index}` }).catch(error => errors.push(String(error))).finally(() => catalogueSettled++)
}
},
async info() { return (await fetch("/api/meta")).json() },
async witness() { return (await fetch("/api/witness")).json() },
snapshot() { return { selected: selected(), loaded: loaded(), draft: draft(), opens: opens(), events: events(), pending: pending(), catalogueSettled, errors } },
}
;(window as any).fixture = fixture
render(() => <main>
<input aria-label="Draft" value={draft()} onInput={event => setDraft(event.currentTarget.value)} />
<button onClick={() => {
setSelected("B")
void client.session.get({ sessionID: "B" }).then(session => setLoaded(session.id)).catch(error => errors.push(String(error)))
}}>Switch to B</button>
<output data-selected>{selected()}</output>
<output data-loaded>{loaded()}</output>
</main>, document.getElementById("root")!)

View file

@ -0,0 +1,44 @@
import { createSignal } from "solid-js"
import { render } from "solid-js/web"
import MessageTimeline, { type TimelineSegment } from "../../../src/components/message-timeline"
import { I18nProvider } from "../../../src/lib/i18n"
import { ConfigProvider } from "../../../src/stores/preferences"
import { ThemeProvider } from "../../../src/lib/theme"
import { applyUiSettings } from "./ui-settings"
import { messageStoreBus } from "../../../src/stores/message-v2/bus"
import { serverApi } from "../../../src/lib/api-client"
import "../../../src/index.css"
const count = Math.min(50_000, Math.max(10, Number(new URLSearchParams(location.search).get("count") ?? 10_000)))
const segments: TimelineSegment[] = Array.from({ length: count }, (_, index) => ({
id: `marker-${index}`, messageId: `message-${Math.floor(index / 3)}`, type: index % 3 === 0 ? "user" : index % 3 === 1 ? "tool" : "assistant",
label: String(index), tooltip: `Marker ${index}`, toolName: index % 3 === 1 ? "read" : undefined,
toolPartIds: index % 3 === 1 ? [`tool-${index}`] : undefined, totalChars: 0,
}))
// Seed one resident preview; all other excerpts are deterministic HTTP-boundary
// answers, never transcript loads. This also exercises Markdown in the tooltip.
const store = messageStoreBus.getOrCreate("render-timeline")
store.upsertMessage({ id: "message-0", sessionId: "timeline", role: "user", status: "complete", createdAt: 1,
parts: [{ id: "preview-text", type: "text", text: "**Resident preview**\n\n" + "bounded excerpt ".repeat(1000) }] })
const requests: number[] = []
serverApi.fetchOutlinePreviews = async (_instance, _session, ids) => {
requests.push(ids.length)
return { status: "previews", entries: ids.map(id => ({ id, text: "**Local excerpt**", tools: "Read tool excerpt" })) } as any
}
const [active, setActive] = createSignal("marker-0")
let selected = "", activeCounter = 0
await applyUiSettings({ locale: "en" })
const start = performance.now()
render(() => <ConfigProvider><I18nProvider><ThemeProvider><div class="message-timeline-sidebar" style={{ height: "600px", width: "60px" }}>
<MessageTimeline instanceId="render-timeline" sessionId="timeline" segments={segments}
activeSegmentId={active()} onSegmentClick={segment => { selected = segment.id }} />
</div></ThemeProvider></I18nProvider></ConfigProvider>, document.getElementById("root")!)
const mountMs = performance.now() - start
;(window as any).fixture = {
measure: (updates: number) => {
const start = performance.now()
for (let index = 0; index < updates; index++) setActive(`marker-${++activeCounter % 15}`)
return performance.now() - start
},
snapshot: () => ({ count, selected, active: active(), requests, mountMs }),
}

View file

@ -0,0 +1,172 @@
import { batch } from "solid-js"
import { render } from "solid-js/web"
import { ConfigProvider } from "../../../src/stores/preferences"
import { I18nProvider } from "../../../src/lib/i18n"
import { ThemeProvider } from "../../../src/lib/theme"
import MessageSection from "../../../src/components/message-section"
import { applyUiSettings } from "./ui-settings"
import { sdkManager } from "../../../src/lib/sdk-manager"
import { addInstance } from "../../../src/stores/instances"
import { setSessions, setActiveSession, sessions, messagesLoaded } from "../../../src/stores/session-state"
import { sseManager } from "../../../src/lib/sse-manager"
import { serverEvents } from "../../../src/lib/server-events"
import { loadMessages } from "../../../src/stores/session-api"
import { messageStoreBus } from "../../../src/stores/message-v2/bus"
import "../../../src/index.css"
const params = new URLSearchParams(location.search)
const mode = params.get("mode") ?? "task"
const count = Math.min(1000, Math.max(1, Number(params.get("count") ?? 12)))
const outputSize = Math.min(2_000_000, Math.max(1000, Number(params.get("size") ?? 8000)))
const legacySummary = params.has("legacy") ? [{ id: "legacy-step", tool: "read", title: "Parent legacy summary", status: "completed" }] : []
const instanceId = "render-cost", parentId = "parent", childId = "child"
const model = { providerID: "fixture", id: "fixture" }
const output = Array.from({ length: Math.ceil(outputSize / "Output 00000 with deterministic text\n".length) }, (_, index) => `Output ${String(index).padStart(5, "0")} with deterministic text\n`).join("").slice(0, outputSize)
const tool = (index: number) => ({ id: `step-${index}`, type: "tool", name: "read",
time: { created: index + 1, completed: index + 2 },
state: { status: "completed", input: { filePath: `file-${index}.txt` }, metadata: {}, content: [{ type: "text", text: output }] } })
const history: Record<string, any[]> = mode === "task" ? {
[parentId]: [{ id: "parent-message", type: "assistant", agent: "build", model, time: { created: 1, completed: 2 },
content: [{ id: "parent-task", type: "tool", name: "subagent", time: { created: 1, completed: 2 },
state: { status: "completed", input: { agent: "explore", description: "Deterministic child" }, metadata: { sessionId: childId, summary: legacySummary }, content: [{ type: "text", text: "Task result" }] } }] }],
[childId]: [{ id: "child-message", type: "assistant", agent: "build", model, time: { created: 3 },
content: Array.from({ length: count }, (_, index) => tool(index)) }],
} : {
[parentId]: Array.from({ length: count }, (_, index) => ({ id: `history-${index}`, type: "assistant", agent: "build", model,
time: { created: index + 1, completed: index + 2 }, content: [{ type: "text", text: `History ${index}\n\n` + "A deterministic paragraph.\n\n".repeat(2 + index % 5) }] })),
}
const requests: string[] = []
let holdChildReads = false
let failChildRead = false
const heldReads: Array<() => void> = []
const client: any = { session: {
active: async () => ({}), inbox: { list: async () => ({ data: [] }) },
get: async ({ sessionID }: any) => ({ id: sessionID, location: { directory: "/fixture" }, time: { created: 1, updated: 1 } }),
}, message: { list: async ({ sessionID, limit = 200, cursor }: any) => {
requests.push(sessionID)
if (sessionID === childId && holdChildReads) await new Promise<void>(resolve => heldReads.push(resolve))
if (sessionID === childId && failChildRead) { failChildRead = false; throw new Error("Deterministic read failure") }
const messages = history[sessionID] ?? []
const end = cursor ? Number(cursor) : messages.length, start = Math.max(0, end - limit)
return { data: messages.slice(start, end).reverse(), cursor: start ? { next: String(start) } : {} }
} } }
;(sdkManager as any).clients.set(`${instanceId}:/workspaces/${instanceId}/instance`, client)
sseManager.getStatuses = () => new Map([[instanceId, "connected"]])
addInstance({ id: instanceId, folder: "/fixture", port: 0, pid: 0, proxyPath: "", status: "ready", client })
setSessions(previous => new Map(previous).set(instanceId, new Map(Object.keys(history).map(id => [id, {
id, instanceId, parentId: id === parentId ? null : parentId, title: id, location: { directory: "/fixture" },
status: "idle", agent: "build", model: { providerId: "fixture", modelId: "fixture" }, time: { created: 1, updated: 1 },
} as any]))))
setActiveSession(instanceId, parentId)
await applyUiSettings({ locale: "en", showMessageTimeline: mode !== "task", toolInputsVisibility: "hidden",
toolCallExpansionDefaults: { preset: "custom", thinking: "collapsed", tools: { task: "expanded", read: "collapsed" } } })
for (const id of Object.keys(history)) await loadMessages(instanceId, id, { force: true })
// Instrument a native operation, not a replacement renderer/store. Only ToolCall
// snapshot inputs are counted; timings below also work with instrumentation off.
let snapshots = 0, snapshotMs = 0
const instrumented = params.get("instrument") !== "off"
const clone = window.structuredClone.bind(window)
if (instrumented) window.structuredClone = ((value: any, options?: StructuredSerializeOptions) => {
const matched = value?.type === "tool" && value.id?.startsWith("step-")
const start = performance.now(), result = clone(value, options)
if (matched) { snapshots++; snapshotMs += performance.now() - start }
return result
}) as typeof structuredClone
const clipboard: string[] = []
Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText: async (text: string) => { clipboard.push(text) } } })
let eventId = 100
const emit = (type: string, data: any) => (sseManager as any).handleEvent(instanceId, {
id: `event-${++eventId}`, type, created: eventId, location: { directory: "/fixture" },
data: { sessionID: childId, assistantMessageID: "child-message", ...data },
})
const startText = () => {
if (!history[childId][0].content.some((part: any) => part.type === "text")) history[childId][0].content.push({ type: "text", text: "" })
emit("session.text.started", {})
}
const delta = () => {
const text = history[childId][0].content.find((part: any) => part.type === "text")
text.text += " x"
emit("session.text.delta", { ordinal: count, delta: " x" })
}
const store = messageStoreBus.getOrCreate(instanceId)
render(() => <ConfigProvider><I18nProvider><ThemeProvider>
<main style={{ display: "flex", height: "700px", width: "1000px" }}>
<MessageSection instanceId={instanceId} sessionId={parentId} isActive={true} />
</main>
</ThemeProvider></I18nProvider></ConfigProvider>, document.getElementById("root")!)
let added = 0, removed = 0
const countSteps = (nodes: NodeList) => Array.from(nodes).reduce((total, node) => total + (node instanceof Element
? Number(node.matches('.tool-call[data-part-id^="step-"]')) + node.querySelectorAll('.tool-call[data-part-id^="step-"]').length : 0), 0)
const recordMutations = (records: MutationRecord[]) => {
for (const record of records) { added += countSteps(record.addedNodes); removed += countSteps(record.removedNodes) }
}
const observer = instrumented ? new MutationObserver(recordMutations) : undefined
observer?.observe(document.querySelector("main")!, { childList: true, subtree: true })
;(window as any).fixture = {
startText,
delta,
hold: () => { holdChildReads = true },
release: () => { holdChildReads = false; heldReads.splice(0).forEach(resolve => resolve()) },
reproject: () => loadMessages(instanceId, childId, { force: true }),
replaceChildTools: (indexes: number[], textIndexes: number[] = []) => {
// Same native message identity, but a new authoritative part membership.
history[childId][0].content = indexes.map(index => textIndexes.includes(index)
? { id: `step-${index}`, type: "text", text: output }
: tool(index))
return loadMessages(instanceId, childId, { force: true })
},
failRead: () => { failChildRead = true; delta() },
clearChild: () => { history[childId] = []; return loadMessages(instanceId, childId, { force: true }) },
deleteChild: () => {
history[childId] = []
// Native deletion arrives through the real dispatcher and its Solid batch,
// without rewriting the parent's task metadata or clearing its transcript.
;(serverEvents as any).dispatchBatch([{ type: "instance.event", instanceId, event: {
id: `event-${++eventId}`, type: "session.deleted", created: eventId,
location: { directory: "/fixture" }, data: { sessionID: childId },
} }])
},
toolOutput: () => (store.getMessage("child-message")?.parts["step-0"]?.data as any)?.state?.output,
mutateTool: () => batch(() => {
// Deliberately keep data identity and message revision unchanged; only the
// existing normalized part revision invalidates this in-place update.
const set = store.setState as any
set("messages", "child-message", "parts", "step-0", "data", "state", "output", "Versioned in-place output")
set("messages", "child-message", "parts", "step-0", "revision", (revision: number) => revision + 1)
}),
toolUpdate: (index: number, text: string) => {
history[childId][0].content[index].state.content = [{ type: "text", text }]
emit("session.tool.success", { id: `step-${index}`, content: [{ type: "text", text }], metadata: {} })
},
failTool: (index: number) => {
const part = history[childId][0].content.find((part: any) => part.id === `step-${index}`)
part.state = { ...part.state, status: "error", error: { message: "Deterministic failure" } }
emit("session.tool.failed", { id: part.id, error: part.state.error, metadata: {} })
},
removeTool: (index: number) => store.removeMessagePart("child-message", `step-${index}`, childId),
appendTool: () => {
history[childId][0].content.push({ ...tool(count), id: "appended-step", state: {
status: "completed", input: { filePath: "new.txt" }, metadata: {}, content: [{ type: "text", text: "New step output" }],
} })
emit("session.tool.input.started", { id: "appended-step", name: "read" })
emit("session.tool.called", { id: "appended-step", input: { filePath: "new.txt" } })
emit("session.tool.success", { id: "appended-step", content: [{ type: "text", text: "New step output" }], metadata: {} })
},
reset: () => { observer?.takeRecords(); snapshots = 0; snapshotMs = 0; added = 0; removed = 0 },
measure: (updates: number) => {
const start = performance.now()
for (let index = 0; index < updates; index++) delta()
const ms = performance.now() - start
if (observer) recordMutations(observer.takeRecords())
return { ms, snapshots, snapshotMs, added, removed }
},
snapshot: () => ({ snapshots, snapshotMs, added, removed, requests, clipboard, output, held: heldReads.length,
partIds: store.getMessage("child-message")?.partIds,
childRevision: store.getMessage("child-message")?.revision,
childMessageIds: [...store.getSessionMessageIds(childId)],
childLoaded: messagesLoaded().get(instanceId)?.has(childId) ?? false,
childExists: sessions().get(instanceId)?.has(childId) ?? false,
parentChildId: (store.getMessage("parent-message")?.parts["parent-task"]?.data as any)?.state?.metadata?.sessionId,
loaded: store.getSessionMessageIds(parentId).length }),
}

View file

@ -1,4 +1,4 @@
import { createSignal } from "solid-js"
import { createEffect, createSignal } from "solid-js"
import { render } from "solid-js/web"
import MessagePart from "../../../src/components/message-part"
import MessageTimeline from "../../../src/components/message-timeline"
@ -13,6 +13,14 @@ const code = '<span title="&lt;source&gt;">&amp;</span>'
const body = ["**Markdown still works**", html, "`" + code + "`", "```html\n" + code + "\n```"].join("\n\n")
const malformed = 'Please explain <code><img/src=x onerror="window.htmlExecuted=true"></code> literally\n\ntext <script>if (x<y) z()</script> after'
const [literal, setLiteral] = createSignal(true)
const traceEnabled = new URLSearchParams(location.search).has("trace")
const trace: Array<Record<string, unknown>> = []
const recordCache = (kind: string) => {
const cache = document.querySelector("#cache")
trace.push({ kind, literal: literal(), connected: cache?.isConnected ?? false,
html: cache?.innerHTML, time: performance.now() })
}
if (traceEnabled) createEffect(() => { literal(); queueMicrotask(() => recordCache("literal-effect")) })
const part = { id: "cached-part", type: "text" as const, text: html, version: 1,
renderCache: { text: html, html: '<div class="cached-html">old sanitized HTML</div>', theme: "light", mode: "1:escaped:wrap" } }
@ -28,4 +36,22 @@ render(() => <ConfigProvider><I18nProvider><ThemeProvider>
{ id: "assistant-preview", messageId: "assistant-preview", type: "assistant", label: "Assistant preview", tooltip: html, totalChars: html.length },
]} /></div>
</ThemeProvider></I18nProvider></ConfigProvider>, document.getElementById("root")!)
;(window as any).fixture = { html, code, malformed, literal: setLiteral }
if (traceEnabled) {
const cache = document.querySelector("#cache")
if (cache) new MutationObserver(() => recordCache("mutation")).observe(cache, { childList: true, subtree: true })
recordCache("mounted")
}
const tracedSetLiteral = (value: boolean) => {
if (traceEnabled) recordCache("setter-before")
setLiteral(value)
if (traceEnabled) queueMicrotask(() => recordCache("setter-microtask"))
}
;(window as any).fixture = { html, code, malformed,
literal: traceEnabled ? tracedSetLiteral : setLiteral,
snapshot: () => ({ literal: literal(), cache: document.querySelector("#cache")?.innerHTML,
cachedHtml: document.querySelectorAll("#cache .cached-html").length,
trace, renderCache: part.renderCache,
resources: performance.getEntriesByType("resource").map(entry => entry.name)
.filter(name => /(?:solid|markdown|user-html|\.js(?:\?|$))/.test(name)),
}),
}

View file

@ -7,6 +7,33 @@ import solid from "vite-plugin-solid"
import { prepareGitPrototypeAssets } from "./fixtures/git-history-assets.mjs"
let server: ViteDevServer, browser: Browser, url: string
const gitFocusBoundaryInitScript = String.raw`(() => {
const trace = []
window.gitFocusBoundaryTrace = trace
const describeElement = target => target instanceof Element
? { tag: target.tagName, id: target.id, className: target.className, partId: target.getAttribute("data-part-id") }
: null
const pushTrace = entry => {
trace.push(entry)
if (trace.length > 200) trace.shift()
}
for (const type of ["focusin", "focusout", "pointerdown", "click"]) {
document.addEventListener(type, event => {
pushTrace({ kind: type, target: describeElement(event.target), active: describeElement(document.activeElement), time: performance.now() })
}, true)
}
window.addEventListener("DOMContentLoaded", () => {
new MutationObserver(records => {
for (const record of records) for (const node of record.removedNodes) {
if (!(node instanceof Element)) continue
if (!node.matches(".git-change-context-widget, .git-change-context-widget-host")
&& !node.querySelector(".git-change-context-widget")) continue
pushTrace({ kind: "widget-removed", active: describeElement(document.activeElement), time: performance.now() })
}
}).observe(document.documentElement, { childList: true, subtree: true })
}, { once: true })
})()`
test("large Changes inventories keep the reader controls responsive", async () => {
const page = await browser.newPage({ viewport: { width: 2000, height: 1120 } })
try {
@ -592,6 +619,7 @@ test("central diff inserts local lines and revision-qualified history into the r
const errors: string[] = []
page.on("pageerror", error => errors.push(error.stack ?? error.message))
await page.route("**/api/**", route => route.fulfill({ json: {} }))
if (process.env.CODENOMAD_BROWSER_BOUNDARY_TRACE) await page.addInitScript({ content: gitFocusBoundaryInitScript })
try {
await page.goto(`${url}?session=1`)
const composer = page.locator(".session-view textarea.prompt-input")
@ -606,6 +634,8 @@ test("central diff inserts local lines and revision-qualified history into the r
await insert.click()
assert.match(await composer.inputValue(), /Conserver ce brouillon/)
assert.match(await composer.inputValue(), /Git Diff: Worktree: \/CodeNomad : File: src\/components\/git-panel.tsx : 3-3/)
const composerElement = (await composer.elementHandle())!
await page.waitForFunction(element => element === document.activeElement, composerElement)
assert.equal(await composer.evaluate(element => element === document.activeElement), true)
// Native editor keyboard selection must keep the full range, not just the hovered line.
@ -659,7 +689,25 @@ test("central diff inserts local lines and revision-qualified history into the r
await page.evaluate(() => (window as any).fixture.switchSession("session"))
assert.equal(await composer.inputValue(), draft)
assert.deepEqual(errors, [])
} finally { await page.close() }
} finally {
if (process.env.CODENOMAD_BROWSER_BOUNDARY_TRACE) {
try {
console.error("Git focus boundary trace", await page.evaluate(() => ({
events: (window as any).gitFocusBoundaryTrace,
active: document.activeElement instanceof Element
? { tag: document.activeElement.tagName, id: document.activeElement.id, className: document.activeElement.className }
: null,
widgets: Array.from(document.querySelectorAll<HTMLElement>(".git-change-context-widget")).map(widget => ({
connected: widget.isConnected, rect: widget.getBoundingClientRect().toJSON(),
})),
selections: (window as any).monaco?.editor?.getEditors?.().map((editor: any) => editor.getSelection?.()),
})))
} catch (error) {
console.error("Git focus boundary trace unavailable", error)
}
}
await page.close()
}
})
test("Workspace keyboard focus survives lazy expansion and refresh, including RTL", async () => {

View file

@ -0,0 +1,133 @@
import assert from "node:assert/strict"
import { after, before, test } from "node:test"
import { createServer, type Server } from "node:http"
import type { EventEmitter } from "node:events"
import { chromium, type Browser, type Page } from "playwright"
import { observeHeaderFixture } from "./header-fixture-diagnostics"
import { runWithDiagnosticCleanup } from "./fixture-diagnostic-boundary"
let browser: Browser, server: Server, url: string
before(async () => {
server = createServer((request, response) => {
response.setHeader("Content-Type", request.url?.startsWith("/entry.js") ? "text/javascript" : "text/html")
response.end(request.url?.startsWith("/entry.js") ? "window.fixture = {}" : '<div id="root"></div><script type="module" src="/entry.js?secret=do-not-log"></script>')
})
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve))
url = `http://127.0.0.1:${(server.address() as { port: number }).port}`
browser = await chromium.launch({ executablePath: process.env.CODENOMAD_BROWSER_PATH || undefined })
})
after(async () => { await browser?.close(); await new Promise<void>(resolve => server?.close(() => resolve())) })
const observedEvents = ["console", "pageerror", "requestfailed", "response", "framenavigated"] as const
// Playwright's real Page is an EventEmitter; its public Page interface omits this inspection method.
const listenerCounts = (page: Page) => observedEvents.map(event => (page as unknown as EventEmitter).listenerCount(event))
async function capture(run: () => Promise<unknown>) {
try { return { result: await run() } } catch (error) { return { error } }
}
test("aborted real entry module is observed before navigation; diagnostic/cleanup failures preserve primary identity", async () => {
const page = await browser.newPage(), baseline = listenerCounts(page)
const observer = observeHeaderFixture(page), primary = new Error("readiness sentinel"), stack = primary.stack
const emissionFailure = new Error("emission"), cleanupFailure = new Error("cleanup")
const logs: string[] = [], secondary: unknown[] = []
const result = await capture(() => runWithDiagnosticCleanup({
run: async () => {
await observer.install()
await page.route("**/entry.js?*", route => route.abort("failed"))
const failed = page.waitForEvent("requestfailed", request => request.url().includes("/entry.js"))
await page.goto(url)
await failed
throw primary
},
diagnose: () => observer.diagnose(message => { logs.push(message); throw emissionFailure }),
cleanup: async () => { observer.detach(); assert.deepEqual(listenerCounts(page), baseline); await page.close(); throw cleanupFailure },
onObservationError: error => secondary.push(error),
onCleanupError: error => secondary.push(error),
}))
assert.equal(result.error, primary)
assert.equal(primary.stack, stack)
assert.deepEqual(secondary, [emissionFailure, cleanupFailure])
const report = JSON.parse(logs[0])
assert.ok(report.events.some((event: any) => event.kind === "requestfailed" && event.url.endsWith("/entry.js")))
assert.ok(report.events.some((event: any) => event.kind === "navigation"))
assert.equal(report.snapshot.fixture, "undefined")
assert.equal(report.snapshot.boot[0].phase, "document-init")
assert.equal(report.snapshot.dom.textarea, false)
assert.ok(!logs[0].includes("do-not-log"))
})
test("bounded rings, stage marks and detach use the real browser without asynchronous stage work", async () => {
const page = await browser.newPage(), baseline = listenerCounts(page), observer = observeHeaderFixture(page)
const logs: string[] = []
try {
await observer.install()
await page.goto(url)
const lastConsole = page.waitForEvent("console", message => message.text() === "last")
await page.evaluate(() => {
for (let i = 0; i < 100; i++) {
(window as any).__headerFixtureBoot.mark("phase" + i, "x".repeat(1000))
console.log("y".repeat(1000))
}
console.log("last")
})
await lastConsole
await observer.diagnose(message => logs.push(message))
const report = JSON.parse(logs[0])
assert.equal(report.events.length, 48)
assert.ok(report.dropped >= 53)
assert.ok(report.events.every((event: any) => !event.text || event.text.length <= 256))
assert.equal(report.snapshot.boot.length, 16)
assert.equal(report.snapshot.boot.at(-1).phase, "phase99")
assert.ok(report.snapshot.boot.every((entry: any) => entry.detail.length <= 256))
assert.ok(logs[0].length < 40000)
observer.detach()
observer.detach()
assert.deepEqual(listenerCounts(page), baseline)
await page.evaluate(() => console.log("after detach"))
await observer.diagnose(message => logs.push(message))
assert.deepEqual(JSON.parse(logs[1]).events, report.events)
} finally { observer.detach(); await page.close() }
})
test("destroyed execution context snapshot is best effort and cannot replace a primary error", async () => {
const page = await browser.newPage(), observer = observeHeaderFixture(page), primary = new Error("primary")
const logs: string[] = []
const result = await capture(() => runWithDiagnosticCleanup({
run: async () => { await page.close(); throw primary },
diagnose: () => observer.diagnose(message => logs.push(message)),
cleanup: async () => { observer.detach() },
onObservationError: () => assert.fail("snapshot rejection must be recorded"),
onCleanupError: () => assert.fail("unexpected cleanup error"),
}))
assert.equal(result.error, primary)
assert.match(JSON.parse(logs[0]).observationError, /closed|destroyed/i)
})
test("real pending browser evaluation has a global 2s diagnostic deadline and cleanup still preserves primary", async (t) => {
const page = await browser.newPage(), observer = observeHeaderFixture(page), primary = new Error("primary")
const evaluate = page.evaluate.bind(page), logs: string[] = []
// Replace only the snapshot call with an actual never-resolving renderer evaluation.
t.mock.method(page, "evaluate", () => evaluate(() => new Promise(() => {})))
const result = await capture(() => runWithDiagnosticCleanup({
run: async () => { throw primary },
diagnose: () => observer.diagnose(message => logs.push(message)),
cleanup: async () => { observer.detach(); await page.close() },
onObservationError: () => assert.fail("deadline must be recorded"),
onCleanupError: () => assert.fail("unexpected cleanup error"),
}))
assert.equal(result.error, primary)
assert.equal(JSON.parse(logs[0]).observationError, "header snapshot deadline (2000ms)")
})
test("successful run does not diagnose, detaches, and cleanup failure remains red", async () => {
const page = await browser.newPage(), baseline = listenerCounts(page), observer = observeHeaderFixture(page)
const cleanupFailure = new Error("cleanup after success")
const result = await capture(() => runWithDiagnosticCleanup({
run: async () => { await observer.install(); await page.goto(url); return "success" },
diagnose: () => { assert.fail("no diagnostics after success") },
cleanup: async () => { observer.detach(); assert.deepEqual(listenerCounts(page), baseline); await page.close(); throw cleanupFailure },
onObservationError: () => assert.fail("no observation error"),
onCleanupError: () => assert.fail("cleanup must propagate"),
}))
assert.equal(result.error, cleanupFailure)
})

View file

@ -0,0 +1,83 @@
import type { Page } from "playwright"
const maxEvents = 48, maxString = 256, snapshotDeadline = 2000
const bounded = (value: string) => value.slice(0, maxString)
const errorText = (error: unknown) => bounded(error instanceof Error ? error.message : String(error))
function locationLabel(value: string) {
try {
const url = new URL(value)
// Never retain credentials, arbitrary query values or fragments.
return bounded(url.origin + url.pathname + (url.searchParams.has("v") ? `?v=${bounded(url.searchParams.get("v")!)}` : ""))
} catch { return bounded(value.split(/[?#]/)[0]) }
}
// Installed only on observed composer pages, before their entry module loads.
// No console output or async work: stages/errors stay in a small document-local ring.
export const headerBootScript = `(() => {
const entries = [];
const mark = (phase, detail = '') => {
entries.push({ phase: String(phase).slice(0, 64), detail: String(detail).slice(0, 256), at: performance.now() });
if (entries.length > 16) entries.shift();
};
window.__headerFixtureBoot = { entries, mark };
mark('document-init');
addEventListener('error', e => mark('window-error', e.message || 'resource error'));
addEventListener('unhandledrejection', e => mark('rejection', String(e.reason)));
})()`
export function observeHeaderFixture(page: Page) {
const events: Array<Record<string, string | number>> = []
let dropped = 0, detached = false
const push = (event: Record<string, string | number>) => {
events.push(event)
if (events.length > maxEvents) { events.shift(); dropped++ }
}
const consoleMessage = (message: import("playwright").ConsoleMessage) => push({ kind: "console", level: message.type(), text: bounded(message.text()) })
const pageError = (error: Error) => push({ kind: "pageerror", text: errorText(error) })
const requestFailed = (request: import("playwright").Request) => push({ kind: "requestfailed", url: locationLabel(request.url()), text: bounded(request.failure()?.errorText ?? "") })
const response = (result: import("playwright").Response) => {
const request = result.request(), url = new URL(result.url())
if (["script", "fetch", "xhr", "eventsource", "document"].includes(request.resourceType()) || url.pathname.includes("/api/")) {
push({ kind: "response", url: locationLabel(result.url()), status: result.status() })
}
}
const navigation = (frame: import("playwright").Frame) => {
if (frame === page.mainFrame()) push({ kind: "navigation", url: locationLabel(frame.url()) })
}
page.on("console", consoleMessage)
page.on("pageerror", pageError)
page.on("requestfailed", requestFailed)
page.on("response", response)
page.on("framenavigated", navigation)
return {
install: () => page.addInitScript(headerBootScript),
async diagnose(emit: (message: string) => void) {
let timer: ReturnType<typeof setTimeout> | undefined
let snapshot: unknown, observationError: string | undefined
try {
snapshot = await Promise.race([
page.evaluate(() => {
const boot = (window as any).__headerFixtureBoot
return { readyState: document.readyState, fixture: typeof (window as any).fixture,
boot: boot?.entries?.slice(-16).map((entry: any) => ({ phase: String(entry.phase).slice(0, 64), detail: String(entry.detail).slice(0, 256), at: typeof entry.at === "number" ? entry.at : undefined })),
dom: { rootChildren: document.getElementById("root")?.childElementCount ?? 0,
textarea: Boolean(document.querySelector("textarea.prompt-input")), footer: Boolean(document.querySelector(".prompt-input-footer")) } }
}),
new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error("header snapshot deadline (2000ms)")), snapshotDeadline) }),
])
} catch (error) { observationError = errorText(error) }
finally { if (timer !== undefined) clearTimeout(timer) }
emit(JSON.stringify({ kind: "header-fixture-diagnostic", url: locationLabel(page.url()), dropped, detached, events: events.slice(), snapshot, observationError }))
},
detach() {
if (detached) return
detached = true
page.off("console", consoleMessage)
page.off("pageerror", pageError)
page.off("requestfailed", requestFailed)
page.off("response", response)
page.off("framenavigated", navigation)
},
}
}

View file

@ -9,6 +9,8 @@ import { build } from "esbuild"
import { chromium, devices, _electron, type ElectronApplication, type Browser } from "playwright"
import { createServer, type ViteDevServer } from "vite"
import solid from "vite-plugin-solid"
import { runWithDiagnosticCleanup } from "./fixture-diagnostic-boundary"
import { observeHeaderFixture } from "./header-fixture-diagnostics"
let server: ViteDevServer, browser: Browser, url: string
before(async () => {
@ -128,50 +130,58 @@ test("native Electron zoom preserves CSS minimums across close/recreate, shared
})
for (const touch of [false, true]) for (const [width, height] of [[320, 740], [360, 740], [390, 844], [430, 932], [932, 430]]) {
test(`composer controls fit ${width}px ${touch ? "touch" : "mouse"} with a preserved draft`, async () => {
test(`composer controls fit ${width}px ${touch ? "touch" : "mouse"} with a preserved draft`, async (t) => {
const page = await browser.newPage({ ...(touch ? devices["Pixel 5"] : {}), viewport: { width, height } })
await page.route("**/api/**", route => route.fulfill({ json: {} }))
try {
await page.goto(url)
await page.waitForFunction(() => Boolean((window as any).fixture))
await page.evaluate(() => (window as any).fixture.setLocale("fr"))
const input = page.locator("textarea.prompt-input")
await input.fill("Brouillon mobile conservé")
const geometry = await page.evaluate(() => {
const footer = document.querySelector(".prompt-input-footer")!.getBoundingClientRect()
const buttons = [...document.querySelectorAll<HTMLElement>(".prompt-context-controls .selector-trigger, .prompt-input-footer-actions button")]
.filter(el => el.getBoundingClientRect().width > 0).map(el => {
const r = el.getBoundingClientRect()
return { x: r.x, y: r.y, right: r.right, bottom: r.bottom, width: r.width, height: r.height }
})
return { footer: { x: footer.x, right: footer.right, bottom: footer.bottom, width: footer.width }, buttons,
selectors: [...document.querySelectorAll(".prompt-context-controls .selector-trigger")].map(el => el.getBoundingClientRect().y),
actionsY: document.querySelector(".prompt-input-footer-actions")!.getBoundingClientRect().y }
})
for (const b of geometry.buttons) {
assert.ok(b.x >= geometry.footer.x - 1 && b.right <= geometry.footer.right + 1 && b.bottom <= geometry.footer.bottom + 1, JSON.stringify(b))
if (touch) assert.ok(b.width >= 32 && b.height >= 32, "dense touch controls retain compact targets")
}
for (let i = 0; i < geometry.buttons.length; i++) for (const b of geometry.buttons.slice(i + 1)) {
const a = geometry.buttons[i]
assert.ok(a.right <= b.x + 1 || b.right <= a.x + 1 || a.bottom <= b.y + 1 || b.bottom <= a.y + 1, "controls must not overlap")
}
assert.equal(new Set(geometry.selectors).size, 1, "selectors retain one compact row")
assert.ok(Math.abs(geometry.actionsY - geometry.selectors[0]) <= 1, "actions and selectors always share one row")
if (width === 390) {
const worktree = page.locator('.prompt-context-controls[data-has-worktree="true"] > .sidebar-selector').first().getByRole("button")
assert.match(await worktree.innerText(), /Espace de travail/)
await worktree.focus()
await page.keyboard.press("ArrowDown")
await page.getByRole("listbox").waitFor({ state: "visible" })
await page.keyboard.press("Escape")
await page.getByRole("listbox").waitFor({ state: "hidden" })
const diagnostics = observeHeaderFixture(page)
await runWithDiagnosticCleanup({
run: async () => {
await page.route("**/api/**", route => route.fulfill({ json: {} }))
await diagnostics.install()
await page.goto(url)
await page.waitForFunction(() => Boolean((window as any).fixture))
await page.evaluate(() => (window as any).fixture.setLocale("fr"))
const input = page.locator("textarea.prompt-input")
await input.fill("Brouillon mobile conservé")
const geometry = await page.evaluate(() => {
const footer = document.querySelector(".prompt-input-footer")!.getBoundingClientRect()
const buttons = [...document.querySelectorAll<HTMLElement>(".prompt-context-controls .selector-trigger, .prompt-input-footer-actions button")]
.filter(el => el.getBoundingClientRect().width > 0).map(el => {
const r = el.getBoundingClientRect()
return { x: r.x, y: r.y, right: r.right, bottom: r.bottom, width: r.width, height: r.height }
})
return { footer: { x: footer.x, right: footer.right, bottom: footer.bottom, width: footer.width }, buttons,
selectors: [...document.querySelectorAll(".prompt-context-controls .selector-trigger")].map(el => el.getBoundingClientRect().y),
actionsY: document.querySelector(".prompt-input-footer-actions")!.getBoundingClientRect().y }
})
for (const b of geometry.buttons) {
assert.ok(b.x >= geometry.footer.x - 1 && b.right <= geometry.footer.right + 1 && b.bottom <= geometry.footer.bottom + 1, JSON.stringify(b))
if (touch) assert.ok(b.width >= 32 && b.height >= 32, "dense touch controls retain compact targets")
}
for (let i = 0; i < geometry.buttons.length; i++) for (const b of geometry.buttons.slice(i + 1)) {
const a = geometry.buttons[i]
assert.ok(a.right <= b.x + 1 || b.right <= a.x + 1 || a.bottom <= b.y + 1 || b.bottom <= a.y + 1, "controls must not overlap")
}
assert.equal(new Set(geometry.selectors).size, 1, "selectors retain one compact row")
assert.ok(Math.abs(geometry.actionsY - geometry.selectors[0]) <= 1, "actions and selectors always share one row")
if (width === 390) {
const worktree = page.locator('.prompt-context-controls[data-has-worktree="true"] > .sidebar-selector').first().getByRole("button")
assert.match(await worktree.innerText(), /Espace de travail/)
await worktree.focus()
await page.keyboard.press("ArrowDown")
await page.getByRole("listbox").waitFor({ state: "visible" })
await page.keyboard.press("Escape")
await page.getByRole("listbox").waitFor({ state: "hidden" })
assert.equal(await input.inputValue(), "Brouillon mobile conservé")
}
if (process.env.CODENOMAD_MOBILE_CAPTURE) await page.screenshot({ path: `${process.env.CODENOMAD_MOBILE_CAPTURE}/${touch ? "touch" : "mouse"}-${width}.png`, scale: "css" })
await page.setViewportSize({ width: 600, height: 851 })
assert.equal(await input.inputValue(), "Brouillon mobile conservé")
}
if (process.env.CODENOMAD_MOBILE_CAPTURE) await page.screenshot({ path: `${process.env.CODENOMAD_MOBILE_CAPTURE}/${touch ? "touch" : "mouse"}-${width}.png`, scale: "css" })
await page.setViewportSize({ width: 600, height: 851 })
assert.equal(await input.inputValue(), "Brouillon mobile conservé")
} finally { await page.close() }
},
diagnose: () => diagnostics.diagnose(message => t.diagnostic(message)),
cleanup: async () => { diagnostics.detach(); await page.close() },
onObservationError: () => t.diagnostic("header diagnostic emission failed"),
onCleanupError: () => t.diagnostic("header page cleanup failed after primary error"),
})
})
}

View file

@ -442,13 +442,16 @@ async function dragNativeThumb(page: Page, selector: string, fraction: number) {
const height = Math.max(18, track * element.clientHeight / element.scrollHeight)
const travel = track - height
const ratio = element.scrollTop / (element.scrollHeight - element.clientHeight)
return { x: rect.right - inset / 2, top: rect.top + inset, height, travel,
return { x: rect.right - inset / 2, ratio, travel,
y: ratio > 0.99 ? rect.bottom - 16 : ratio < 0.01 ? rect.top + 16 : rect.top + inset + height / 2 + travel * ratio }
})
await page.mouse.move(geometry.x, geometry.y)
await page.mouse.down()
await page.waitForTimeout(750)
await page.mouse.move(geometry.x, geometry.top + geometry.height / 2 + geometry.travel * fraction, { steps: 12 })
// Native drags retain the grab offset within the thumb. At an end we grab
// near its edge, so targeting the future centre undershoots the requested
// ratio. Move by the scroll-ratio delta from the actual press instead.
await page.mouse.move(geometry.x, geometry.y + geometry.travel * (fraction - geometry.ratio), { steps: 12 })
await page.mouse.up()
}

View file

@ -0,0 +1,261 @@
import assert from "node:assert/strict"
import type { IncomingMessage, ServerResponse } from "node:http"
import type { Socket } from "node:net"
import { after, before, test } from "node:test"
import { fileURLToPath } from "node:url"
import Fastify from "fastify"
import replyFrom from "@fastify/reply-from"
import { OpenCode } from "@opencode/client"
import pino from "pino"
import { chromium, type Browser, type Page } from "playwright"
import { createServer, type ViteDevServer } from "vite"
import solid from "vite-plugin-solid"
import { EventBus } from "../../../server/src/events/bus"
import { ClientConnectionManager } from "../../../server/src/clients/connection-manager"
import { createRuntimeFetch } from "../../../server/src/opencode/compatibility/transport"
import { registerInstanceProxyRoutes, type InstanceProxyWorkspaceManager } from "../../../server/src/server/http-server"
import { registerEventRoutes } from "../../../server/src/server/routes/events"
import { registerMetaRoutes } from "../../../server/src/server/routes/meta"
import { WorktreeDeletionFence } from "../../../server/src/workspaces/worktree-session-evacuation"
let browser: Browser, assets: ViteDevServer, assetUrl: string
before(async () => {
assets = await createServer({ configFile: false, root: fileURLToPath(new URL("../..", import.meta.url)),
logLevel: "error", plugins: [solid()], resolve: { dedupe: ["solid-js"] },
server: { host: "127.0.0.1", port: 0, cors: true, hmr: false, watch: null },
})
await assets.listen()
assetUrl = `http://127.0.0.1:${(assets.httpServer!.address() as { port: number }).port}`
browser = await chromium.launch({ executablePath: process.env.CODENOMAD_BROWSER_PATH || undefined })
})
after(async () => { await browser?.close(); await assets?.close() })
async function until(predicate: () => boolean, description: string) {
// Only a deadlock safety bound, never a responsiveness/performance threshold.
const deadline = Date.now() + 10_000
while (!predicate()) {
if (Date.now() > deadline) throw new Error(`Did not observe ${description}`)
await new Promise(resolve => setTimeout(resolve, 10))
}
}
async function harness() {
// Reuse the production proxy/admission/fetch/fence as in the server disconnect
// diagnostic; use a real upstream SDK instead of copying its fake client tree.
const held = Promise.withResolvers<void>()
let released = false, nativeObservers = 0, nativeHttpA = 0, aborted = 0, heldHandlers = 0
let forwardsA = 0, environmentWrites = 0, invalidations = 0, catalogueRequests = 0
const nativeHttpCatalogues = new Set<ServerResponse>()
const heldMutations = new Set<ServerResponse>()
const captured: Array<{ request: IncomingMessage; response: ServerResponse; abortedListeners: number }> = []
const clients = new Set<() => void>(), sockets = new Set<Socket>()
const backendErrors: string[] = []
const versions = new Set<string>(), bus = new EventBus(), fence = new WorktreeDeletionFence()
const logger = pino({ level: "silent" }), connections = new ClientConnectionManager(logger)
let peakSockets = 0
const upstream = Fastify({ forceCloseConnections: true })
upstream.get("/api/session/:id", async (request, reply) => {
const id = (request.params as { id: string }).id
if (id === "A") {
nativeHttpA++
reply.raw.once("close", () => nativeHttpA--)
heldHandlers++
try { await held.promise } // intentionally ignore disconnect; exercise late completion
finally { heldHandlers-- }
}
return { data: { id, location: { directory: "/fixture" } } }
})
upstream.get("/api/agent/:id", async (_request, reply) => {
catalogueRequests++
nativeHttpCatalogues.add(reply.raw)
reply.raw.once("close", () => nativeHttpCatalogues.delete(reply.raw))
heldHandlers++
try { await held.promise }
finally { heldHandlers-- }
return []
})
upstream.get("/api/location", async () => ({ directory: "/fixture" }))
upstream.post("/api/session/:id/:action", async () => { forwardsA++; return {} })
upstream.put("/api/session/:id/environment", async (_request, reply) => { environmentWrites++; return reply.code(204).send() })
await upstream.listen({ host: "127.0.0.1", port: 0 })
const endpoint = { url: `http://127.0.0.1:${(upstream.server.address() as { port: number }).port}` }
const runtimeFetch = createRuntimeFetch(endpoint)
const native = OpenCode.make({ baseUrl: endpoint.url, fetch: runtimeFetch })
const client = { ...native, session: { ...native.session, get: async (...args: Parameters<typeof native.session.get>) => {
if (args[0].sessionID !== "A") return native.session.get(...args)
nativeObservers++
const signal = args[1]?.signal, onAbort = () => { aborted++ }
signal?.addEventListener("abort", onAbort, { once: true })
try { return await native.session.get(...args) }
finally { nativeObservers--; signal?.removeEventListener("abort", onAbort) }
} } }
const connection = { endpoint, client, fetch: runtimeFetch, assertCurrent() {},
invalidate() { invalidations++ }, profile: async () => "modern" as const }
// Only workspace ownership is supplied here. Reads cross real SDK HTTP; no
// daemon discovery, database, provider, filesystem or shared profile is used.
const manager = {
get: () => ({ id: "workspace", path: "/fixture" }),
getSharedServiceConnection: async () => connection,
getInstanceAuthorizationHeader: () => undefined,
getServiceDirectory: () => "/fixture",
getServiceDirectoryForPath: async () => "/fixture",
getWorktreeIdentityForPath: async () => "/fixture",
getSessionEnvironment: async () => ({}),
ownsDirectory: async (_id: string, directory: string) => directory === "/fixture",
ownsLocation: async (_id: string, location: { directory: string }, _client: unknown, signal?: AbortSignal) =>
location.directory === "/fixture" && (await native.location.get({ location }, { signal })).directory === "/fixture",
} as unknown as InstanceProxyWorkspaceManager
const backend = Fastify({ forceCloseConnections: true })
backend.addHook("onError", async (_request, _reply, error) => { backendErrors.push(error.stack ?? error.message) })
backend.server.on("connection", socket => {
sockets.add(socket); peakSockets = Math.max(peakSockets, sockets.size)
socket.once("close", () => sockets.delete(socket))
})
backend.addHook("onRequest", async (request, reply) => {
versions.add(request.raw.httpVersion)
if (request.method === "POST" && /\/session\/A\/(prompt|compact)$/.test(request.url)) {
captured.push({ request: request.raw, response: reply.raw, abortedListeners: request.raw.listenerCount("aborted") })
heldMutations.add(reply.raw)
reply.raw.once("close", () => { setImmediate(() => heldMutations.delete(reply.raw)) })
}
})
await backend.register(replyFrom)
registerInstanceProxyRoutes(backend, { workspaceManager: manager, logger, worktreeDeletionFence: fence })
registerEventRoutes(backend, { eventBus: bus, logger, connectionManager: connections,
registerClient: close => { clients.add(close); return () => { clients.delete(close) } },
})
backend.get("/api/auth/status", async () => ({ authenticated: true }))
backend.get("/api/witness", async () => ({ witness: true }))
// Business HTTP/SSE goes directly to Fastify, not through Vite's Node proxy.
// Vite serves modules on a separate origin and cannot consume this HTTP budget.
backend.get("/fixture", async (_request, reply) => reply.type("text/html").send(
`<html><body><div id="root"></div><script type="module" src="${assetUrl}/tests/browser/fixtures/http-admission-responsiveness.tsx"></script></body></html>`,
))
const meta = { localUrl: "", eventsUrl: "/api/events", host: "127.0.0.1", listeningMode: "local" as const,
localPort: 0, hostLabel: "private fixture", workspaceRoot: "/fixture", addresses: [] }
registerMetaRoutes(backend, { serverMeta: meta })
await backend.listen({ host: "127.0.0.1", port: 0 })
meta.localPort = (backend.server.address() as { port: number }).port
const url = meta.localUrl = `http://127.0.0.1:${meta.localPort}`
return {
url, captured, fence,
emit() { bus.publish({ type: "instance.eventStatus", instanceId: "workspace", status: "connected", generation: 1 }) },
release() { released = true; held.resolve() },
snapshot() { return { released, nativeObservers, nativeHttpA, aborted, heldHandlers, forwardsA, environmentWrites, invalidations,
catalogueRequests, nativeHttpCatalogues: nativeHttpCatalogues.size, heldMutations: heldMutations.size,
sse: clients.size, sockets: sockets.size, peakSockets, versions: [...versions], backendErrors } },
async dispose() {
held.resolve(); connections.shutdown(); for (const close of clients) close()
await backend.close(); await upstream.close()
},
}
}
async function setup(page: Page, h: Awaited<ReturnType<typeof harness>>) {
const errors: string[] = [], protocols = new Map<string, string>()
page.on("pageerror", error => errors.push(error.message))
const cdp = await page.context().newCDPSession(page)
await cdp.send("Network.enable")
cdp.on("Network.responseReceived", ({ response }) => {
if (response.url.startsWith(h.url)) protocols.set(new URL(response.url).pathname, response.protocol)
})
await page.goto(`${h.url}/fixture`)
await page.getByRole("button", { name: "Switch to B" }).waitFor()
await page.waitForFunction(() => (window as any).fixture.snapshot().opens === 1)
await until(() => h.snapshot().sse === 1, "one real SSE subscription")
const previousEvents = await page.evaluate(() => (window as any).fixture.snapshot().events)
h.emit()
await page.waitForFunction(previous => (window as any).fixture.snapshot().events > previous, previousEvents)
return { errors, protocols }
}
for (const scenario of [
{ action: "prompt", close: "page", background: false },
{ action: "compact", close: "page", background: false },
{ action: "prompt", close: "context", background: false },
{ action: "compact", close: "context", background: false },
{ action: "compact", close: "context", background: true },
] as const) {
test(`HTTP/1 SSE + held ${scenario.action}: ${scenario.close} reconnect${scenario.background ? " with separate backgroundReads(2) occupancy" : ""}`,
{ timeout: 60_000 }, async t => {
const h = await harness(), context = await browser.newContext()
let reconnectContext = context
try {
const page = await context.newPage(), diagnostics = await setup(page, h)
await page.evaluate(action => (window as any).fixture.start(action), scenario.action)
await until(() => h.snapshot().nativeHttpA === 1, "actual SDK session A HTTP preflight")
if (scenario.background) {
await page.evaluate(() => (window as any).fixture.catalogues())
await until(() => h.snapshot().catalogueRequests === 2, "two admitted background catalogue reads")
}
const started = performance.now()
await page.getByRole("textbox", { name: "Draft" }).fill("draft while A is held")
await page.getByRole("button", { name: "Switch to B" }).click()
await page.waitForFunction(() => (window as any).fixture.snapshot().loaded === "B" || (window as any).fixture.snapshot().errors.length > 0)
assert.equal((await page.evaluate(() => (window as any).fixture.snapshot())).loaded, "B", JSON.stringify({ backend: h.snapshot(), browser: await page.evaluate(() => (window as any).fixture.snapshot()) }))
const info = await page.evaluate(() => (window as any).fixture.info())
const witness = await page.evaluate(() => (window as any).fixture.witness())
assert.deepEqual(info.system, { platform: process.platform, arch: process.arch })
assert.deepEqual(witness, { witness: true })
const state = await page.evaluate(() => (window as any).fixture.snapshot())
assert.equal(state.draft, "draft while A is held")
assert.equal(state.selected, "B")
assert.equal(state.pending, true)
assert.deepEqual(state.errors, [])
const during = h.snapshot()
assert.equal(during.released, false)
assert.equal(during.nativeObservers, 1)
assert.equal(during.sse, 1)
assert.equal(during.forwardsA, 0)
assert.equal(during.heldHandlers, scenario.background ? 3 : 1)
assert.deepEqual(during.backendErrors, [])
if (scenario.background) {
assert.equal(during.catalogueRequests, 2, "three catalogue intents must remain queued, not occupy HTTP sockets")
assert.equal(during.nativeHttpCatalogues, 2)
assert.equal(state.catalogueSettled, 0)
}
for (const path of ["/api/events", "/api/witness", "/api/meta", "/workspaces/workspace/instance/api/session/B"]) {
assert.equal(diagnostics.protocols.get(path), "http/1.1", `${path} must use real Chromium HTTP/1.1`)
}
t.diagnostic(`browser=${browser.version()} platform=${process.platform}; held-progress=${Math.round(performance.now() - started)} ms; during=${JSON.stringify(during)}`)
assert.deepEqual(diagnostics.errors, [])
const closing = performance.now()
if (scenario.close === "page") await page.close()
else { await context.close(); reconnectContext = await browser.newContext() }
await until(() => {
const state = h.snapshot()
return state.sse === 0 && state.heldMutations === 0 && state.nativeHttpA === 0
&& state.nativeObservers === 0 && state.nativeHttpCatalogues === 0
}, "downstream/SSE/upstream observers retired before releasing the held handler")
assert.equal(h.snapshot().aborted, 1)
for (const entry of h.captured) {
assert.equal(entry.request.listenerCount("aborted"), entry.abortedListeners)
assert.equal(entry.response.listeners("close").some(listener => ["close", "disconnect"].includes(listener.name)), false)
}
await h.fence.run("/fixture", ["/fixture"], async () => {})
assert.equal(h.snapshot().heldHandlers, scenario.background ? 3 : 1, "upstream handlers stay deliberately held after HTTP observers leave")
t.diagnostic(`close-cleanup=${Math.round(performance.now() - closing)} ms; before-release=${JSON.stringify(h.snapshot())}; deletion fence drained`)
const next = await reconnectContext.newPage(), reconnected = await setup(next, h)
await next.getByRole("button", { name: "Switch to B" }).click()
await next.waitForFunction(() => (window as any).fixture.snapshot().loaded === "B")
assert.deepEqual(await next.evaluate(() => (window as any).fixture.witness()), { witness: true })
assert.equal(h.snapshot().released, false, "new page must progress before the old upstream handler is released")
assert.equal(h.snapshot().sse, 1)
const beforeRelease = h.snapshot()
h.release()
await until(() => h.snapshot().heldHandlers === 0, "all deliberately held upstream continuations finished")
await new Promise<void>(resolve => setImmediate(resolve))
await new Promise<void>(resolve => setImmediate(resolve))
assert.equal(h.snapshot().forwardsA, 0, "late preflight completion must not forward/replay the abandoned mutation")
assert.equal(h.snapshot().environmentWrites, 0)
assert.equal(h.snapshot().invalidations, 0)
assert.equal(h.snapshot().catalogueRequests, beforeRelease.catalogueRequests, "destroyed page cannot restart queued catalogue intents")
assert.deepEqual(h.snapshot().versions, ["1.1"])
assert.deepEqual(h.snapshot().backendErrors, [])
assert.deepEqual(reconnected.errors, [])
t.diagnostic(`reconnected-before-release=${JSON.stringify(beforeRelease)}; no late mutation/environment/invalidation`)
} finally { await reconnectContext.close(); await context.close(); await h.dispose() }
})
}

View file

@ -0,0 +1,344 @@
import assert from "node:assert/strict"
import { after, before, test } from "node:test"
import { fileURLToPath } from "node:url"
import { mkdtemp, readFile, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { chromium, type Browser, type Page } from "playwright"
import { createServer, type ViteDevServer } from "vite"
import solid from "vite-plugin-solid"
let server: ViteDevServer, browser: Browser, url: string
let cacheDirectory: string | undefined
before(async () => {
// Concurrent browser fixtures must not replace this run's optimized modules.
cacheDirectory = await mkdtemp(join(tmpdir(), "codenomad-render-vite-"))
server = await createServer({ configFile: false, root: fileURLToPath(new URL("../..", import.meta.url)), logLevel: "error", cacheDir: cacheDirectory,
plugins: [solid(), { name: "render-cost-fixture", configureServer(s) {
s.middlewares.use("/fixture", async (req, res) => {
res.setHeader("Content-Type", "text/html")
const name = req.url?.includes("mode=timeline") ? "render-cost-timeline" : "render-cost"
res.end(await s.transformIndexHtml("/fixture", `<html><body><div id="root"></div><script type="module" src="/tests/browser/fixtures/${name}.tsx"></script></body></html>`))
})
}, async load(id) {
// Optional audit-only A/B source override; never changes the checkout.
if (process.env.CODENOMAD_RENDER_TASK_BASELINE && id.replaceAll("\\", "/").endsWith("/src/components/tool-call/renderers/task.tsx")) {
return readFile(process.env.CODENOMAD_RENDER_TASK_BASELINE, "utf8")
}
} }], resolve: { dedupe: ["solid-js"] }, optimizeDeps: { exclude: ["lucide-solid"] },
server: { host: "127.0.0.1", port: 0, hmr: false, watch: null },
})
await server.listen()
url = `http://127.0.0.1:${(server.httpServer!.address() as { port: number }).port}/fixture`
browser = await chromium.launch({ executablePath: process.env.CODENOMAD_BROWSER_PATH || undefined })
})
after(async () => {
await browser?.close()
await server?.close()
if (cacheDirectory) await rm(cacheDirectory, { recursive: true, force: true })
})
async function open(query: string, run: (page: Page) => Promise<void>) {
const page = await browser.newPage({ viewport: { width: 1100, height: 800 }, locale: "en-US" })
const errors: string[] = []
page.on("pageerror", error => { errors.push(error.message); console.error("fixture-error", error.message) })
await page.route("**/*", route => {
const request = new URL(route.request().url())
if (request.origin !== new URL(url).origin) return route.abort("blockedbyclient")
if (request.pathname.startsWith("/api/")) return route.fulfill({ contentType: "application/json", body: "{}" })
return route.continue()
})
try {
await page.goto(`${url}?${query}`)
await page.waitForFunction(() => Boolean((window as any).fixture))
await run(page)
assert.deepEqual(errors, [])
} finally { await page.close() }
}
const step = (page: Page, index = 0) => page.locator(`.tool-call[data-part-id="step-${index}"]`)
const frames = (page: Page) => page.evaluate(() => new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))))
test("unrelated child text keeps mounted task steps, disclosure, scroller and copies", async () => open("mode=task&count=12", async page => {
const tool = step(page)
await tool.waitFor()
await page.evaluate(() => (window as any).fixture.startText())
await frames(page)
await tool.locator(":scope > .tool-call-header > .tool-call-header-toggle").click()
await tool.locator("pre code").waitFor()
await frames(page)
await tool.evaluate(element => { (window as any).originalStep = element })
await page.evaluate(() => (window as any).fixture.reproject())
assert.equal(await tool.evaluate(element => element === (window as any).originalStep), true, "authoritative reprojection must not recreate the tool shell")
const output = tool.locator(".tool-call-markdown")
const box = (await output.boundingBox())!
await page.mouse.move(box.x + 30, box.y + 60)
await page.mouse.down({ button: "middle" })
try {
await output.evaluate(element => { (window as any).originalScroller = element; element.scrollTop = 240 })
await page.evaluate(() => { (window as any).fixture.hold(); (window as any).fixture.reset(); (window as any).fixture.delta() })
await page.waitForFunction(() => (window as any).fixture.snapshot().held > 0)
assert.equal(await tool.evaluate(element => element === (window as any).originalStep), true, "pending display refresh must retain mounted steps")
await page.evaluate(() => (window as any).fixture.release())
await frames(page)
const state = await page.evaluate(() => (window as any).fixture.snapshot())
console.log("unrelated-update", { snapshots: state.snapshots, childRevision: state.childRevision, added: state.added, removed: state.removed })
assert.equal(await tool.evaluate(element => element === (window as any).originalStep), true, "unrelated text must not remount a completed tool")
assert.equal(await tool.locator(":scope > .tool-call-header > .tool-call-header-toggle").getAttribute("aria-expanded"), "true")
assert.equal(await output.evaluate(element => element === (window as any).originalScroller), true)
assert.equal(await output.evaluate(element => element.scrollTop), 240)
assert.equal(state.added + state.removed, 0)
} finally { await page.mouse.up({ button: "middle" }) }
await tool.locator(":scope > .tool-call-header > .tool-call-header-copy").first().click()
const state = await page.evaluate(() => (window as any).fixture.snapshot())
assert.equal(state.clipboard[0], state.output)
await page.evaluate(() => (window as any).fixture.toolUpdate(0, "Changed native output"))
await page.waitForFunction(() => document.querySelector('.tool-call[data-part-id="step-0"]')?.textContent?.includes("Changed native output"))
assert.equal(await tool.evaluate(element => element === (window as any).originalStep), true)
await tool.locator(":scope > .tool-call-header > .tool-call-header-copy").first().click()
assert.equal((await page.evaluate(() => (window as any).fixture.snapshot())).clipboard.at(-1), "Changed native output")
await page.evaluate(() => (window as any).fixture.mutateTool())
await page.waitForFunction(() => document.querySelector('.tool-call[data-part-id="step-0"]')?.textContent?.includes("Versioned in-place output"))
assert.equal(await tool.evaluate(element => element === (window as any).originalStep), true)
await page.evaluate(() => (window as any).fixture.failTool(0))
await tool.locator(".tool-call-error-content").waitFor()
assert.match((await tool.textContent())!, /Deterministic failure/)
await page.evaluate(() => (window as any).fixture.removeTool(0))
await tool.waitFor({ state: "detached" })
await page.evaluate(() => (window as any).fixture.clearChild())
assert.equal(await page.locator('.tool-call[data-part-id^="step-"]').count(), 0, "authoritative empty pages must clear stale displayed steps")
}))
test("a failed child refresh keeps the last display snapshot with an explicit retry error", async () => open("mode=task&count=12", async page => {
const tool = step(page)
await tool.waitFor()
await page.evaluate(() => (window as any).fixture.startText())
await frames(page)
await tool.locator(":scope > .tool-call-header > .tool-call-header-toggle").click()
await tool.locator("pre code").waitFor()
await tool.evaluate(element => { (window as any).originalStep = element })
await page.evaluate(() => (window as any).fixture.failRead())
await page.getByRole("alert").filter({ hasText: "Deterministic read failure" }).waitFor()
assert.equal(await tool.evaluate(element => element === (window as any).originalStep), true)
assert.equal(await tool.locator(":scope > .tool-call-header > .tool-call-header-toggle").getAttribute("aria-expanded"), "true")
await page.getByRole("alert").getByRole("button").click()
await page.getByRole("alert").waitFor({ state: "detached" })
assert.equal(await tool.evaluate(element => element === (window as any).originalStep), true)
}))
test("native page output must not reuse zero-revision Markdown when a tool is reopened", async () => open("mode=task&count=1", async page => {
const tool = step(page), toggle = tool.locator(":scope > .tool-call-header > .tool-call-header-toggle")
await tool.waitFor()
await toggle.click()
await tool.locator("pre code").waitFor()
await page.evaluate(() => (window as any).fixture.toolUpdate(0, "Replacement native page output"))
await page.waitForFunction(() => (window as any).fixture.toolOutput() === "Replacement native page output")
await frames(page)
// The old renderer closes on reprojection. Reopen it to distinguish a stale
// render-cache hit from the separately asserted disclosure/remount defect.
if (await toggle.getAttribute("aria-expanded") === "false") await toggle.click()
await tool.locator("pre code").waitFor()
await frames(page)
assert.equal((await tool.locator("pre code").textContent())?.trim() === "Replacement native page output", true,
"the native record changed but the displayed Markdown is stale")
await tool.locator(":scope > .tool-call-header > .tool-call-header-copy").first().click()
assert.equal((await page.evaluate(() => (window as any).fixture.snapshot())).clipboard.at(-1), "Replacement native page output")
}))
test("changing an indexed task key does not transfer a removed tool's disclosure", async () => open("mode=task&count=200", async page => {
const first = step(page)
await first.waitFor()
await first.locator(":scope > .tool-call-header > .tool-call-header-toggle").click()
await first.locator("pre code").waitFor()
await page.evaluate(() => (window as any).fixture.appendTool())
await first.waitFor({ state: "detached" })
assert.equal(await step(page, 1).locator(":scope > .tool-call-header > .tool-call-header-toggle").getAttribute("aria-expanded"), "false")
assert.equal(await page.locator('.tool-call[data-part-id="appended-step"]').count(), 1)
}))
for (const retainedIndex of [0, 1]) {
test(`authoritative task membership 2->1->2 preserves retained step-${retainedIndex}`, async () => open("mode=task&count=2", async page => {
const retained = step(page, retainedIndex)
const counter = page.locator('.tool-call[data-part-id="parent-task"] .tool-call-task-section-meta').filter({ hasText: /^\d+ steps$/ })
await retained.waitFor()
assert.equal(await counter.textContent(), "2 steps")
await retained.locator(":scope > .tool-call-header > .tool-call-header-toggle").click()
const output = retained.locator(".tool-call-markdown")
await retained.locator("pre code").waitFor()
await frames(page)
await retained.evaluate(element => { (window as any).retainedShell = element })
await output.evaluate(element => { (window as any).retainedScroller = element; element.scrollTop = 240 })
await page.evaluate(index => (window as any).fixture.replaceChildTools([index]), retainedIndex)
await frames(page)
assert.equal(await page.locator('.tool-call[data-part-id^="step-"]').count(), 1)
assert.equal(await counter.textContent(), "1 steps", "removed parts must also leave the membership counter")
await page.evaluate(index => (window as any).fixture.replaceChildTools([index, 2]), retainedIndex)
await frames(page)
assert.equal(await counter.textContent(), "2 steps")
assert.equal(await step(page, 2).count(), 1, "new identity at the former cardinality must appear exactly once")
assert.equal(await retained.evaluate(element => element === (window as any).retainedShell), true)
assert.equal(await retained.locator(":scope > .tool-call-header > .tool-call-header-toggle").getAttribute("aria-expanded"), "true")
assert.equal(await output.evaluate(element => element === (window as any).retainedScroller), true)
assert.equal(await output.evaluate(element => element.scrollTop), 240)
}))
}
test("authoritative task membership replaces and reorders equal-cardinality identities", async () => open("mode=task&count=2", async page => {
const retained = step(page, 0)
const counter = page.locator('.tool-call[data-part-id="parent-task"] .tool-call-task-section-meta').filter({ hasText: /^\d+ steps$/ })
await retained.waitFor()
await retained.locator(":scope > .tool-call-header > .tool-call-header-toggle").click()
await retained.locator("pre code").waitFor()
await retained.evaluate(element => { (window as any).retainedShell = element })
await page.evaluate(() => (window as any).fixture.replaceChildTools([2, 0]))
await frames(page)
assert.equal(await counter.textContent(), "2 steps")
assert.deepEqual(await page.locator('.tool-call[data-part-id^="step-"]').evaluateAll(elements => elements.map(element => element.getAttribute("data-part-id"))), ["step-2", "step-0"])
assert.equal(await step(page, 1).count(), 0)
assert.equal(await retained.evaluate(element => element === (window as any).retainedShell), true)
assert.equal(await retained.locator(":scope > .tool-call-header > .tool-call-header-toggle").getAttribute("aria-expanded"), "true")
}))
test("authoritative task membership drops a tool changed to text under the same part id", async () => open("mode=task&count=2", async page => {
await step(page, 1).waitFor()
await page.evaluate(() => (window as any).fixture.replaceChildTools([0, 1], [1]))
await frames(page)
const counter = page.locator('.tool-call[data-part-id="parent-task"] .tool-call-task-section-meta').filter({ hasText: /^\d+ steps$/ })
assert.equal(await counter.textContent(), "1 steps")
assert.equal(await step(page, 1).count(), 0)
assert.equal(await step(page, 0).count(), 1)
}))
test("authoritative task membership keeps the 200-step bound and clears stale truncation after shrink", async () => open("mode=task&count=201", async page => {
await step(page, 200).waitFor()
const counter = page.locator('.tool-call[data-part-id="parent-task"] .tool-call-task-section-meta').filter({ hasText: /^\d+\+? steps$/ })
assert.equal(await counter.textContent(), "200+ steps")
assert.equal(await page.locator('.tool-call[data-part-id^="step-"]').count(), 200)
await page.evaluate(() => (window as any).fixture.replaceChildTools(Array.from({ length: 199 }, (_, index) => index + 2)))
await frames(page)
assert.equal(await counter.textContent(), "199 steps")
assert.equal(await page.locator('.tool-call[data-part-id^="step-"]').count(), 199)
assert.equal(await page.locator('.tool-call[data-part-id="parent-task"] .tool-call-diagnostic-message').count(), 0)
}))
for (const { count, legacy } of [{ count: 2, legacy: false }, { count: 201, legacy: false }, { count: 201, legacy: true }]) {
test(`native batched child deletion clears task membership (count=${count}, legacy=${legacy})`, async () => open(`mode=task&count=${count}${legacy ? "&legacy=1" : ""}`, async page => {
const parent = page.locator('.tool-call[data-part-id="parent-task"]')
const counter = parent.locator(".tool-call-task-section-meta").filter({ hasText: /^\d+\+? steps$/ })
await step(page, count - 1).waitFor()
assert.equal(await counter.textContent(), `${count > 200 ? "200+" : count} steps`)
assert.equal(await page.locator('.tool-call[data-part-id^="step-"]').count(), Math.min(count, 200))
assert.equal(await parent.locator('[data-task-id="legacy-step"]').count(), 0, "native steps supersede the parent's legacy summary")
await parent.evaluate(element => { (window as any).originalParentTask = element })
await page.evaluate(() => (window as any).fixture.deleteChild())
await frames(page)
const state = await page.evaluate(() => (window as any).fixture.snapshot())
assert.deepEqual(state.childMessageIds, [])
assert.equal(state.childLoaded, false)
assert.equal(state.childExists, false)
assert.equal(state.parentChildId, "child", "native child deletion must not need a parent metadata update")
assert.equal(state.loaded, 1, "parent transcript remains resident")
assert.equal(await parent.evaluate(element => element === (window as any).originalParentTask), true)
assert.equal(await page.locator('.tool-call[data-part-id^="step-"]').count(), 0)
assert.equal(await parent.locator(".tool-call-diagnostic-message").count(), 0, "deleted native membership must clear its truncation state")
if (legacy) {
assert.equal(await counter.textContent(), "1 steps", "fallback count must use the parent's legacy summary, not deleted native keys")
assert.equal(await parent.locator('[data-task-id="legacy-step"]').textContent().then(text => text?.includes("Parent legacy summary")), true)
} else {
assert.equal(await counter.count(), 0, "a deleted child without legacy steps must not retain a ghost count")
assert.equal(await parent.getByText("Steps", { exact: true }).count(), 0)
}
}))
}
test("audit sample: deterministic native child deltas (timings are observations, not a CI threshold)", async () => open(`mode=task&count=80&instrument=${process.env.CODENOMAD_RENDER_INSTRUMENT ?? "on"}`, async page => {
await step(page, 79).waitFor()
await page.evaluate(() => (window as any).fixture.startText())
await frames(page)
await page.evaluate(() => (window as any).fixture.measure(3))
await frames(page)
const samples = []
const cdp = await page.context().newCDPSession(page)
await cdp.send("Performance.enable")
const metrics = async () => {
const { metrics } = await cdp.send("Performance.getMetrics")
const ms = (name: string) => metrics.find(metric => metric.name === name)!.value * 1000
return { scriptMs: ms("ScriptDuration"), taskMs: ms("TaskDuration"), layoutMs: ms("LayoutDuration"), styleMs: ms("RecalcStyleDuration") }
}
for (let index = 0; index < 10; index++) {
const before = await metrics()
const sync = await page.evaluate(() => { const f = (window as any).fixture; f.reset(); return f.measure(1) })
await frames(page)
const state = await page.evaluate(() => (window as any).fixture.snapshot())
const after = await metrics()
samples.push({ syncMs: sync.ms, scriptMs: after.scriptMs - before.scriptMs, taskMs: after.taskMs - before.taskMs,
layoutMs: after.layoutMs - before.layoutMs, styleMs: after.styleMs - before.styleMs,
snapshots: state.snapshots, added: state.added, removed: state.removed })
}
console.log("render-cost-native-deltas", JSON.stringify({ variant: process.env.CODENOMAD_RENDER_TASK_BASELINE ? "baseline" : "fixed",
browser: browser.version(), instrumented: process.env.CODENOMAD_RENDER_INSTRUMENT !== "off", samples }))
if (!process.env.CODENOMAD_RENDER_TASK_BASELINE && process.env.CODENOMAD_RENDER_INSTRUMENT !== "off") {
assert.ok(samples.every(sample => sample.added === 0 && sample.removed === 0), "structural rescans must not remount unchanged task shells")
}
assert.equal(await page.locator('.tool-call[data-part-id^="step-"]').count(), 80)
}))
test("large tool output bounds rendered text but preserves full clipboard source", async () => open("mode=task&count=1&size=2000000", async page => {
const tool = step(page)
await tool.waitFor()
assert.equal(await tool.locator(".tool-call-details").count(), 0, "collapsed steps must not mount their bodies")
const start = await page.evaluate(() => performance.now())
await tool.locator(":scope > .tool-call-header > .tool-call-header-toggle").click()
await tool.locator("pre code").waitFor()
const content = await tool.locator("pre code").textContent()
assert.ok(content && content.length <= 10_000)
assert.ok((await tool.textContent())!.includes("Output truncated"))
console.log("large-output", { renderedCharacters: content!.length, ...await page.evaluate(start => ({ msToObservedRender: performance.now() - start, nodes: document.querySelectorAll("*").length }), start) })
await tool.locator(":scope > .tool-call-header > .tool-call-header-copy").first().click()
const state = await page.evaluate(() => (window as any).fixture.snapshot())
assert.equal(state.clipboard[0], state.output)
assert.equal(state.output.length, 2_000_000)
}))
test("transcript loads one bounded history page and mounts only virtual rows", async () => open("mode=transcript&count=1000", async page => {
await page.waitForFunction(() => document.querySelector(".message-stream")?.textContent?.includes("History 999"))
await frames(page)
const state = await page.evaluate(() => (window as any).fixture.snapshot())
const rows = await page.locator("[data-virtual-follow-key]").count()
const markers = await page.locator(".timeline-virtual-row").count()
console.log("transcript-bounds", { loaded: state.loaded, rows, markers, requests: state.requests })
assert.equal(state.loaded, 200)
assert.ok(rows < 40)
assert.ok(markers < 80)
}))
test("timeline exact extent, bounded DOM and active highlight updates do not mount messages", async () => open("mode=timeline&count=10000", async page => {
await page.locator('.message-timeline-segment[data-message-id="message-0"]').first().waitFor()
await frames(page)
const samples = []
for (let index = 0; index < 10; index++) {
samples.push(await page.evaluate(() => (window as any).fixture.measure(30)))
await frames(page)
}
console.log("timeline-active-updates", JSON.stringify({ samples, snapshot: await page.evaluate(() => (window as any).fixture.snapshot()) }))
const geometry = await page.evaluate(() => {
const content = document.querySelector<HTMLElement>(".timeline-virtual-content")!
const marker = document.querySelector<HTMLElement>(".timeline-measure button")!
const gap = document.querySelector<HTMLElement>(".timeline-measure div")!
const markerHeight = parseFloat(getComputedStyle(marker).height), gapHeight = parseFloat(getComputedStyle(gap).height)
// Each 3-marker group has a double gap before its tool and following user.
let units = 0
for (let index = 1; index < 10000; index++) units += index % 3 === 2 ? 1 : 2
return { extent: parseFloat(content.style.height), expected: markerHeight * 10000 + gapHeight * units, mounted: document.querySelectorAll(".timeline-virtual-row").length }
})
console.log("timeline-geometry", geometry)
// CSS serialization and native scroll extents round at this magnitude.
assert.ok(Math.abs(geometry.extent - geometry.expected) < 1)
assert.ok(geometry.mounted < 80)
assert.equal(await page.locator(".tool-call, [data-view=message-item]").count(), 0)
await page.locator('.message-timeline-segment[data-message-id="message-0"]').first().hover()
await page.getByRole("tooltip").locator("strong").waitFor()
assert.ok((await page.getByRole("tooltip").textContent())!.length <= 4096)
await page.locator('.message-timeline-segment[data-message-id="message-0"]').first().click()
assert.equal((await page.evaluate(() => (window as any).fixture.snapshot())).selected, "marker-0")
}))

View file

@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"
import { chromium, type Browser, type Page } from "playwright"
import { createServer, type ViteDevServer } from "vite"
import solid from "vite-plugin-solid"
import { runWithDiagnosticCleanup } from "./fixture-diagnostic-boundary"
const root = fileURLToPath(new URL("../..", import.meta.url))
let server: ViteDevServer, browser: Browser, baseUrl: string
@ -34,7 +35,15 @@ after(async () => { await browser?.close(); await server?.close() })
async function open(name: string, run: (page: Page) => Promise<void>) {
const page = await browser.newPage({ viewport: { width: 1100, height: 700 }, locale: "en-US" })
const errors: string[] = []
page.on("pageerror", error => errors.push(error.message))
const boundaryDiagnostics: Array<Record<string, unknown>> = []
page.on("pageerror", error => {
errors.push(error.message)
if (process.env.CODENOMAD_BROWSER_BOUNDARY_TRACE) boundaryDiagnostics.push({ kind: "pageerror", message: error.message, stack: error.stack })
})
if (process.env.CODENOMAD_BROWSER_BOUNDARY_TRACE) {
page.on("console", message => boundaryDiagnostics.push({ kind: "console", type: message.type(), text: message.text() }))
page.on("requestfailed", request => boundaryDiagnostics.push({ kind: "requestfailed", url: request.url(), error: request.failure()?.errorText }))
}
await page.addInitScript(`(() => {
window.fixtureScrollEvents = [];
for (const type of ['wheel', 'scroll', 'pointerdown']) document.addEventListener(type, event => {
@ -46,18 +55,23 @@ async function open(name: string, run: (page: Page) => Promise<void>) {
}, { capture: true, passive: true });
})()`)
await page.route("**/api/**", route => route.fulfill({ contentType: route.request().url().includes("events") ? "text/event-stream" : "application/json", body: "" }))
try {
await page.goto(`${baseUrl}/fixture?${name}`)
await page.waitForFunction(() => Boolean((window as any).fixture))
await run(page)
assert.deepEqual(errors, [])
} catch (error) {
console.error("Browser fixture failure", name, await page.evaluate(() => ({
state: (window as any).fixture?.snapshot?.(), events: (window as any).fixtureScrollEvents,
streams: Array.from(document.querySelectorAll(".message-stream")).map(el => ({ top: el.scrollTop, height: el.scrollHeight, viewport: el.clientHeight })),
})))
throw error
} finally { await page.close() }
await runWithDiagnosticCleanup({
run: async () => {
await page.goto(`${baseUrl}/fixture?${name}`)
await page.waitForFunction(() => Boolean((window as any).fixture))
await run(page)
assert.deepEqual(errors, [])
},
diagnose: async () => {
console.error("Browser fixture failure", name, await page.evaluate(() => ({
state: (window as any).fixture?.snapshot?.(), events: (window as any).fixtureScrollEvents,
streams: Array.from(document.querySelectorAll(".message-stream")).map(el => ({ top: el.scrollTop, height: el.scrollHeight, viewport: el.clientHeight })),
})), { boundaryDiagnostics })
},
cleanup: () => page.close(),
onObservationError: error => console.error("Browser fixture diagnostic capture failed", name, error, { boundaryDiagnostics }),
onCleanupError: error => console.error("Browser fixture cleanup failed", name, error, { boundaryDiagnostics }),
})
}
test("user HTML remains literal through live delivery and history while assistant HTML still renders", async () => {
@ -80,7 +94,7 @@ test("user HTML remains literal through live delivery and history while assistan
})
test("user HTML mode preserves Markdown, pasted disclosures and code source and fences legacy caches", async () => {
await open("user-html", async page => {
await open(process.env.CODENOMAD_BROWSER_BOUNDARY_TRACE ? "user-html&trace=1" : "user-html", async page => {
await page.locator("#root").evaluate(root => { root.style.display = "block"; root.style.height = "auto" })
await page.locator("#user strong").waitFor()
await page.locator("#assistant .sample-html").waitFor()

View file

@ -0,0 +1,57 @@
import { DatabaseSync } from "node:sqlite"
import { performance } from "node:perf_hooks"
import { setImmediate as turn } from "node:timers/promises"
import { execFileSync } from "node:child_process"
import { fileURLToPath } from "node:url"
import { buildSync } from "esbuild"
import { readSessionOutline as currentOutline } from "../packages/server/src/opencode/session-pruning/outline-index"
const baseline = process.argv.includes("--baseline")
let readSessionOutline = currentOutline
if (baseline) {
// Compile the immutable pre-mission source without changing the working tree.
const directory = fileURLToPath(new URL("../packages/server/src/opencode/session-pruning/", import.meta.url))
const source = execFileSync("git", ["show", "366830f6:packages/server/src/opencode/session-pruning/outline-index.ts"],
{ cwd: directory, encoding: "utf8" })
const built = buildSync({ stdin: { contents: source, loader: "ts", resolveDir: directory }, bundle: true,
platform: "node", format: "esm", write: false })
readSessionOutline = (await import(`data:text/javascript;base64,${Buffer.from(built.outputFiles[0].text).toString("base64")}`)).readSessionOutline
}
// Synthetic memory-only database. Never discovers or opens an OpenCode database.
const count = Number(process.argv[2] ?? 256)
const bytes = Number(process.argv[3] ?? 262144)
if (!Number.isInteger(count) || count < 1 || count > 1024 || !Number.isInteger(bytes) || bytes < 1 || bytes > 1048576) {
throw new Error("Use 1–1024 rows and 1–1048576 payload bytes per row")
}
const db = new DatabaseSync(":memory:")
try {
db.exec(`CREATE TABLE session_v2(id TEXT,directory TEXT,project_id TEXT,workspace_id TEXT,revert TEXT);
CREATE TABLE session_message(id TEXT PRIMARY KEY,session_id TEXT,type TEXT,seq INTEGER,data TEXT,time_updated INTEGER DEFAULT 0);
CREATE UNIQUE INDEX session_message_session_seq_idx ON session_message(session_id,seq);
INSERT INTO session_v2 VALUES ('s','/repo','p',NULL,NULL);`)
const data = JSON.stringify({ content: [{ type: "tool", name: "read", state: { content: [{ type: "text", text: "x".repeat(bytes) }] } }] })
const insert = db.prepare("INSERT INTO session_message(id,session_id,type,seq,data) VALUES (?,'s','assistant',?,?)")
for (let i = 0; i < count; i++) insert.run(`m${i}`, i, data)
for (let sample = 0; sample < 3; sample++) {
await turn()
const gaps: number[] = []
let previous = performance.now()
const start = previous
let stopped = false
const tick = () => {
if (stopped) return
const now = performance.now()
gaps.push(now - previous)
previous = now
setImmediate(tick)
}
setImmediate(tick)
const result = await readSessionOutline(db, { directory: "/repo", projectID: "p", sessionID: "s" }, undefined, new AbortController().signal)
const elapsed = performance.now() - start
await turn()
stopped = true
console.log(JSON.stringify({ baseline, sample, count, bytes, entries: result.status === "outline" ? result.entries.length : -1,
elapsedMs: Math.round(elapsed), maxTurnGapMs: Math.round(Math.max(...gaps)), turns: gaps.length }))
}
} finally { db.close() }

View file

@ -0,0 +1,383 @@
// Explicit CLI only: no discovery, shared service, user database or external provider.
import assert from "node:assert/strict"
import { execFileSync, spawn } from "node:child_process"
import { createHash } from "node:crypto"
import { createServer } from "node:http"
import { mkdtemp, mkdir, realpath, writeFile } from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { setTimeout as delay } from "node:timers/promises"
import { fileURLToPath } from "node:url"
import { OpenCode } from "@opencode/client"
import { build } from "esbuild"
const cli = process.argv[2]
if (!cli || !path.isAbsolute(cli)) throw new Error("Pass an absolute isolated CLI executable")
const contextFlag = process.argv.indexOf("--context-mib")
const contextMiB = contextFlag < 0 ? 0 : Number(process.argv[contextFlag + 1])
if (![0, 2, 8, 32].includes(contextMiB)) throw new Error("--context-mib accepts only 0, 2, 8 or 32")
const skipOutline = process.argv.includes("--skip-outline")
const temporaryRoot = path.join(os.tmpdir(), "opencode")
await mkdir(temporaryRoot, { recursive: true })
const root = await realpath(await mkdtemp(path.join(temporaryRoot, "compaction-isolation-")))
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(OPENCODE_|XDG_)/i.test(key)))
for (const key of ["XDG_DATA_HOME", "XDG_CONFIG_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"]) env[key] = path.join(root, key)
Object.assign(env, {
HOME: root, USERPROFILE: root, OPENCODE_TEST_HOME: root,
OPENCODE_CONFIG_DIR: path.join(root, "config"), OPENCODE_DB: path.join(root, "test.db"),
OPENCODE_SERVER_PASSWORD: "private-compaction-fixture", OPENCODE_CONFIG_PROJECT_DISABLE: "1",
OPENCODE_DISABLE_MODELS_FETCH: "1", OPENCODE_DISABLE_FFF: "1",
})
const runtimeVersion = execFileSync(cli, ["--version"], { cwd: root, env, encoding: "utf8" }).trim().replace(/^opencode2? v/, "")
assert.match(runtimeVersion, /^2\.0\.(19|21)$/, "This fixture qualifies only the requested runtimes")
await Promise.all(["config", "plugin", "pruning"].map(name => mkdir(path.join(root, name), { recursive: true })))
const bundle = await build({
entryPoints: [fileURLToPath(new URL("../packages/server/src/opencode/session-pruning/plugin.ts", import.meta.url))],
bundle: true, platform: "node", format: "esm", target: "node22", write: false,
banner: { js: 'import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);' },
})
await writeFile(path.join(root, "pruning", "index.mjs"), bundle.outputFiles[0].contents)
await writeFile(path.join(root, "plugin", "index.ts"), `
export default { id:'private-compaction-fixture', async setup(ctx) {
await ctx.session.hook('http.request', event => {
event.request.headers.set('x-fixture-kind', event.kind)
event.request.headers.set('x-fixture-session', event.sessionID)
})
await ctx.tool.transform(editor => editor.add({ name:'isolation_probe', description:'Inert fixture',
input:{type:'object',properties:{}}, options:{codemode:false}, execute:async()=>({content:'Synthetic tool result'}) }))
} }
`)
const gates = new Map(), requests = [], toolIssued = new Set()
const gate = key => {
let release
const promise = new Promise(resolve => { release = resolve })
const value = { promise, release, released: false }
gates.set(key, value)
return () => { value.released = true; release(); gates.delete(key) }
}
const summary = ["## Objective", "## Requirements", "## Decisions", "## Work State", "### Completed",
"### Active", "### Blocked", "## Next Move", "## Relevant Files", "## Important Context"]
.map(heading => `${heading}\n- Synthetic isolation fixture`).join("\n\n")
let providerError
const provider = createServer(async (request, response) => {
try {
const started = performance.now()
let raw = ""
for await (const chunk of request) raw += chunk
const bodyReceived = performance.now()
const body = JSON.parse(raw), kind = request.headers["x-fixture-kind"], sessionID = request.headers["x-fixture-session"]
const record = { kind, sessionID, started, bodyReceived, received: performance.now(), bytes: Buffer.byteLength(raw),
messageCount: body.messages?.length, markerCount: (raw.match(/NATIVE_CONTEXT_CANARY_\d+/g) ?? []).length,
payloadFillBytes: (raw.match(/Z{1024,}/g) ?? []).reduce((sum, fill) => sum + fill.length, 0) }
requests.push(record)
const pending = gates.get(`${kind}:${sessionID}`)
if (pending) await pending.promise
if (response.destroyed) return
const text = kind === "compaction" ? summary : "Synthetic conclusion"
if (!body.stream) {
response.setHeader("Content-Type", "application/json")
response.end(JSON.stringify({ id: "aux", choices: [{ message: { role: "assistant", content: text }, finish_reason: "stop" }],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 } }))
return
}
response.setHeader("Content-Type", "text/event-stream")
const chunk = (delta, finish_reason = null) => response.write(`data: ${JSON.stringify({
id: "fixture", object: "chat.completion.chunk", model: "fixture",
choices: [{ index: 0, delta, finish_reason }],
...(finish_reason ? { usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 } } : {}),
})}\n\n`)
chunk({ role: "assistant" })
const tool = kind === "primary" && !toolIssued.has(sessionID)
if (tool) {
toolIssued.add(sessionID)
chunk({ tool_calls: [{ index: 0, id: `inert-${sessionID}`, type: "function", function: { name: "isolation_probe", arguments: "{}" } }] })
} else {
// Exercise real compaction delta events, without an artificial slow observer.
for (let i = 0; i < text.length; i += 16) chunk({ content: text.slice(i, i + 16) })
}
chunk({}, tool ? "tool_calls" : "stop")
response.end("data: [DONE]\n\n")
record.responded = performance.now()
} catch (error) { providerError = error; response.destroy(error) }
})
await new Promise(resolve => provider.listen(0, "127.0.0.1", resolve))
env.OPENCODE_CONFIG_CONTENT = JSON.stringify({
model: "fixture/fixture",
// Fixture-only: preserve the entire imported corpus in a manual summary request.
...(contextMiB ? { compaction: { auto: false, keep: { tokens: 0 } } } : {}),
providers: { fixture: { package: "@opencode/ai/providers/openai-compatible", settings: {
baseURL: `http://127.0.0.1:${provider.address().port}/v1`, apiKey: "fixture-only",
}, models: { fixture: contextMiB ? { limit: { context: 16777216, output: 8192 } } : {} } } },
plugins: [path.join(root, "plugin"), { package: path.join(root, "pruning"), options: { databasePath: env.OPENCODE_DB } }],
})
let output = "", observerError, spawnError
const child = spawn(cli, ["serve", "--hostname", "127.0.0.1", "--port", "0", "--log-level", "debug", "--print-logs"], { cwd: root, env, windowsHide: true })
const stopped = new Promise(resolve => child.once("close", resolve))
child.on("error", error => { spawnError = error })
child.stdout.on("data", data => { output += data })
child.stderr.on("data", data => { output += data })
const streams = new AbortController(), events = [], measurements = []
let phase = "setup"
const report = { runtimeVersion, root, pid: child.pid, contextMiB, skipOutline, measurements, requests, outline: undefined,
pruningBundleSha256: createHash("sha256").update(bundle.outputFiles[0].contents).digest("hex") }
let observer, client, releaseA, releaseB
const options = () => ({ signal: AbortSignal.timeout(15_000) })
async function until(predicate, timeout = 30_000) {
const end = performance.now() + timeout
while (performance.now() < end) {
if (providerError) throw providerError
if (observerError) throw observerError
if (spawnError) throw spawnError
if (child.exitCode !== null) throw new Error("Private server exited early")
if (await predicate()) return
await delay(20)
}
throw new Error("Private fixture condition timed out")
}
async function measured(name, call) {
const started = performance.now()
const startedPhase = phase
try { const value = await call(); const finished = performance.now(); measurements.push({ name, started, finished, startedPhase, finishedPhase: phase, ms: finished - started, status: "ok" }); return value }
catch (error) { const finished = performance.now(); measurements.push({ name, started, finished, startedPhase, finishedPhase: phase, ms: finished - started, status: "failed" }); throw error }
}
const wait = sessionID => client.session.wait({ sessionID }, options())
function distribution(values) {
const sorted = [...values].sort((a, b) => a - b)
return { count: sorted.length, min: sorted[0], median: sorted[Math.floor(sorted.length / 2)], max: sorted.at(-1) }
}
try {
await until(() => /http:\/\/127\.0\.0\.1:\d+/.test(output))
const baseUrl = output.match(/http:\/\/127\.0\.0\.1:\d+/)[0]
report.baseUrl = baseUrl
client = OpenCode.make({ baseUrl, headers: { Authorization: `Basic ${Buffer.from("opencode:private-compaction-fixture").toString("base64")}` } })
const info = await client.server.info(options())
assert.equal(info.version, runtimeVersion)
assert.equal(info.pid, child.pid, "The endpoint must belong to the child we created")
observer = (async () => {
for await (const event of client.event.subscribe({ signal: streams.signal })) {
events.push({ phase, type: event.type, sessionID: event.data?.sessionID, created: event.created, received: Date.now() })
}
})().catch(error => { if (!streams.signal.aborted) observerError = error })
await until(() => events.some(event => event.type === "server.connected"))
const location = { directory: root }
let a = await client.session.create({ location, title: "Private compaction A" }, options())
const b = await client.session.create({ location, title: "Private witness B" }, options())
report.sessions = { a: a.id, b: b.id }
await until(async () => {
const plugins = (await client.plugin.list({ location }, options())).data
const failed = plugins.filter(item => item.source.type !== "builtin" && item.state.status === "failed")
assert.equal(failed.length, 0, JSON.stringify(failed))
return plugins.some(item => item.id === "codenomad-session-pruning" && item.state.status === "active")
})
phase = "seed"
for (const session of [a, b]) {
await client.session.prompt({ sessionID: session.id, text: "Use isolation_probe, then conclude." }, options())
await wait(session.id)
}
const nativeMessages = (await client.message.list({ sessionID: a.id, limit: 20 }, options())).data
const template = nativeMessages.find(message => message.type === "assistant" && message.content.some(part => part.type === "tool"))
assert(template, "A real native assistant/tool template must exist")
const userTemplate = nativeMessages.find(message => message.type === "user")
assert(userTemplate)
const tool = template.content.find(part => part.type === "tool")
assert.equal(tool.state.status, "completed")
if (contextMiB) {
const exportedA = await client.session.export({ sessionID: a.id }, options())
const count = contextMiB * 4, bytes = 262144
let fillBytes = 0
const imported = Array.from({ length: count }, (_, index) => {
const marker = `NATIVE_CONTEXT_CANARY_${String(index).padStart(4, "0")}\n`
const text = marker + "Z".repeat(bytes - marker.length)
fillBytes += bytes - marker.length
return [
{ ...structuredClone(userTemplate), id: `msg_context_user_${String(index).padStart(4, "0")}`, text: `Inspect synthetic item ${index}.` },
{ ...structuredClone(template), id: `msg_context_assistant_${String(index).padStart(4, "0")}`,
content: [{ ...structuredClone(tool), id: `tool_context_${index}`, state: { ...structuredClone(tool.state), content: [{ type: "text", text }] } }] },
]
}).flat()
// Native manual compaction retains the current turn even with keep.tokens=0.
// A small trailing turn puts every large tool result in the summarized prefix.
imported.push(
{ ...structuredClone(userTemplate), id: "msg_context_tail_user", text: "Corpus complete; retain this small trailing turn." },
{ ...structuredClone(template), id: "msg_context_tail_assistant", content: [{ type: "text", text: "Synthetic trailing conclusion" }] },
)
await client.session.remove({ sessionID: a.id }, options())
a = await client.session.import({ ...exportedA, messages: imported, location }, { signal: AbortSignal.timeout(60_000) })
report.sessions.a = a.id
const stored = await client.session.context({ sessionID: a.id }, { signal: AbortSignal.timeout(60_000) })
const storedTools = stored.filter(message => message.type === "assistant").flatMap(message => message.content.filter(part => part.type === "tool"))
const storedBytes = storedTools.reduce((sum, part) => sum + part.state.content.reduce((total, content) => total + Buffer.byteLength(content.text ?? ""), 0), 0)
assert.equal(storedTools.length, count)
assert.equal(storedBytes, contextMiB * 1048576)
assert.equal(stored.length, imported.length)
report.contextCorpus = { turns: count + 1, payloadTurns: count, messages: imported.length, payloadBytes: storedBytes, payloadFillBytes: fillBytes,
keepTokens: 0, modelContextLimit: 16777216 }
}
// Seeding is outside the timed compaction/outline intervals and uses native import.
let corpus
if (!skipOutline) {
const empty = await client.session.create({ location, title: "Private outline corpus" }, options())
const exported = await client.session.export({ sessionID: empty.id }, options())
await client.session.remove({ sessionID: empty.id }, options())
const payload = "x".repeat(262144)
const messages = Array.from({ length: 128 }, (_, index) => ({ ...structuredClone(template), id: `msg_isolation_${index}`,
content: [{ ...structuredClone(tool), id: `tool_isolation_${index}`, state: { ...structuredClone(tool.state), content: [{ type: "text", text: payload }] } }],
}))
corpus = await client.session.import({ ...exported, messages, location }, { signal: AbortSignal.timeout(60_000) })
}
// Flush seed observations before the measured phase; import is not compaction.
await delay(100)
phase = "assembly-before-provider"
const compactStart = performance.now()
releaseA = gate(`compaction:${a.id}`)
const admission = measured("A.compact.admission", () => client.session.compact({ sessionID: a.id }, options()))
admission.catch(() => {})
let probingAssembly = true
const assemblyProbes = (async () => {
while (probingAssembly && !requests.some(item => item.kind === "compaction" && item.sessionID === a.id)) {
await Promise.all([
measured("assembly.info", () => client.server.info(options())),
measured("assembly.B.get", () => client.session.get({ sessionID: b.id }, options())),
])
if (probingAssembly) await delay(5)
}
})()
assemblyProbes.catch(() => {})
const earlyB = (async () => {
await measured("assembly.B.prompt.admission", () => client.session.prompt({ sessionID: b.id, text: "Conclude during A summary assembly." }, options()))
await measured("assembly.B.wait", () => wait(b.id))
assert.equal((await client.session.get({ sessionID: b.id }, options())).outcome, "succeeded")
})()
earlyB.catch(() => {})
const admitted = await admission
assert.equal(typeof admitted.time.created, "number")
try { await until(() => requests.some(item => item.kind === "compaction" && item.sessionID === a.id)) }
finally { probingAssembly = false; await assemblyProbes }
const assembled = requests.find(item => item.kind === "compaction" && item.sessionID === a.id)
const admittedAt = measurements.find(item => item.name === "A.compact.admission").finished
report.compactionAssembly = { submissionToProviderHeadersMs: assembled.started - compactStart,
admissionResponseToProviderHeadersMs: assembled.started - admittedAt,
submissionToProviderBodyMs: assembled.bodyReceived - compactStart, providerParseMs: assembled.received - assembled.bodyReceived,
providerBytes: assembled.bytes, providerMessages: assembled.messageCount, markerCount: assembled.markerCount, payloadFillBytes: assembled.payloadFillBytes }
if (contextMiB) {
assert.equal(assembled.markerCount, report.contextCorpus.payloadTurns, "Provider must receive every corpus tool marker")
assert.equal(assembled.payloadFillBytes, report.contextCorpus.payloadFillBytes, "Stored corpus bytes must really reach the compaction provider")
}
await until(() => events.some(event => event.sessionID === a.id && event.type === "session.compaction.started"))
await earlyB
assert(!events.some(event => event.sessionID === a.id && event.type === "session.compaction.ended"))
report.earlyBCompletedBeforeReleaseA = true
report.assemblyWitnesses = measurements.filter(item => item.name.startsWith("assembly.")).map(item => ({ ...item,
startedBeforeProviderHeaders: item.started < assembled.started, completedBeforeProviderHeaders: item.finished < assembled.started,
completedBeforeProviderBody: item.finished < assembled.bodyReceived }))
for (const name of ["assembly.info", "assembly.B.get", "assembly.B.prompt.admission"]) {
assert(report.assemblyWitnesses.some(item => item.name === name && item.startedBeforeProviderHeaders), `${name} must be issued before A reaches the provider`)
}
report.preProviderLatencyMs = Object.fromEntries([...new Set(report.assemblyWitnesses.map(item => item.name))]
.map(name => [name, distribution(report.assemblyWitnesses.filter(item => item.name === name && item.startedBeforeProviderHeaders).map(item => item.ms))]))
report.whollyPreProviderLatencyMs = Object.fromEntries([...new Set(report.assemblyWitnesses.map(item => item.name))]
.map(name => [name, distribution(report.assemblyWitnesses.filter(item => item.name === name && item.completedBeforeProviderHeaders).map(item => item.ms))]))
report.bCompletedBeforeProviderHeaders = report.assemblyWitnesses.some(item => item.name === "assembly.B.wait" && item.completedBeforeProviderHeaders)
phase = "held-compaction"
let aFinished = false
const waitingA = wait(a.id).then(() => { aFinished = true })
// Attach a rejection handler immediately; still await the actual result later.
waitingA.catch(() => {})
for (let sample = 0; sample < 4; sample++) {
await Promise.all([
measured("held.info", () => client.server.info(options())),
measured("held.A.get", () => client.session.get({ sessionID: a.id }, options())),
measured("held.B.get", () => client.session.get({ sessionID: b.id }, options())),
measured("held.active", () => client.session.active(options())),
])
await delay(50)
}
phase = "both-held"
releaseB = gate(`primary:${b.id}`)
const beforeB = requests.length
await measured("held.B.prompt.admission", () => client.session.prompt({ sessionID: b.id, text: "Conclude while A summary is held." }, options()))
await until(() => requests.slice(beforeB).some(item => item.kind === "primary" && item.sessionID === b.id))
report.activeWhileBothHeld = await measured("both-held.active", () => client.session.active(options()))
assert(report.activeWhileBothHeld[a.id] && report.activeWhileBothHeld[b.id], "Both native executions must be active")
await measured("both-held.B.get", () => client.session.get({ sessionID: b.id }, options()))
releaseB(); releaseB = undefined
await measured("A-held.B.wait", () => wait(b.id))
const completedB = await client.session.get({ sessionID: b.id }, options())
assert.equal(completedB.outcome, "succeeded", "B must succeed, not simply stop after an error")
const messagesB = (await client.message.list({ sessionID: b.id, limit: 8 }, options())).data
assert(messagesB.some(message => message.type === "assistant" && message.content.some(part => part.type === "text" && part.text === "Synthetic conclusion")))
assert.equal(aFinished, false, "B completed independently before releasing A")
assert(!events.some(event => event.sessionID === a.id && event.type === "session.compaction.ended"))
report.bCompletedBeforeReleaseA = true
if (!skipOutline) {
phase = "outline-under-held-compaction"
let outlining = true
const outlineStart = performance.now(), heartbeats = []
const heartbeat = (async () => {
while (outlining) {
const start = performance.now()
await client.server.info(options())
heartbeats.push({ startedMs: start - outlineStart, ms: performance.now() - start })
if (outlining) await delay(5)
}
})()
heartbeat.catch(() => {})
try {
const outline = (await measured("A-held.outline", () => client.rpc.call({ rpcID: "codenomad.session-pruning", method: "outline", location,
input: { sessionID: corpus.id } }, options()))).output
assert.equal(outline.status, "outline", JSON.stringify(outline))
assert.equal(outline.entries.length, 128)
assert(outline.entries.every(entry => entry.tools === 1 && entry.toolName === tool.name))
const elapsedMs = performance.now() - outlineStart
report.outline = { count: 128, payloadBytes: 262144, elapsedMs, heartbeats,
completedHeartbeatsDuringOutline: heartbeats.filter(item => item.startedMs + item.ms <= elapsedMs).length }
} finally { outlining = false; await heartbeat }
}
assert.equal(aFinished, false)
phase = "held-compaction"
// A genuine pending provider wait, not merely a race through a fast reply.
await delay(1_000)
await measured("held-final.B.get", () => client.session.get({ sessionID: b.id }, options()))
await measured("held-final.info", () => client.server.info(options()))
assert.equal(aFinished, false)
report.aHeldMs = performance.now() - compactStart
const releasedAt = performance.now()
phase = "summary-release"
releaseA(); releaseA = undefined
await waitingA
report.aCompletionAfterReleaseMs = performance.now() - releasedAt
const context = await client.session.context({ sessionID: a.id }, options())
assert(context.some(message => message.type === "compaction" && message.status === "completed"), "Real native compaction must finish successfully")
await until(() => events.some(event => event.sessionID === a.id && event.type === "session.compaction.ended"))
assert(!events.some(event => event.sessionID === a.id && event.type === "session.compaction.failed"))
report.eventCounts = Object.fromEntries([...new Set(events.map(event => event.type))].map(type => [type, events.filter(event => event.type === type).length]))
const business = events.filter(event => event.type.startsWith("session.") && typeof event.created === "number")
report.eventDeliveryLagByPhaseMs = Object.fromEntries([...new Set(business.map(event => event.phase))]
.map(name => [name, distribution(business.filter(event => event.phase === name).map(event => event.received - event.created))]))
report.events = events
report.latencyByOperationMs = Object.fromEntries([...new Set(measurements.map(item => item.name))]
.map(name => [name, distribution(measurements.filter(item => item.name === name).map(item => item.ms))]))
report.status = "passed"
console.log(JSON.stringify({ ...report, events: undefined, measurements: undefined, requests: undefined, assemblyWitnesses: undefined }, null, 2))
console.log(`PASS: B completed before releasing A's provider; corpus=${contextMiB} MiB, outline=${!skipOutline}`)
} catch (error) {
report.status = "failed"
report.error = String(error.stack ?? error)
throw error
} finally {
releaseA?.(); releaseB?.()
for (const value of gates.values()) value.release()
streams.abort()
await observer
child.kill()
await stopped
provider.closeAllConnections()
await new Promise(resolve => provider.close(resolve))
await writeFile(path.join(root, "server.log"), output)
await writeFile(path.join(root, "result.json"), JSON.stringify(report, null, 2))
console.log(`Private fixture retained at ${root}; only its child was stopped`)
}

View file

@ -4,7 +4,7 @@ import assert from "node:assert/strict"
import { setTimeout as delay } from "node:timers/promises"
import { tsImport } from "tsx/esm/api"
export async function testNativeEventRelay({ client, location }) {
export async function testNativeEventRelay({ client, location, service }) {
const { EventBus } = await tsImport("../packages/server/src/events/bus.ts", import.meta.url)
const { InstanceEventBridge } = await tsImport("../packages/server/src/workspaces/instance-events.ts", import.meta.url)
const controller = new AbortController()
@ -18,8 +18,8 @@ export async function testNativeEventRelay({ client, location }) {
list: () => records,
ownsDirectory: owns,
ownsLocation: (id, ref) => owns(id, ref.directory),
getSharedServiceClient: async () => client,
subscribeToSharedService: signal => client.event.subscribe({ signal }),
getSharedServiceClient: () => service ? service.client() : Promise.resolve(client),
subscribeToSharedService: signal => service ? service.subscribe({ signal }) : client.event.subscribe({ signal }),
invalidateWorktrees() {},
}
bus.on("instance.eventStatus", event => { if (event.status === "connected") connected = true })
@ -43,6 +43,26 @@ export async function testNativeEventRelay({ client, location }) {
try {
bus.publish({ type: "workspace.started", workspace: records[0] })
await until(() => connected && nativeConnected)
if (service) {
for (const mode of ["abort", "return", "throw"]) {
const local = new AbortController()
const wrapped = (await service.subscribe({ signal: local.signal }))[Symbol.asyncIterator]()
try {
assert.equal((await wrapped.next()).value.type, "server.connected")
if (mode === "abort") {
const next = wrapped.next()
local.abort()
assert.equal((await next).done, true)
} else if (mode === "throw") {
const failure = new Error("isolated subscriber failure")
await assert.rejects(wrapped.throw(failure), error => error === failure)
} else await wrapped.return()
await client.server.info()
assert.equal((await service.acquire()).client, client, `${mode} must retain the shared native client`)
} finally { local.abort(); await wrapped.return() }
}
console.log("PASS: native service subscriber abort/return/throw retain the healthy shared connection")
}
const session = await client.session.create({ location: { directory: location.directory } })
sessions.push(session.id)
const titles = Array.from({ length: 8 }, (_, index) => `relay-fixture-${index}`)
@ -56,6 +76,13 @@ export async function testNativeEventRelay({ client, location }) {
await until(() => renames(routed.filter(x => x.instanceId === "slow").map(x => x.event)).length === titles.length)
assert.deepEqual(renames(routed.filter(x => x.instanceId === "slow").map(x => x.event)), titles)
console.log("PASS: native relay isolates slow recipients, preserves session ordering and does not block another shared SDK subscriber")
if (service) {
bridge.shutdown()
await client.session.update({ sessionID: session.id, title: "after-relay-shutdown" })
await until(() => renames(native).includes("after-relay-shutdown"))
assert.equal((await service.acquire()).client, client)
console.log("PASS: relay shutdown leaves another native subscriber and shared client requests usable")
}
} finally {
release(false)
bridge.shutdown()

View file

@ -18,7 +18,7 @@ import { testNativeWorktreeFamily } from "./test-worktree-families-native.mjs"
import { testNativeWorktreeManagement } from "./test-native-worktree-management.mjs"
import { testNativeEventRelay } from "./test-native-event-relay.mjs"
export async function testNativeLocationIdentity({ client, connection, root }) {
export async function testNativeLocationIdentity({ client, connection, root, service }) {
const { contractProfile, runtimeIdentity } = await tsImport("../packages/server/src/opencode/compatibility/runtime.ts", import.meta.url)
const { locationRequestOptions } = await tsImport("../packages/server/src/opencode/compatibility/location.ts", import.meta.url)
const profile = connection.profile ? await connection.profile() : contractProfile(runtimeIdentity(connection.endpoint))
@ -36,7 +36,7 @@ export async function testNativeLocationIdentity({ client, connection, root }) {
const rootLocation = await client.location.get({ location: { directory } })
const worktreeLocation = await client.location.get({ location: { directory: worktree } })
assert.equal(rootLocation.project.id, worktreeLocation.project.id)
await testNativeEventRelay({ client, location: rootLocation })
await testNativeEventRelay({ client, location: rootLocation, service })
await testNativeWorktreeFamily({ client, profile, rootLocation, worktreeLocation })
await testNativeWorktreeManagement({ client, root })
if (profile !== "legacy") {

View file

@ -0,0 +1,57 @@
import { execFileSync, spawnSync } from "node:child_process"
import { createHash } from "node:crypto"
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { dirname, join, resolve } from "node:path"
import { fileURLToPath } from "node:url"
// Read-only source A/B in isolated loopback Chromium fixtures. No checkout,
// desktop, daemon, database or user-profile mutation is needed.
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const ui = join(root, "packages/ui")
const reference = "47f9e43e3fa1a3be59c0f94932a7af0a4be20660"
const source = "packages/ui/src/components/tool-call/renderers/task.tsx"
const repetitions = Number(process.argv[2] ?? 3)
if (!Number.isInteger(repetitions) || repetitions < 1 || repetitions > 10) throw new Error("Use 1–10 repetitions")
const directory = mkdtempSync(join(tmpdir(), "codenomad-render-cost-"))
const baselinePath = join(directory, "task-baseline.tsx")
writeFileSync(baselinePath, execFileSync("git", ["show", `${reference}:${source}`], { cwd: root }))
const runs = []
for (let repetition = 0; repetition < repetitions; repetition++) {
for (const variant of ["baseline", "fixed"]) {
const env = { ...process.env, CODENOMAD_RENDER_INSTRUMENT: "off" }
if (variant === "baseline") env.CODENOMAD_RENDER_TASK_BASELINE = baselinePath
else delete env.CODENOMAD_RENDER_TASK_BASELINE
const result = spawnSync(process.execPath, ["--import", "tsx", "--test", "--test-concurrency=1",
"--test-name-pattern=^audit sample:", "tests/browser/render-cost.test.ts"], {
cwd: ui, env, encoding: "utf8", windowsHide: true, timeout: 120_000,
})
writeFileSync(join(directory, `${repetition + 1}-${variant}.log`), `${result.stdout ?? ""}\n${result.stderr ?? ""}`)
if (result.status !== 0) throw new Error(`Fixture failed (${variant}): ${result.error ?? result.stderr}; logs: ${directory}`)
const match = result.stdout.match(/render-cost-native-deltas (\{[^\r\n]*\})/)
if (!match) throw new Error(`No metrics in ${variant}; logs: ${directory}`)
const metrics = JSON.parse(match[1])
if (metrics.variant !== variant || metrics.instrumented || metrics.samples.length !== 10) throw new Error("Invalid A/B fixture metadata")
runs.push({ repetition: repetition + 1, ...metrics })
console.log(`${repetition + 1}/${repetitions} ${variant}: 10 samples recorded`)
}
}
function stats(values) {
const sorted = [...values].sort((a, b) => a - b)
const mean = values.reduce((sum, value) => sum + value, 0) / values.length
return { n: values.length, median: (sorted[(sorted.length - 1) >> 1] + sorted[sorted.length >> 1]) / 2,
min: sorted[0], max: sorted.at(-1), mean,
sampleStandardDeviation: Math.sqrt(values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / (values.length - 1)) }
}
const summary = Object.fromEntries(["baseline", "fixed"].map(variant => {
const samples = runs.filter(run => run.variant === variant).flatMap(run => run.samples)
return [variant, Object.fromEntries(["syncMs", "scriptMs", "taskMs", "layoutMs", "styleMs"].map(key => [key, stats(samples.map(sample => sample[key]))]))]
}))
const artifact = { reference, fixedSourceSha256: createHash("sha256").update(readFileSync(join(root, source))).digest("hex"),
browserPath: process.env.CODENOMAD_BROWSER_PATH ?? "Playwright default", node: process.version,
repetitions, scenario: "80 completed read steps × 8,000 characters; 3 warmup deltas, 10 single native text deltas per run",
notes: "CDP cumulative CPU deltas through two animation frames; not frame latency. Instrumentation off. Other host activity is not controlled.",
summary, runs }
writeFileSync(join(directory, "metrics.json"), JSON.stringify(artifact, null, 2))
console.log(JSON.stringify(summary, null, 2))
console.log(`Raw metrics and logs: ${join(directory, "metrics.json")}`)

View file

@ -165,7 +165,7 @@ try {
assert.equal((await client.server.info()).version, runtimeVersion)
console.log(`Testing official runtime ${runtimeVersion}`)
const { testNativeLocationIdentity } = await import("./test-opencode-location-native.mjs")
await testNativeLocationIdentity({ client, connection, root })
await testNativeLocationIdentity({ client, connection, root, service: sharedService })
const { testNativeProxy } = await import("./test-opencode-proxy-native.mjs")
await testNativeProxy({ client, baseUrl, root, runtimeFetch, connection, authorization: `Basic ${Buffer.from("opencode:isolated-pruning-fixture").toString("base64")}`,
exercise: async (proxy, sessionID) => {