Merge remote-tracking branch 'origin/main' into brennanb2025/floating-agent-launch

This commit is contained in:
Brennan Benson
2026-09-18 02:04:47 -04:00
537 changed files with 58342 additions and 626 deletions
+7
View File
@@ -41,3 +41,10 @@
# Generated method->params catalog: compared byte-for-byte by
# verify:rpc-params-catalog, so a CRLF checkout would fail the gate.
/src/shared/rpc-contract/rpc-params-catalog.generated.ts linguist-generated=true text eol=lf
# Mobile web bundle source. Every text byte here is hashed into an asset digest and
# from there into buildId, so a CRLF checkout produces a different bundle id for the
# same commit (91af2897 vs 9d78435e). The PNG is -text because it must not be touched.
/src/mobile-web/index.html text eol=lf
/src/mobile-web/src/*.ts text eol=lf
/src/mobile-web/src/*.css text eol=lf
/src/mobile-web/src/*.png -text
+6
View File
@@ -780,6 +780,12 @@ jobs:
- name: Project web client from renderer build
run: pnpm run build:web-from-renderer
# Why here and not inside "Build package inputs": this job assembles packaging inputs step by
# step instead of calling build:release, and electron-builder's beforePack guard hard-fails
# without out/mobile-web.
- name: Build mobile web bundle
run: pnpm run build:mobile-web
- name: Build native components
run: pnpm run build:native
+11 -1
View File
@@ -14,6 +14,10 @@ const {
} = require('./packaged-runtime-node-modules.cjs')
const { verifyLinuxGlibcFloor } = require('./scripts/verify-linux-glibc-floor.cjs')
const { writeMacBuildCompatibility } = require('./scripts/mac-build-compatibility.cjs')
const {
MOBILE_WEB_BUNDLE_DIR,
assertMobileWebBundleBuilt
} = require('./scripts/verify-packaged-mobile-web-bundle.cjs')
const { verifyPackagedPluginResources } = require('./scripts/verify-packaged-plugin-resources.cjs')
const {
verifyPackagedWindowsNodePty
@@ -177,6 +181,9 @@ module.exports = {
// Why: these repo-only inputs are either bundled into out/ or copied via
// extraResources. Shipping them in app.asar bloats the desktop bundle.
'!src{,/**/*}',
// Redundant under !src above, kept explicit: the built bundle ships from out/mobile-web via the
// out rules exactly as out/web does, and the source tree must never be mistaken for it.
'!src/mobile-web{,/**/*}',
'!config{,/**/*}',
'!docs{,/**/*}',
'!mobile{,/**/*}',
@@ -289,8 +296,11 @@ module.exports = {
verifyStaticAppImagePackage(file, arch)
}
},
beforePack: (context) => {
// electron-builder calls this with the context alone. The second parameter is the bundle root,
// so a test can point the guard at a scratch bundle instead of needing the repo's out/ built.
beforePack: (context, mobileWebBundleDir = MOBILE_WEB_BUNDLE_DIR) => {
assertPackagedNativeVariantsInstalled(context.electronPlatformName, context.arch)
assertMobileWebBundleBuilt(mobileWebBundleDir)
},
afterPack: async (context) => {
const resourcesDir =
+1
View File
@@ -16,6 +16,7 @@
"src/main/hang-watchdog/main-thread-hang-watchdog-entry.ts",
"src/main/agent-hooks/managed-agent-hook-controls.ts",
"src/main/claude-accounts/keychain.ts",
"src/mobile-web/src/bootstrap.ts",
"src/renderer/src/main.tsx",
"src/renderer/src/popout.tsx",
"src/renderer/src/web/main.tsx",
+9
View File
@@ -56,6 +56,15 @@
"anti-slop/no-module-mocking": "off"
}
},
// mock-descendant-sweep.ts (daemon and relay) is a test-only side-effect shim: its whole body
// is one vi.mock that keeps mock PTY PIDs away from the host process table, and it exists so
// 60 suites do not each inline the same hoisted factory. It is never imported by product code.
{
"files": ["**/mock-descendant-sweep.ts"],
"rules": {
"anti-slop/no-module-mocking": "off"
}
},
// The exemptions below are file-scoped rather than inline `oxlint-disable` comments
// because the root lint scan does not load this plugin, so an inline directive naming
// an anti-slop rule always reads back as an unused directive there.
+230
View File
@@ -0,0 +1,230 @@
import { createHash } from 'node:crypto'
import { realpathSync } from 'node:fs'
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import * as esbuild from 'esbuild'
const projectDir = fileURLToPath(new URL('../..', import.meta.url))
const sourceDir = join(projectDir, 'src', 'mobile-web')
const defaultOutDir = join(projectDir, 'out', 'mobile-web')
export const MOBILE_WEB_BUNDLE_SCHEMA_VERSION = 1
export const MOBILE_WEB_BUNDLE_ENTRYPOINT = 'index.html'
const CONTENT_TYPE_BY_EXTENSION = {
css: 'text/css; charset=utf-8',
html: 'text/html; charset=utf-8',
js: 'text/javascript; charset=utf-8',
png: 'image/png'
}
/**
* Canonical serialization the buildId hashes. Key order is fixed and the list is sorted by path,
* so the id is a pure function of content. Must stay byte-identical to the contract module's
* serializer in src/shared/mobile-web-bundle/.
*/
export function serializeMobileWebBundleAssets(assets) {
return JSON.stringify(
[...assets]
.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0))
.map(({ path, sha256, byteLength, contentType }) => ({
path,
sha256,
byteLength,
contentType
}))
)
}
export function computeMobileWebBundleBuildId(assets) {
return createHash('sha256').update(serializeMobileWebBundleAssets(assets), 'utf8').digest('hex')
}
function sha256Hex(bytes) {
return createHash('sha256').update(bytes).digest('hex')
}
function contentTypeForExtension(extension) {
const contentType = CONTENT_TYPE_BY_EXTENSION[extension]
if (!contentType) {
throw new Error(`[build-mobile-web-bundle] no content type registered for .${extension}`)
}
return contentType
}
function readIntegerConstant(source, name) {
const match = new RegExp(`export const ${name} = (\\d+)`).exec(source)
if (!match) {
throw new Error(`[build-mobile-web-bundle] ${name} not found in src/shared/protocol-version.ts`)
}
return Number.parseInt(match[1], 10)
}
/**
* Parsed rather than imported because protocol-version.ts is TypeScript and this script runs on
* bare node during packaging, before any build output exists.
*/
async function readProtocolWindow() {
const source = await readFile(join(projectDir, 'src', 'shared', 'protocol-version.ts'), 'utf8')
return {
runtimeProtocolVersion: readIntegerConstant(source, 'RUNTIME_PROTOCOL_VERSION'),
// The bundle is a client: the floor it cares about is the oldest host protocol it can talk to.
minCompatibleRuntimeProtocolVersion: readIntegerConstant(
source,
'MIN_COMPATIBLE_RUNTIME_SERVER_VERSION'
)
}
}
async function readDesktopVersion() {
const packageJson = JSON.parse(await readFile(join(projectDir, 'package.json'), 'utf8'))
if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
throw new Error('[build-mobile-web-bundle] root package.json has no version')
}
return packageJson.version
}
async function transformEntries(protocolWindow, desktopVersion) {
const result = await esbuild.build({
absWorkingDir: sourceDir,
entryPoints: [join(sourceDir, 'src', 'bootstrap.ts'), join(sourceDir, 'src', 'bootstrap.css')],
bundle: true,
minify: true,
// Virtual: write is false, so outdir only names the emitted files esbuild hands back.
outdir: 'dist',
write: false,
format: 'iife',
target: ['es2022'],
charset: 'utf8',
legalComments: 'none',
// Why no sourcemap and no metafile: both embed absolute paths, which would break reproducibility.
sourcemap: false,
logLevel: 'silent',
define: {
ORCA_MOBILE_WEB_DESKTOP_VERSION: JSON.stringify(desktopVersion),
ORCA_MOBILE_WEB_RUNTIME_PROTOCOL_VERSION: JSON.stringify(
protocolWindow.runtimeProtocolVersion
),
ORCA_MOBILE_WEB_MIN_COMPATIBLE_RUNTIME_PROTOCOL_VERSION: JSON.stringify(
protocolWindow.minCompatibleRuntimeProtocolVersion
)
}
})
const byExtension = new Map()
for (const file of result.outputFiles) {
const extension = file.path.endsWith('.css') ? 'css' : 'js'
byExtension.set(extension, Buffer.from(file.contents))
}
const script = byExtension.get('js')
const stylesheet = byExtension.get('css')
if (!script || !stylesheet) {
throw new Error('[build-mobile-web-bundle] esbuild did not emit both a script and a stylesheet')
}
return { script, stylesheet }
}
function hashedAsset(bytes, extension) {
const sha256 = sha256Hex(bytes)
return {
bytes,
path: `assets/${sha256}.${extension}`,
sha256,
byteLength: bytes.byteLength,
contentType: contentTypeForExtension(extension)
}
}
export async function buildMobileWebBundle({ outDir = defaultOutDir } = {}) {
const [desktopVersion, protocolWindow] = await Promise.all([
readDesktopVersion(),
readProtocolWindow()
])
const { script, stylesheet } = await transformEntries(protocolWindow, desktopVersion)
const mark = await readFile(join(sourceDir, 'src', 'orca-mark.png'))
const hashed = [
hashedAsset(script, 'js'),
hashedAsset(stylesheet, 'css'),
hashedAsset(mark, 'png')
]
const [scriptAsset, stylesheetAsset, markAsset] = hashed
const template = await readFile(join(sourceDir, MOBILE_WEB_BUNDLE_ENTRYPOINT), 'utf8')
const substitutions = {
__ORCA_BOOTSTRAP_JS__: scriptAsset.path,
__ORCA_BOOTSTRAP_CSS__: stylesheetAsset.path,
__ORCA_MARK_PNG__: markAsset.path
}
let html = template
for (const [token, value] of Object.entries(substitutions)) {
if (!html.includes(token)) {
throw new Error(`[build-mobile-web-bundle] ${MOBILE_WEB_BUNDLE_ENTRYPOINT} lacks ${token}`)
}
html = html.replaceAll(token, value)
}
const indexBytes = Buffer.from(html, 'utf8')
const indexAsset = {
bytes: indexBytes,
path: MOBILE_WEB_BUNDLE_ENTRYPOINT,
sha256: sha256Hex(indexBytes),
byteLength: indexBytes.byteLength,
contentType: contentTypeForExtension('html')
}
const written = [indexAsset, ...hashed]
const assets = written
.map(({ path, sha256, byteLength, contentType }) => ({ path, sha256, byteLength, contentType }))
.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0))
const manifest = {
schemaVersion: MOBILE_WEB_BUNDLE_SCHEMA_VERSION,
buildId: computeMobileWebBundleBuildId(assets),
desktopVersion,
minCompatibleRuntimeProtocolVersion: protocolWindow.minCompatibleRuntimeProtocolVersion,
runtimeProtocolVersion: protocolWindow.runtimeProtocolVersion,
entrypoint: MOBILE_WEB_BUNDLE_ENTRYPOINT,
totalBytes: assets.reduce((total, asset) => total + asset.byteLength, 0),
assets
}
// Why a full clear: a stale asset left from an earlier build would ship unreferenced inside asar.
await rm(outDir, { recursive: true, force: true })
await mkdir(join(outDir, 'assets'), { recursive: true })
for (const asset of written) {
await writeFile(join(outDir, asset.path), asset.bytes)
}
await writeFile(join(outDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
return { manifest, outDir }
}
/**
* Whether this module was run as the entry script. Two ways to get this wrong, both of which end
* with the builder exiting 0 having written nothing: `file://${path}` never matches on Windows,
* where import.meta.url is `file:///C:/...`; and Node resolves symlinks in import.meta.url but not
* in argv[1], so `node /tmp/...` against a /private/tmp realpath compares two different strings.
* Both seams are injectable so win32 and a missing path can be exercised from a posix runner.
*/
export function isDirectInvocation(
moduleUrl,
scriptPath,
{ toFileUrl = pathToFileURL, realpath = realpathSync } = {}
) {
if (!scriptPath) {
return false
}
let resolved = scriptPath
try {
resolved = realpath(scriptPath)
} catch {
// A path that cannot be resolved cannot be this module; fall through to the literal compare.
}
return moduleUrl === toFileUrl(resolved).href
}
if (isDirectInvocation(import.meta.url, process.argv[1])) {
const { manifest, outDir } = await buildMobileWebBundle()
console.log(
`[build-mobile-web-bundle] OK — ${String(manifest.assets.length)} asset(s), ` +
`${String(manifest.totalBytes)} bytes, buildId ${manifest.buildId} -> ${outDir}`
)
}
@@ -0,0 +1,261 @@
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
buildMobileWebBundle,
computeMobileWebBundleBuildId,
isDirectInvocation,
serializeMobileWebBundleAssets
} from './build-mobile-web-bundle.mjs'
import {
MOBILE_WEB_BUNDLE_PHASE_A_MAX_ASSETS,
MOBILE_WEB_BUNDLE_PHASE_A_MAX_TOTAL_BYTES,
assertNoCarriageReturnsInSource
} from './verify-mobile-web-bundle.mjs'
async function buildIntoScratch() {
const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-build-'))
const bundleDir = join(scratch, 'mobile-web')
const { manifest } = await buildMobileWebBundle({ outDir: bundleDir })
return { scratch, bundleDir, manifest }
}
describe('buildMobileWebBundle', () => {
it('emits a content-addressed bundle whose only stable name is the entrypoint', async () => {
const { scratch, bundleDir, manifest } = await buildIntoScratch()
try {
const root = await readdir(bundleDir)
expect(root.sort()).toEqual(['assets', 'index.html', 'manifest.json'])
for (const name of await readdir(join(bundleDir, 'assets'))) {
const [digest, extension] = name.split('.')
expect(digest).toMatch(/^[0-9a-f]{64}$/)
const bytes = await readFile(join(bundleDir, 'assets', name))
expect(createHash('sha256').update(bytes).digest('hex')).toBe(digest)
expect(extension).toMatch(/^(js|css|png)$/)
}
const html = await readFile(join(bundleDir, 'index.html'), 'utf8')
for (const asset of manifest.assets) {
if (asset.path !== 'index.html') {
expect(html).toContain(asset.path)
}
}
expect(html).not.toContain('__ORCA_')
} finally {
await rm(scratch, { recursive: true, force: true })
}
})
it('carries every manifest field the Phase A contract names', async () => {
const { scratch, manifest } = await buildIntoScratch()
try {
expect(Object.keys(manifest)).toEqual([
'schemaVersion',
'buildId',
'desktopVersion',
'minCompatibleRuntimeProtocolVersion',
'runtimeProtocolVersion',
'entrypoint',
'totalBytes',
'assets'
])
expect(manifest.schemaVersion).toBe(1)
expect(manifest.entrypoint).toBe('index.html')
const packageJson = JSON.parse(
await readFile(new URL('../../package.json', import.meta.url), 'utf8')
)
expect(manifest.desktopVersion).toBe(packageJson.version)
const protocolSource = await readFile(
new URL('../../src/shared/protocol-version.ts', import.meta.url),
'utf8'
)
expect(protocolSource).toContain(
`export const RUNTIME_PROTOCOL_VERSION = ${String(manifest.runtimeProtocolVersion)}`
)
expect(protocolSource).toContain(
`export const MIN_COMPATIBLE_RUNTIME_SERVER_VERSION = ${String(manifest.minCompatibleRuntimeProtocolVersion)}`
)
expect(manifest.totalBytes).toBe(
manifest.assets.reduce((total, asset) => total + asset.byteLength, 0)
)
} finally {
await rm(scratch, { recursive: true, force: true })
}
})
it('produces the same buildId from two independent builds', async () => {
const first = await buildIntoScratch()
const second = await buildIntoScratch()
try {
expect(second.manifest.buildId).toBe(first.manifest.buildId)
expect(second.manifest).toEqual(first.manifest)
} finally {
await rm(first.scratch, { recursive: true, force: true })
await rm(second.scratch, { recursive: true, force: true })
}
})
it('embeds no absolute path from the machine that built it', async () => {
const { scratch, bundleDir } = await buildIntoScratch()
try {
const names = [
'index.html',
'manifest.json',
...(await readdir(join(bundleDir, 'assets'))).map((name) => join('assets', name))
]
for (const name of names) {
const text = (await readFile(join(bundleDir, name))).toString('latin1')
expect(text).not.toContain(scratch)
expect(text).not.toContain(process.cwd())
}
} finally {
await rm(scratch, { recursive: true, force: true })
}
})
it('stays inside the Phase A budget', async () => {
const { scratch, manifest } = await buildIntoScratch()
try {
expect(manifest.assets.length).toBeLessThanOrEqual(MOBILE_WEB_BUNDLE_PHASE_A_MAX_ASSETS)
expect(manifest.totalBytes).toBeLessThanOrEqual(MOBILE_WEB_BUNDLE_PHASE_A_MAX_TOTAL_BYTES)
} finally {
await rm(scratch, { recursive: true, force: true })
}
})
})
describe('computeMobileWebBundleBuildId', () => {
const assets = [
{ path: 'index.html', sha256: 'a'.repeat(64), byteLength: 3, contentType: 'text/html' },
{ path: 'assets/b.js', sha256: 'b'.repeat(64), byteLength: 5, contentType: 'text/javascript' }
]
it('sorts by path, so input order cannot change the id', () => {
expect(computeMobileWebBundleBuildId(assets.toReversed())).toBe(
computeMobileWebBundleBuildId(assets)
)
})
it('serializes a fixed key order regardless of the input object key order', () => {
const reordered = assets.map(({ contentType, byteLength, sha256, path }) => ({
contentType,
byteLength,
sha256,
path
}))
expect(serializeMobileWebBundleAssets(reordered)).toBe(serializeMobileWebBundleAssets(assets))
})
it('changes when any hashed field changes', () => {
const baseline = computeMobileWebBundleBuildId(assets)
for (const field of ['sha256', 'byteLength', 'contentType', 'path']) {
const mutated = assets.map((asset, index) =>
index === 0 ? { ...asset, [field]: field === 'byteLength' ? 4 : `${asset[field]}x` } : asset
)
expect(computeMobileWebBundleBuildId(mutated)).not.toBe(baseline)
}
})
})
describe('isDirectInvocation', () => {
const thisFile = import.meta.filename
it('matches the path this module was loaded from', () => {
expect(isDirectInvocation(import.meta.url, thisFile)).toBe(true)
})
it('does not match a different script', () => {
expect(isDirectInvocation(import.meta.url, join(thisFile, '..', 'other.mjs'))).toBe(false)
})
it('tolerates an absent argv[1]', () => {
expect(isDirectInvocation(import.meta.url, undefined)).toBe(false)
expect(isDirectInvocation(import.meta.url, '')).toBe(false)
})
// Why an injected converter: a win32 path cannot be exercised through node:url's pathToFileURL
// on a posix runner, and CI is ubuntu.
const toWin32FileUrl = (windowsPath) => new URL(`file:///${windowsPath.replaceAll('\\', '/')}`)
it('matches a Windows entry path, which the file:// template form never does', () => {
const scriptPath = 'C:\\orca\\config\\scripts\\build-mobile-web-bundle.mjs'
const moduleUrl = 'file:///C:/orca/config/scripts/build-mobile-web-bundle.mjs'
const keepAsIs = (path) => path
expect(
isDirectInvocation(moduleUrl, scriptPath, {
toFileUrl: toWin32FileUrl,
realpath: keepAsIs
})
).toBe(true)
// The regression this guards: `file://${argv[1]}` yields file://C:\orca\... on Windows,
// so the builder exited 0 having written nothing and packaging failed downstream.
expect(`file://${scriptPath}`).not.toBe(moduleUrl)
})
it('is not written with the file:// template form', async () => {
const source = await readFile(new URL('./build-mobile-web-bundle.mjs', import.meta.url), 'utf8')
expect(source).not.toMatch(/file:\/\/\$\{process\.argv\[1\]\}/)
expect(source).toContain('pathToFileURL')
})
})
describe('mobile web source line endings', () => {
it('accepts the committed source tree', async () => {
await expect(assertNoCarriageReturnsInSource()).resolves.toBeUndefined()
})
it('rejects a CRLF source file, because CRLF changes every asset hash and the buildId', async () => {
const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-eol-'))
try {
await writeFile(join(scratch, 'bootstrap.ts'), 'const a = 1\r\nconst b = 2\r\n', 'utf8')
await expect(assertNoCarriageReturnsInSource(scratch)).rejects.toThrow(
/CRLF in mobile web source/
)
} finally {
await rm(scratch, { recursive: true, force: true })
}
})
it('pins eol=lf for every committed text source and -text for the binary', () => {
const files = execFileSync('git', ['ls-files', 'src/mobile-web'], { encoding: 'utf8' })
.split('\n')
.filter(Boolean)
expect(files.length).toBeGreaterThanOrEqual(4)
for (const file of files) {
const attributes = execFileSync('git', ['check-attr', 'text', 'eol', '--', file], {
encoding: 'utf8'
})
if (file.endsWith('.png')) {
expect(attributes).toContain('text: unset')
} else {
expect(attributes).toContain('eol: lf')
}
}
})
})
describe('running the builder through a symlink', () => {
// Node resolves symlinks in import.meta.url but not in argv[1]. Before the guard realpath'd the
// entry path, `node /tmp/<link>` compared /tmp against /private/tmp and the builder exited 0
// having written nothing — a green packaging job with no bundle in it.
it('still recognises the entry module', async () => {
const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-link-'))
try {
const builderUrl = new URL('./build-mobile-web-bundle.mjs', import.meta.url).href
const real = join(scratch, 'entry.mjs')
await writeFile(
real,
`import { isDirectInvocation } from ${JSON.stringify(builderUrl)}\n` +
'process.stdout.write(String(isDirectInvocation(import.meta.url, process.argv[1])))\n',
'utf8'
)
const link = join(scratch, 'entry-link.mjs')
await symlink(real, link)
expect(execFileSync(process.execPath, [link], { encoding: 'utf8' })).toBe('true')
} finally {
await rm(scratch, { recursive: true, force: true })
}
})
})
@@ -3,7 +3,8 @@ import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { buildMobileWebBundle } from './build-mobile-web-bundle.mjs'
const REPO_ROOT = join(import.meta.dirname, '..', '..')
const SRC_MAIN_DIR = join(REPO_ROOT, 'src', 'main')
@@ -442,8 +443,23 @@ describe('arch-aware packaging guard', () => {
const OTHER_ARCH_NAME = process.arch === 'arm64' ? 'x64' : 'arm64'
const SHERPA_PLATFORM = process.platform === 'win32' ? 'win' : process.platform
const otherSherpa = `sherpa-onnx-${SHERPA_PLATFORM}-${OTHER_ARCH_NAME}`
// beforePack also hash-verifies the mobile web bundle, which the unit-test job never builds.
// Point it at a real bundle built into a temp dir: these tests are about the native-variant
// guard, and the bundle guard has its own suite.
let scratch
let bundleDir
beforeAll(async () => {
scratch = await mkdtemp(join(tmpdir(), 'orca-electron-builder-guard-'))
bundleDir = join(scratch, 'mobile-web')
await buildMobileWebBundle({ outDir: bundleDir })
})
afterAll(async () => {
await rm(scratch, { recursive: true, force: true })
})
const packHost = (arch) =>
electronBuilderConfig.beforePack({ electronPlatformName: process.platform, arch })
electronBuilderConfig.beforePack({ electronPlatformName: process.platform, arch }, bundleDir)
it('allows packaging the host platform and architecture', () => {
expect(() => packHost(HOST_ARCH)).not.toThrow()
@@ -471,7 +487,7 @@ describe('arch-aware packaging guard', () => {
(resource) => resource.to === join('node_modules', '@vscode', 'windows-process-tree')
)
const packWindows = () =>
electronBuilderConfig.beforePack({ electronPlatformName: 'win32', arch: 1 })
electronBuilderConfig.beforePack({ electronPlatformName: 'win32', arch: 1 }, bundleDir)
if (process.platform === 'win32' || windowsAddon) {
expect(packWindows).not.toThrow()
} else {
@@ -0,0 +1,159 @@
import { readFileSync, readdirSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { parseDocument } from 'yaml'
const workflowsDir = fileURLToPath(new URL('../../.github/workflows', import.meta.url))
// Every script whose chain reaches build:mobile-web. build:unpack -> build -> build:desktop, and
// build:mac/linux/win each call build:desktop, so all of them produce out/mobile-web. The chain
// itself is not an assumption here: 'the build scripts' below resolves each one for real.
const BUNDLE_PRODUCING_SCRIPTS = [
'build',
'build:desktop',
'build:release',
'build:release:parallel',
'build:unpack',
'build:mobile-web',
'build:mac',
'build:mac:release',
'build:linux',
'build:win'
]
const BUNDLE_PRODUCER = new RegExp(
`pnpm (?:run )?(?:${BUNDLE_PRODUCING_SCRIPTS.join('|')})(?=$|[\\s'"&|;])`,
'm'
)
const packageScripts = JSON.parse(
readFileSync(fileURLToPath(new URL('../../package.json', import.meta.url)), 'utf8')
).scripts
const SCRIPT_INVOCATION = /pnpm (?:run )?([\w:-]+)(?=$|[\s'"&|;])/g
/** Whether `pnpm run <name>` eventually runs build:mobile-web. */
function reachesBundleBuild(name, seen = new Set()) {
if (name === 'build:mobile-web') {
return true
}
if (seen.has(name)) {
return false
}
seen.add(name)
const body = packageScripts[name]
if (typeof body !== 'string') {
return false
}
return [...body.matchAll(SCRIPT_INVOCATION)].some((match) => reachesBundleBuild(match[1], seen))
}
/**
* Whether `pnpm run <name>` eventually runs electron-builder without --prepackaged, i.e. runs
* beforePack. A workflow job that packs through such a script is a packaging job even though the
* literal electron-builder line lives in package.json (daemon-relocation-spike's build:unpack).
*/
function reachesElectronBuilder(name, seen = new Set()) {
if (seen.has(name)) {
return false
}
seen.add(name)
const body = packageScripts[name]
if (typeof body !== 'string') {
return false
}
if (packsWithBeforePack(body)) {
return true
}
return [...body.matchAll(SCRIPT_INVOCATION)].some((match) =>
reachesElectronBuilder(match[1], seen)
)
}
/** Whether text invokes electron-builder in a way that reaches beforePack. */
function packsWithBeforePack(text) {
const invocations = [...text.matchAll(/[^\n]*electron-builder --config[^\n]*/g)].map(
(match) => match[0]
)
// --prepackaged short-circuits doPack before emitBeforePack, so those jobs never run the guard.
return (
invocations.length > 0 &&
!invocations.every((invocation) => invocation.includes('--prepackaged'))
)
}
// Every job that packs an app and therefore runs beforePack. Listed so that a new packaging
// workflow has to be added here deliberately, with its bundle step, rather than slipping in.
const EXPECTED_PACKAGING_JOBS = [
'adhoc-mac-build.yml build-adhoc-mac',
'daemon-relocation-spike.yml spike',
'daily-mac-build.yml build-daily-mac',
'dev-channel-win-build.yml build-win',
'hourly-mac-build.yml build-hourly-mac',
'pr.yml package',
'pr.yml package_windows',
'release-cut.yml build',
'release-mac-build.yml build-mac',
'win-crash-survival-e2e.yml crash-survival',
'win-update-survival-e2e.yml survival',
'windows-signing-rehearsal.yml rehearse'
]
/**
* Raw source text per job, sliced by the parsed job boundaries. Why not yaml.stringify(job):
* re-serializing folds long lines, and the fold in dev-channel-win-build's build-win landed
* between `electron-builder` and `--config`, hiding a whole packaging job from this census.
*/
function packagingJobs() {
const jobs = []
for (const file of readdirSync(workflowsDir).filter((name) => name.endsWith('.yml'))) {
const source = readFileSync(join(workflowsDir, file), 'utf8')
const jobsNode = parseDocument(source).get('jobs', true)
const items = jobsNode?.items ?? []
for (const [index, pair] of items.entries()) {
const end = index + 1 < items.length ? items[index + 1].key.range[0] : jobsNode.range[2]
const text = source.slice(pair.key.range[0], end)
const packsViaScript = [...text.matchAll(SCRIPT_INVOCATION)].some((match) =>
reachesElectronBuilder(match[1])
)
if (!packsWithBeforePack(text) && !packsViaScript) {
continue
}
jobs.push({ label: `${file} ${String(pair.key.value)}`, text })
}
}
return jobs
}
describe('mobile web bundle packaging coverage', () => {
it('finds every packaging job', () => {
// A rename or a restructure that shrank this list would make every assertion below vacuous.
const labels = packagingJobs().map((job) => job.label)
expect(labels.length).toBeGreaterThanOrEqual(EXPECTED_PACKAGING_JOBS.length)
expect(labels.toSorted()).toEqual(EXPECTED_PACKAGING_JOBS.toSorted())
})
it.each(packagingJobs().map((job) => [job.label, job]))(
'produces out/mobile-web before electron-builder packs: %s',
(_label, job) => {
// Job granularity, not step ordering: the failure this exists for is a job that never builds
// the bundle at all, which is what beforePack turns into a hard packaging failure.
expect(job.text).toMatch(BUNDLE_PRODUCER)
}
)
})
describe('the build scripts the census trusts', () => {
// The census only checks that a packaging job invokes one of these. If a chain stopped calling
// build:mobile-web, every job would still look covered while packaging failed at beforePack.
it.each(BUNDLE_PRODUCING_SCRIPTS)('%s runs build:mobile-web', (name) => {
expect(packageScripts[name]).toBeTypeOf('string')
expect(reachesBundleBuild(name)).toBe(true)
})
it('pr.yml package builds the bundle by hand, because it never calls build:release', () => {
const source = readFileSync(join(workflowsDir, 'pr.yml'), 'utf8')
expect(source).toMatch(/- name: Build mobile web bundle\n\s+run: pnpm run build:mobile-web\n/)
})
})
@@ -0,0 +1,119 @@
/**
* The canonical serialization that buildId hashes exists three times, because the two packaging
* scripts run on bare node before any build output exists and so cannot import the TypeScript
* contract. Three copies drift; this is what stops them. A divergence in any one of them would
* reject every honest bundle at packaging, or ship a bundle whose id the phone recomputes
* differently and re-downloads forever.
*/
import { createHash } from 'node:crypto'
import { createRequire } from 'node:module'
import { describe, expect, it } from 'vitest'
import {
computeMobileWebBundleBuildId,
serializeMobileWebBundleAssets as serializeInBuilder
} from './build-mobile-web-bundle.mjs'
import {
computeMobileWebBundleId,
MobileWebBundleManifestSchema,
serializeMobileWebBundleAssets as serializeInContract
} from '../../src/shared/mobile-web-bundle/manifest-contract'
const require = createRequire(import.meta.url)
const { serializeAssets: serializeInGuard } = require('./verify-packaged-mobile-web-bundle.cjs')
const digest = (hex) => `${hex}`.padStart(64, '0')
/**
* Mixed content types, a nested path, and an uppercase segment that sorts before a lowercase one
* only under code-unit order: `localeCompare` would put `assets/aQ.js` first, so any serializer
* that reached for it produces a different string here.
*/
const ASSETS = [
{
path: 'assets/Za.js',
sha256: digest('a1'),
byteLength: 2048,
contentType: 'text/javascript; charset=utf-8'
},
{ path: 'assets/aQ.css', sha256: digest('b2'), byteLength: 512, contentType: 'text/css' },
{
path: 'assets/nested/mark.png',
sha256: digest('c3'),
byteLength: 40_960,
contentType: 'image/png'
},
{
path: 'index.html',
sha256: digest('d4'),
byteLength: 640,
contentType: 'text/html; charset=utf-8'
}
]
const REORDERED = [ASSETS[3], ASSETS[1], ASSETS[0], ASSETS[2]]
const REVERSED = ASSETS.toReversed()
const sha256Hex = (value) => createHash('sha256').update(value, 'utf8').digest('hex')
describe('the three mobile web bundle serializers', () => {
it('produce one string for the builder, the packaging guard, and the shared contract', () => {
const fromContract = serializeInContract(ASSETS)
expect(serializeInBuilder(ASSETS)).toBe(fromContract)
expect(serializeInGuard(ASSETS)).toBe(fromContract)
})
it.each([
['reordered', REORDERED],
['reversed', REVERSED]
])('are order-independent, so %s input serializes identically', (_label, input) => {
const expected = serializeInContract(ASSETS)
expect(serializeInContract(input)).toBe(expected)
expect(serializeInBuilder(input)).toBe(expected)
expect(serializeInGuard(input)).toBe(expected)
})
it('leaves the caller-supplied array untouched, so a build cannot depend on the sort', () => {
const input = [...REORDERED]
serializeInContract(input)
serializeInBuilder(input)
serializeInGuard(input)
expect(input).toEqual(REORDERED)
})
it('emit exactly path, sha256, byteLength, contentType, in that order, and nothing else', () => {
const decorated = ASSETS.map((asset) => ({ ...asset, sourcePath: '/tmp/ignored', extra: 1 }))
expect(serializeInContract(decorated)).toBe(serializeInContract(ASSETS))
expect(serializeInBuilder(decorated)).toBe(serializeInContract(ASSETS))
expect(serializeInGuard(decorated)).toBe(serializeInContract(ASSETS))
expect(JSON.parse(serializeInContract(ASSETS))[0]).toEqual({
path: 'assets/Za.js',
sha256: digest('a1'),
byteLength: 2048,
contentType: 'text/javascript; charset=utf-8'
})
})
it('hash to one buildId, which the manifest schema then accepts', () => {
const buildId = computeMobileWebBundleId(REORDERED)
expect(computeMobileWebBundleBuildId(REORDERED)).toBe(buildId)
expect(sha256Hex(serializeInGuard(REORDERED))).toBe(buildId)
const manifest = {
schemaVersion: 1,
buildId,
desktopVersion: '1.4.200',
minCompatibleRuntimeProtocolVersion: 2,
runtimeProtocolVersion: 2,
entrypoint: 'index.html',
totalBytes: ASSETS.reduce((total, asset) => total + asset.byteLength, 0),
assets: [...ASSETS]
}
expect(MobileWebBundleManifestSchema.parse(manifest).buildId).toBe(buildId)
})
})
+11
View File
@@ -178,6 +178,17 @@ export const PR_E2E_SOURCE_ROUTES = [
file
)
},
{
// Why: layout resolution is the only place a split direction can be invented, and the
// loss is one-way — the guess is published and written back over the real tree.
id: 'terminal-session.split-orientation-resolution',
specs: ['tests/e2e/desktop-published-split-orientation-legacy-leaf.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/renderer\/src\/runtime\/(?:remote-terminal-layout-resolution\.ts|sync-runtime-graph\/(?:graph-publication|mobile-session-terminal-tabs|mobile-session-surfaces)\.ts|web-session-tabs-sync\/terminal-surfaces\.ts)$/.test(
file
)
},
{
id: 'terminal-session.remote-pane-layout-retry',
specs: ['tests/e2e/paired-remote-pane-layout-retry.spec.ts'],
+1
View File
@@ -78,6 +78,7 @@ const result = spawnSync(
'tests/e2e/ssh-reconnect-tab-destruction.spec.ts',
'tests/e2e/ssh-restart-tab-accumulation.spec.ts',
'tests/e2e/ssh-skill-installation.spec.ts',
'tests/e2e/ssh-stale-resume-execution-host-scope.spec.ts',
'tests/e2e/ssh-terminal-window-wake-stale-grid-repro.spec.ts',
'--config',
'tests/playwright.config.ts',
@@ -2,9 +2,14 @@ import { spawn } from 'node:child_process'
import { availableParallelism } from 'node:os'
import { fileURLToPath } from 'node:url'
// The three projects overlap heavily in src/shared but have no build dependency on
// These projects overlap heavily in src/shared but have no build dependency on
// each other, so tsc can check them concurrently instead of in a `&&` chain.
const projects = ['tsconfig.node.json', 'tsconfig.tc.cli.json', 'tsconfig.tc.web.json']
const projects = [
'tsconfig.node.json',
'tsconfig.tc.cli.json',
'tsconfig.tc.web.json',
'tsconfig.mobile-web.json'
]
const repoRoot = fileURLToPath(new URL('../..', import.meta.url))
const tsc = fileURLToPath(new URL('../../node_modules/typescript/bin/tsc', import.meta.url))
+117
View File
@@ -0,0 +1,117 @@
import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { buildMobileWebBundle, isDirectInvocation } from './build-mobile-web-bundle.mjs'
import { assertMobileWebBundleBuilt } from './verify-packaged-mobile-web-bundle.cjs'
const projectDir = fileURLToPath(new URL('../..', import.meta.url))
const bundleDir = join(projectDir, 'out', 'mobile-web')
const sourceDir = join(projectDir, 'src', 'mobile-web')
// Phase A budget, not the contract ceiling: a bootstrap page past a quarter-megabyte has stopped
// being a bootstrap. Phase C raises these deliberately.
export const MOBILE_WEB_BUNDLE_PHASE_A_MAX_ASSETS = 16
export const MOBILE_WEB_BUNDLE_PHASE_A_MAX_TOTAL_BYTES = 256 * 1024
class VerificationError extends Error {}
function fail(message) {
throw new VerificationError(message)
}
async function buildIntoScratch() {
const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-verify-'))
try {
const { manifest } = await buildMobileWebBundle({ outDir: join(scratch, 'mobile-web') })
return manifest
} finally {
await rm(scratch, { recursive: true, force: true })
}
}
async function listSourceFiles(directory) {
const entries = await readdir(directory, { withFileTypes: true })
const files = []
for (const entry of entries) {
const entryPath = join(directory, entry.name)
if (entry.isDirectory()) {
files.push(...(await listSourceFiles(entryPath)))
} else if (entry.isFile()) {
files.push(entryPath)
}
}
return files.sort()
}
/**
* A CRLF checkout changes the bytes of every text source, which changes every asset hash and so
* the buildId. .gitattributes pins eol=lf; this is what notices when that pin stops working.
*/
export async function assertNoCarriageReturnsInSource(directory = sourceDir) {
const offenders = []
for (const file of await listSourceFiles(directory)) {
// Binary assets are pinned -text and may legitimately contain 0x0d.
if (file.endsWith('.png')) {
continue
}
if ((await readFile(file)).includes(0x0d)) {
offenders.push(file.slice(directory.length + 1))
}
}
if (offenders.length > 0) {
fail(
`CRLF in mobile web source, which would change every asset hash and the buildId: ` +
`${offenders.join(', ')}. Check the .gitattributes eol=lf pin for src/mobile-web.`
)
}
}
export async function verifyMobileWebBundle() {
await assertNoCarriageReturnsInSource()
// The packaging guard owns manifest integrity (safe paths, recomputed buildId, totalBytes, hashes,
// no stray files); a manifest edited after the build fails here exactly as it would at beforePack.
const manifest = assertMobileWebBundleBuilt(bundleDir)
if (manifest.assets.length > MOBILE_WEB_BUNDLE_PHASE_A_MAX_ASSETS) {
fail(
`bundle has ${String(manifest.assets.length)} assets, over the Phase A budget of ` +
`${String(MOBILE_WEB_BUNDLE_PHASE_A_MAX_ASSETS)}`
)
}
if (manifest.totalBytes > MOBILE_WEB_BUNDLE_PHASE_A_MAX_TOTAL_BYTES) {
fail(
`bundle is ${String(manifest.totalBytes)} bytes, over the Phase A budget of ` +
`${String(MOBILE_WEB_BUNDLE_PHASE_A_MAX_TOTAL_BYTES)}`
)
}
// Two fresh builds into scratch dirs: a timestamp, an absolute path, or an unstable ordering
// anywhere in the pipeline shows up here as a buildId mismatch rather than as a phone cache miss.
const first = await buildIntoScratch()
const second = await buildIntoScratch()
if (first.buildId !== second.buildId) {
fail(`buildId is not reproducible: ${first.buildId} then ${second.buildId}`)
}
if (first.buildId !== manifest.buildId) {
fail(
`${bundleDir} is stale: it carries buildId ${manifest.buildId}, a fresh build produces ${first.buildId}`
)
}
return manifest
}
if (isDirectInvocation(import.meta.url, process.argv[1])) {
try {
const manifest = await verifyMobileWebBundle()
console.log(
`[verify-mobile-web-bundle] OK — ${String(manifest.assets.length)} asset(s), ` +
`${String(manifest.totalBytes)}/${String(MOBILE_WEB_BUNDLE_PHASE_A_MAX_TOTAL_BYTES)} bytes, ` +
`reproducible buildId ${manifest.buildId}`
)
} catch (error) {
console.error(`[verify-mobile-web-bundle] ${error.message}`)
process.exit(1)
}
}
@@ -0,0 +1,193 @@
const { createHash } = require('node:crypto')
const { readFileSync, readdirSync, statSync } = require('node:fs')
const { join, resolve } = require('node:path')
const projectDir = resolve(__dirname, '..', '..')
const MOBILE_WEB_BUNDLE_DIR = join(projectDir, 'out', 'mobile-web')
const REMEDY = 'Run pnpm build:mobile-web (build:desktop and build:release already do).'
const ENTRYPOINT = 'index.html'
const SHA256_PATTERN = /^[0-9a-f]{64}$/
function failure(message) {
return new Error(`[verify-packaged-mobile-web-bundle] ${message}`)
}
function assertSafeRelativePath(path) {
if (typeof path !== 'string' || path.length === 0) {
throw failure('manifest asset has a missing or empty path')
}
const segments = path.split('/')
if (
path.includes('\\') ||
path.startsWith('/') ||
/^[a-zA-Z]:/.test(path) ||
segments.some((segment) => segment === '' || segment === '.' || segment === '..')
) {
throw failure(`manifest asset path is not a safe relative path: ${path}`)
}
}
function assertInteger(value, field) {
if (!Number.isSafeInteger(value) || value < 0) {
throw failure(`manifest field ${field} is not a non-negative integer: ${String(value)}`)
}
}
/**
* Canonical serialization of the asset list. Must stay byte-identical to
* serializeMobileWebBundleAssets in config/scripts/build-mobile-web-bundle.mjs; a divergence here
* would reject every honest bundle, so the two move together.
*/
function serializeAssets(assets) {
return JSON.stringify(
[...assets]
.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0))
.map(({ path, sha256, byteLength, contentType }) => ({
path,
sha256,
byteLength,
contentType
}))
)
}
function parseManifest(bundleDir) {
const manifestPath = join(bundleDir, 'manifest.json')
let raw
try {
raw = readFileSync(manifestPath, 'utf8')
} catch (error) {
throw failure(
`no bundle manifest at ${manifestPath} (${error.code ?? error.message}). ${REMEDY}`
)
}
let manifest
try {
manifest = JSON.parse(raw)
} catch (error) {
throw failure(`${manifestPath} is not valid JSON: ${error.message}. ${REMEDY}`)
}
if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) {
throw failure(`${manifestPath} is not a JSON object. ${REMEDY}`)
}
if (manifest.schemaVersion !== 1) {
throw failure(`unsupported manifest schemaVersion: ${String(manifest.schemaVersion)}`)
}
if (typeof manifest.buildId !== 'string' || !SHA256_PATTERN.test(manifest.buildId)) {
throw failure(`manifest buildId is not a sha256 digest: ${String(manifest.buildId)}`)
}
if (typeof manifest.desktopVersion !== 'string' || manifest.desktopVersion.length === 0) {
throw failure('manifest desktopVersion is missing')
}
assertInteger(manifest.minCompatibleRuntimeProtocolVersion, 'minCompatibleRuntimeProtocolVersion')
assertInteger(manifest.runtimeProtocolVersion, 'runtimeProtocolVersion')
assertInteger(manifest.totalBytes, 'totalBytes')
if (manifest.entrypoint !== ENTRYPOINT) {
throw failure(`manifest entrypoint must be ${ENTRYPOINT}, got ${String(manifest.entrypoint)}`)
}
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
throw failure('manifest lists no assets')
}
for (const asset of manifest.assets) {
if (typeof asset !== 'object' || asset === null) {
throw failure('manifest asset entry is not an object')
}
assertSafeRelativePath(asset.path)
if (typeof asset.sha256 !== 'string' || !SHA256_PATTERN.test(asset.sha256)) {
throw failure(`manifest asset ${asset.path} has no sha256 digest`)
}
assertInteger(asset.byteLength, `assets[${asset.path}].byteLength`)
if (typeof asset.contentType !== 'string' || asset.contentType.length === 0) {
throw failure(`manifest asset ${asset.path} has no contentType`)
}
}
if (!manifest.assets.some((asset) => asset.path === manifest.entrypoint)) {
throw failure(`manifest entrypoint ${manifest.entrypoint} is not one of its assets`)
}
const declaredTotal = manifest.assets.reduce((total, asset) => total + asset.byteLength, 0)
if (declaredTotal !== manifest.totalBytes) {
throw failure(
`manifest totalBytes is ${String(manifest.totalBytes)}, its assets sum to ${String(declaredTotal)}`
)
}
const recomputed = createHash('sha256')
.update(serializeAssets(manifest.assets), 'utf8')
.digest('hex')
if (recomputed !== manifest.buildId) {
throw failure(
`manifest buildId ${manifest.buildId} does not match its asset list (expected ${recomputed}). ${REMEDY}`
)
}
return manifest
}
/** Every file under the bundle directory, as a manifest-shaped relative path. */
function listBundleFiles(directory, prefix = '') {
const found = []
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const relativePath = prefix === '' ? entry.name : `${prefix}/${entry.name}`
if (entry.isDirectory()) {
found.push(...listBundleFiles(join(directory, entry.name), relativePath))
} else {
found.push(relativePath)
}
}
return found
}
/**
* Nothing in the bundle directory may be unaccounted for. An asset dropped from the manifest but
* left on disk by an interrupted build ships inside asar, unreachable and unverified, and grows
* the installer; content-addressed names mean stale copies never get overwritten.
*/
function assertNoUnlistedFiles(bundleDir, manifest) {
const listed = new Set(['manifest.json', ...manifest.assets.map((asset) => asset.path)])
const strays = listBundleFiles(bundleDir).filter((path) => !listed.has(path))
if (strays.length > 0) {
throw failure(
`${bundleDir} holds ${String(strays.length)} file(s) the manifest does not list: ` +
`${strays.sort().join(', ')}. ${REMEDY}`
)
}
}
/**
* Packaging guard: electron-builder only warns about a missing input, so without this a release
* would ship an app that advertises the bundle capability and then errors on every request. The
* hash check, not the existence check, is what catches a half-written or stale out/.
*/
function assertMobileWebBundleBuilt(bundleDir = MOBILE_WEB_BUNDLE_DIR) {
const manifest = parseManifest(bundleDir)
assertNoUnlistedFiles(bundleDir, manifest)
for (const asset of manifest.assets) {
const assetPath = join(bundleDir, asset.path)
let size
try {
size = statSync(assetPath).size
} catch (error) {
throw failure(
`manifest lists ${asset.path}, which is missing from ${bundleDir} (${error.code ?? error.message}). ${REMEDY}`
)
}
if (size !== asset.byteLength) {
throw failure(
`${asset.path} is ${String(size)} bytes on disk, manifest says ${String(asset.byteLength)}. ${REMEDY}`
)
}
const sha256 = createHash('sha256').update(readFileSync(assetPath)).digest('hex')
if (sha256 !== asset.sha256) {
throw failure(
`${asset.path} hashes to ${sha256} on disk, manifest says ${asset.sha256}. ${REMEDY}`
)
}
}
console.log(
`[verify-packaged-mobile-web-bundle] OK — buildId ${manifest.buildId}, ` +
`${String(manifest.assets.length)} asset(s), ${String(manifest.totalBytes)} bytes`
)
return manifest
}
// serializeAssets is exported for the parity test that pins it against the builder's and the
// contract's serializers; nothing in packaging calls it from outside this module.
module.exports = { MOBILE_WEB_BUNDLE_DIR, assertMobileWebBundleBuilt, serializeAssets }
@@ -0,0 +1,214 @@
import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { buildMobileWebBundle } from './build-mobile-web-bundle.mjs'
const require = createRequire(import.meta.url)
const {
MOBILE_WEB_BUNDLE_DIR,
assertMobileWebBundleBuilt
} = require('./verify-packaged-mobile-web-bundle.cjs')
const electronBuilderConfig = require('../electron-builder.config.cjs')
const REPO_ROOT = join(import.meta.dirname, '..', '..')
async function withBundle(run) {
const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-guard-'))
const bundleDir = join(scratch, 'mobile-web')
try {
const { manifest } = await buildMobileWebBundle({ outDir: bundleDir })
await run({ bundleDir, manifest })
} finally {
await rm(scratch, { recursive: true, force: true })
}
}
async function rewriteManifest(bundleDir, mutate) {
const manifestPath = join(bundleDir, 'manifest.json')
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
mutate(manifest)
await writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
}
describe('assertMobileWebBundleBuilt', () => {
beforeEach(() => {
vi.spyOn(console, 'log').mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
it('accepts a freshly built bundle', async () => {
await withBundle(({ bundleDir, manifest }) => {
expect(() => assertMobileWebBundleBuilt(bundleDir)).not.toThrow()
expect(manifest.entrypoint).toBe('index.html')
expect(manifest.assets.length).toBeGreaterThanOrEqual(3)
expect(
new Set(manifest.assets.map((asset) => asset.contentType)).size
).toBeGreaterThanOrEqual(2)
})
})
it('fails on a file the manifest does not list, so no stale asset ships inside asar', async () => {
await withBundle(async ({ bundleDir }) => {
// An asset dropped from the manifest keeps its content-addressed name, so nothing ever
// overwrites it; without this check it packs unreachable and unverified.
await writeFile(join(bundleDir, 'assets', 'stale.js'), '// from an earlier build\n', 'utf8')
expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(
/does not list: assets\/stale\.js/
)
})
})
it('accepts exactly the manifest, the entrypoint and the listed assets', async () => {
await withBundle(async ({ bundleDir, manifest }) => {
const onDisk = (await readdir(bundleDir, { recursive: true, withFileTypes: true }))
.filter((entry) => entry.isFile())
.map((entry) => join(entry.parentPath, entry.name).slice(bundleDir.length + 1))
expect(onDisk.toSorted()).toEqual(
['manifest.json', ...manifest.assets.map((asset) => asset.path)].toSorted()
)
})
})
it('fails when the manifest is missing', async () => {
const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-guard-'))
try {
expect(() => assertMobileWebBundleBuilt(scratch)).toThrow(/no bundle manifest/)
} finally {
await rm(scratch, { recursive: true, force: true })
}
})
it('fails when the manifest is not JSON', async () => {
await withBundle(async ({ bundleDir }) => {
await writeFile(join(bundleDir, 'manifest.json'), 'not json', 'utf8')
expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/not valid JSON/)
})
})
it('fails when an asset is tampered with on disk', async () => {
await withBundle(async ({ bundleDir, manifest }) => {
const asset = manifest.assets.find((entry) => entry.path.endsWith('.js'))
const bytes = await readFile(join(bundleDir, asset.path))
// Same length, different content: only the hash check can catch this.
bytes[bytes.length - 1] = bytes.at(-1) === 0x20 ? 0x09 : 0x20
await writeFile(join(bundleDir, asset.path), bytes)
expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/hashes to .* on disk/)
})
})
it('fails when an asset is truncated', async () => {
await withBundle(async ({ bundleDir, manifest }) => {
const asset = manifest.assets.find((entry) => entry.path.endsWith('.css'))
await writeFile(join(bundleDir, asset.path), 'truncated', 'utf8')
expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/bytes on disk, manifest says/)
})
})
it('fails when a listed asset was never written', async () => {
await withBundle(async ({ bundleDir, manifest }) => {
const asset = manifest.assets.find((entry) => entry.path.endsWith('.png'))
await rm(join(bundleDir, asset.path))
expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/which is missing from/)
})
})
it('fails when the manifest buildId no longer matches its asset list', async () => {
await withBundle(async ({ bundleDir }) => {
await rewriteManifest(bundleDir, (manifest) => {
manifest.buildId = 'f'.repeat(64)
})
expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/does not match its asset list/)
})
})
it('fails on an unknown schemaVersion', async () => {
await withBundle(async ({ bundleDir }) => {
await rewriteManifest(bundleDir, (manifest) => {
manifest.schemaVersion = 2
})
expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(
/unsupported manifest schemaVersion/
)
})
})
it('fails when a required field is dropped', async () => {
await withBundle(async ({ bundleDir }) => {
await rewriteManifest(bundleDir, (manifest) => {
delete manifest.desktopVersion
})
expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/desktopVersion is missing/)
})
})
it('fails when totalBytes disagrees with the asset list', async () => {
await withBundle(async ({ bundleDir }) => {
await rewriteManifest(bundleDir, (manifest) => {
manifest.totalBytes += 1
})
expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/its assets sum to/)
})
})
it('refuses an asset path that escapes the bundle directory', async () => {
await withBundle(async ({ bundleDir }) => {
await rewriteManifest(bundleDir, (manifest) => {
manifest.assets[0].path = '../outside.js'
})
expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/not a safe relative path/)
})
})
it('refuses a manifest whose entrypoint is not one of its assets', async () => {
await withBundle(async ({ bundleDir }) => {
await rewriteManifest(bundleDir, (manifest) => {
manifest.entrypoint = 'index.html'
manifest.assets = manifest.assets.filter((asset) => asset.path !== 'index.html')
})
expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow(/is not one of its assets/)
})
})
})
describe('electron-builder packaging wiring', () => {
it('excludes the mobile-web source tree from app.asar', () => {
expect(electronBuilderConfig.files).toContain('!src/mobile-web{,/**/*}')
// The source tree lives under src/, which is excluded wholesale; the explicit entry above
// only survives as a marker, so assert the broad rule is still what does the work.
expect(electronBuilderConfig.files).toContain('!src{,/**/*}')
})
it('does not exclude the built bundle, so out/mobile-web ships like out/web', () => {
const excludesBuiltBundle = electronBuilderConfig.files.some(
(entry) => typeof entry === 'string' && entry.startsWith('!out/mobile-web')
)
expect(excludesBuiltBundle).toBe(false)
})
it('runs the bundle guard in beforePack', () => {
expect(String(electronBuilderConfig.beforePack)).toContain('assertMobileWebBundleBuilt')
})
it('defaults the bundle root to out/mobile-web when electron-builder calls it', () => {
expect(MOBILE_WEB_BUNDLE_DIR).toBe(join(REPO_ROOT, 'out', 'mobile-web'))
// electron-builder passes the context alone, so the default is what ships.
expect(electronBuilderConfig.beforePack.length).toBe(1)
})
it('verifies the bundle root it is given, not the repo one', async () => {
// The seam exists so unit tests need no built out/; it would be worthless if the root were
// accepted and then ignored.
await withBundle(async ({ bundleDir }) => {
await rm(join(bundleDir, 'manifest.json'))
expect(() =>
electronBuilderConfig.beforePack(
{ electronPlatformName: process.platform, arch: process.arch === 'arm64' ? 3 : 1 },
bundleDir
)
).toThrow(/no bundle manifest/)
})
})
})
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
"include": ["../src/mobile-web/src/**/*"],
"compilerOptions": {
"composite": true,
"types": []
}
}
+4 -4
View File
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 61m">
<title>downloads: 61m</title>
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 62m">
<title>downloads: 62m</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
@@ -15,7 +15,7 @@
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text x="37" y="15" fill="#010101" fill-opacity=".3">downloads</text>
<text x="37" y="14">downloads</text>
<text x="90" y="15" fill="#010101" fill-opacity=".3">61m</text>
<text x="90" y="14">61m</text>
<text x="90" y="15" fill="#010101" fill-opacity=".3">62m</text>
<text x="90" y="14">62m</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 935 B

After

Width:  |  Height:  |  Size: 935 B

@@ -0,0 +1,32 @@
# Release aborted shared auth filesystem waits
When a filesystem operation remains pending, later Codex/Kimi quota polls reuse it and wait with new deadlines. The old waiter uses `Promise.race` for every poll. On the installed Electron runtime, each abandoned race retains its rejection reason until the raw operation settles, despite removing its abort listener. The fix uses the existing `PromiseSettlementWaiters` registry, which attaches one raw-result reaction and removes expired waiters.
The original ownership symbols, last-waiter cancellation finalizer, one-raw-operation behavior, live callers, and future reads of a late result are preserved. Auth opts into deferred abort settlement so an already-queued raw result keeps its `Promise.race` priority; 24 success/failure/abort schedules compare equal before and after. Existing registry consumers keep their immediate-abort behavior. The abort factory type accepts `unknown` so false, zero, strings, and objects retain the existing auth rejection semantics. No admission or timeout limit changes.
## Reproduce
```sh
ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=128 docs/audits/auth-filesystem-wait-retention/reproduce.cjs
```
For Electron, run the installed Electron executable with `ELECTRON_RUN_AS_NODE=1` and the same arguments/environment. This launches no window. The proof reconstructs the original sources by reversing `fix.patch`; expected baseline hashes in `source-versions.json` make source drift fail. Both versions run the actual production scheduler/waiter code, with only the raw filesystem operation replaced by one manually settled promise. A 15-second deadline fails stalled proof execution.
| Runtime / source | Plain aborted Errors alive while raw result pending | Amplified payload objects alive | After raw result settles | After owner drops |
| ---------------------------------------- | --------------------------------------------------- | ------------------------------- | ------------------------ | ----------------- |
| Node 26.6.0 / original | 1 of 128 | 1 of 128 | 1 | 0 |
| Node 26.6.0 / fixed | 1 of 128 | 1 of 128 | 1 | 0 |
| Electron 43.7.0, Node 24.21.0 / original | 128 of 128 | 128 of 128 | 1 | 0 |
| Electron 43.7.0, Node 24.21.0 / fixed | 1 of 128 | 1 of 128 | 1 | 0 |
The one remaining reason belongs to the existing cancellation controller's first abort. Dropping the shared operation releases it. AbortController objects and abort listeners are released by both versions. The amplified arm attaches a **synthetic 64 KiB Uint8Array** to each Error, at most 8 MiB per case. Ordinary timeout errors are much smaller; the separate plain-Error arm verifies that artificial bytes are not needed to reproduce retention. Runtime differences are measured, without attributing them to a particular V8 change.
Controls check an already-aborted first caller never starting raw work, raw rejection identity, aborted caller identity, future callers receiving late and already-settled results, a live sibling surviving cancellation, arbitrary abort reasons, and removed listeners. The unit regression additionally checks that repeated expired waits add no raw-result reactions and that their plain Error objects are collectible while a live caller still needs the operation.
Validation: 50 auth/registry tests pass, including 18 new cases; the reverse-patch configuration produces two expected failures and 48 passes. Six existing watcher-consumer suites pass another 39 tests. Node, Web, and CLI typechecks, focused ordinary/type-aware lint, formatting, and the changed-code quality gate pass. `validation.json` records the test paths and scope. To run the prior implementation against the current tests, use `--config docs/audits/auth-filesystem-wait-retention/before.config.mjs` with those six auth/registry test paths.
## Scope and limits
The three production consumers are `codex-auth-presence.ts`, `codex-backend-auth.ts`, and `kimi-fetcher.ts`. Each intentionally retains the shared operation until actual filesystem settlement to avoid stacking native requests when UNC/WSL reads stall. This audit does not reproduce a real filesystem stall, historical Electron binary behavior, or an affected-host workload.
The auth source matches `v1.4.198` and the audited main revision; the existing registry also matches main. The earlier broad accumulator PR #10179, reverted by #10255, added path/waiter/admission limits to this module. This change instead removes abandoned wait reactions and introduces no such limits. Nothing here attributes #19831 or #19768 to this mechanism or claims an incident-scale memory slope.
@@ -0,0 +1,24 @@
import { resolve } from 'node:path'
import { createRequire } from 'node:module'
import { defineConfig, mergeConfig } from 'vitest/config'
import baseConfig from '../../../config/vitest.config.ts'
const loadSources = createRequire(import.meta.url)(
resolve('docs/audits/auth-filesystem-wait-retention/sources.cjs')
)
const { before } = loadSources()
export default mergeConfig(
baseConfig,
defineConfig({
plugins: [
{
name: 'auth-wait-before-fix',
enforce: 'pre',
transform(_code, id) {
const source = before.get(resolve(id.split('?')[0]))
return source === undefined ? undefined : { code: source, map: null }
}
}
]
})
)
@@ -0,0 +1,598 @@
{
"sourceHashes": {
"src/main/rate-limits/auth-filesystem-operation.ts": {
"before": "581d7045220e92602ca83ec335576889dcba5ada4a699e866ea1dba10afbecac",
"after": "bfac9dc25c3f3c36ab590bc6e01be39ec19dea5a872f5c409ed149e65258f9e2"
},
"src/shared/promise-settlement-waiters.ts": {
"before": "b0b60bae6dcca4bb4f335ef7dda250f4350aa5b0306d3595b887cc4af4493060",
"after": "92618db591cf1fc1ad52f257cc9f3314310bf902fbd24e5d18abcf93afb8e722"
}
},
"runtime": {
"node": "24.21.0",
"acorn": "8.18.0",
"ada": "4.0.0",
"amaro": "1.1.11",
"ares": "1.34.8",
"brotli": "1.2.0",
"cldr": "48.0",
"icu": "78.2",
"llhttp": "9.4.3",
"merve": "1.2.2",
"modules": "148",
"napi": "10",
"nbytes": "0.1.4",
"ncrypto": "0.0.1",
"nghttp2": "1.70.0",
"nghttp3": "",
"ngtcp2": "",
"openssl": "0.0.0",
"simdjson": "4.6.7",
"simdutf": "7.7.0",
"sqlite": "3.53.4",
"tz": "2025c",
"undici": "7.29.1",
"unicode": "17.0",
"uv": "1.52.1",
"uvwasi": "0.0.23",
"v8": "15.0.245.31-electron.0",
"zlib": "1.3.2.1-motley",
"zstd": "1.6.0",
"electron": "43.7.0",
"chrome": "150.0.7871.250"
},
"scenario": "128 aborted waits on one already-started unresolved operation; amplified arm has 64 KiB synthetic payload per abort Error; real native filesystem stall not reproduced",
"before": {
"cases": [
{
"amplify": false,
"abortedWaits": 128,
"unresolved": {
"reasons": 128,
"controllers": 0,
"payloads": 0,
"rawCalls": 1
},
"settled": {
"reasons": 1,
"controllers": 0,
"payloads": 0
},
"dropped": {
"reasons": 0,
"controllers": 0,
"payloads": 0
}
},
{
"amplify": true,
"abortedWaits": 128,
"unresolved": {
"reasons": 128,
"controllers": 0,
"payloads": 128,
"rawCalls": 1
},
"settled": {
"reasons": 1,
"controllers": 0,
"payloads": 1
},
"dropped": {
"reasons": 0,
"controllers": 0,
"payloads": 0
}
}
],
"controls": {
"lateResultDelivered": true,
"settledResultDelivered": true,
"oneRawCall": 1,
"rawRejectionPreserved": true,
"preAbortedRawCalls": 0,
"allAbortListenersRemoved": true,
"arbitraryAbortReasonsPreserved": 4,
"liveSiblingSurvivesAbort": true
},
"settlementOrder": [
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 1,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 2,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 3,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 4,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 5,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 1,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 2,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 3,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 4,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 5,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 1,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 2,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 3,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 4,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 5,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 1,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 2,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 3,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 4,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 5,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
}
],
"importedHashes": {
"src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a"
},
"bundleSha256": "f895e71ca0f2102e7ff36fa133ae2065fa9810769d7ee6059c826f1a72bcc122"
},
"after": {
"cases": [
{
"amplify": false,
"abortedWaits": 128,
"unresolved": {
"reasons": 1,
"controllers": 0,
"payloads": 0,
"rawCalls": 1
},
"settled": {
"reasons": 1,
"controllers": 0,
"payloads": 0
},
"dropped": {
"reasons": 0,
"controllers": 0,
"payloads": 0
}
},
{
"amplify": true,
"abortedWaits": 128,
"unresolved": {
"reasons": 1,
"controllers": 0,
"payloads": 1,
"rawCalls": 1
},
"settled": {
"reasons": 1,
"controllers": 0,
"payloads": 1
},
"dropped": {
"reasons": 0,
"controllers": 0,
"payloads": 0
}
}
],
"controls": {
"lateResultDelivered": true,
"settledResultDelivered": true,
"oneRawCall": 1,
"rawRejectionPreserved": true,
"preAbortedRawCalls": 0,
"allAbortListenersRemoved": true,
"arbitraryAbortReasonsPreserved": 4,
"liveSiblingSurvivesAbort": true
},
"settlementOrder": [
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 1,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 2,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 3,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 4,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 5,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 1,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 2,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 3,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 4,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 5,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 1,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 2,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 3,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 4,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 5,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 1,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 2,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 3,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 4,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 5,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
}
],
"importedHashes": {
"src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a"
},
"bundleSha256": "04324b281a776aa1018995a36d11ff7cf56b4dd737761c3d5b0fb1ebc0047652"
}
}
@@ -0,0 +1,98 @@
diff --git a/src/main/rate-limits/auth-filesystem-operation.ts b/src/main/rate-limits/auth-filesystem-operation.ts
index 228234e92c..7d030ab01e 100644
--- a/src/main/rate-limits/auth-filesystem-operation.ts
+++ b/src/main/rate-limits/auth-filesystem-operation.ts
@@ -1,4 +1,5 @@
import { parseWslUncPath } from '../../shared/wsl-paths'
+import { PromiseSettlementWaiters } from '../../shared/promise-settlement-waiters'
const MAX_CONCURRENT_WSL_AUTH_OPERATIONS = 2
const activeWslOperationDistros = new Set<string>()
@@ -139,10 +140,9 @@ export function createAuthFilesystemOperation<T>(
const waiters = new Set<symbol>()
let settled = false
const result = scheduleAuthFilesystemOperation(authPath, neededController.signal, operation)
- const markSettled = (): void => {
+ const settlementWaiters = new PromiseSettlementWaiters(result, () => {
settled = true
- }
- void result.then(markSettled, markSettled)
+ })
return {
result,
@@ -156,20 +156,18 @@ export function createAuthFilesystemOperation<T>(
const waiter = Symbol('auth-filesystem-waiter')
waiters.add(waiter)
- let onAbort: (() => void) | null = null
- const aborted = new Promise<never>((_resolve, reject) => {
- onAbort = () => reject(getAbortReason(signal))
- signal.addEventListener('abort', onAbort, { once: true })
- })
- return Promise.race([result, aborted]).finally(() => {
- if (onAbort) {
- signal.removeEventListener('abort', onAbort)
- }
- waiters.delete(waiter)
- if (!settled && waiters.size === 0) {
- neededController.abort(getAbortReason(signal))
- }
- })
+ return settlementWaiters
+ .wait({
+ signal,
+ abortInMicrotask: true,
+ createAbortError: () => getAbortReason(signal)
+ })
+ .finally(() => {
+ waiters.delete(waiter)
+ if (!settled && waiters.size === 0) {
+ neededController.abort(getAbortReason(signal))
+ }
+ })
}
}
}
diff --git a/src/shared/promise-settlement-waiters.ts b/src/shared/promise-settlement-waiters.ts
index 98ec24b25c..97230d54dd 100644
--- a/src/shared/promise-settlement-waiters.ts
+++ b/src/shared/promise-settlement-waiters.ts
@@ -13,8 +13,10 @@ type PromiseSettlementWaiter<T> = {
export type PromiseSettlementWaitOptions<T> = {
signal?: AbortSignal
+ /** Preserve Promise.race ordering when raw settlement and abort share a turn. */
+ abortInMicrotask?: boolean
timeoutMs?: number
- createAbortError?: () => Error
+ createAbortError?: () => unknown
createTimeoutError?: () => Error
onFulfilled?: (value: T) => void
onAbandon?: (reason: 'abort' | 'timeout') => void
@@ -50,7 +52,7 @@ export class PromiseSettlementWaiters<T> {
}
return new Promise<T>((resolve, reject) => {
let waiter!: PromiseSettlementWaiter<T>
- const abandon = (reason: 'abort' | 'timeout', error: Error): void => {
+ const abandon = (reason: 'abort' | 'timeout', error: unknown): void => {
if (!this.waiters.delete(waiter)) {
return
}
@@ -58,8 +60,14 @@ export class PromiseSettlementWaiters<T> {
options.onAbandon?.(reason)
reject(error)
}
- const onAbort = (): void =>
- abandon('abort', options.createAbortError?.() ?? createDefaultAbortError())
+ const onAbort = (): void => {
+ const error = options.createAbortError?.() ?? createDefaultAbortError()
+ if (options.abortInMicrotask) {
+ queueMicrotask(() => abandon('abort', error))
+ } else {
+ abandon('abort', error)
+ }
+ }
waiter = {
resolve,
reject,
@@ -0,0 +1,597 @@
{
"sourceHashes": {
"src/main/rate-limits/auth-filesystem-operation.ts": {
"before": "581d7045220e92602ca83ec335576889dcba5ada4a699e866ea1dba10afbecac",
"after": "bfac9dc25c3f3c36ab590bc6e01be39ec19dea5a872f5c409ed149e65258f9e2"
},
"src/shared/promise-settlement-waiters.ts": {
"before": "b0b60bae6dcca4bb4f335ef7dda250f4350aa5b0306d3595b887cc4af4493060",
"after": "92618db591cf1fc1ad52f257cc9f3314310bf902fbd24e5d18abcf93afb8e722"
}
},
"runtime": {
"node": "26.6.0",
"acorn": "8.17.0",
"ada": "4.0.0",
"amaro": "1.1.11",
"ares": "1.34.8",
"brotli": "1.2.0",
"cldr": "48.0",
"icu": "78.3",
"libffi": "3.7.1",
"llhttp": "9.4.3",
"merve": "1.2.2",
"modules": "147",
"napi": "10",
"nbytes": "0.1.4",
"ncrypto": "0.0.1",
"nghttp2": "1.70.0",
"nghttp3": "",
"ngtcp2": "",
"openssl": "3.6.3",
"simdjson": "4.6.6",
"simdutf": "7.7.0",
"sqlite": "3.53.4",
"tz": "2026a",
"undici": "8.9.0",
"unicode": "17.0",
"uv": "1.52.1",
"uvwasi": "0.0.23",
"v8": "14.6.202.34-node.26",
"zlib": "1.2.12",
"zstd": "1.5.7"
},
"scenario": "128 aborted waits on one already-started unresolved operation; amplified arm has 64 KiB synthetic payload per abort Error; real native filesystem stall not reproduced",
"before": {
"cases": [
{
"amplify": false,
"abortedWaits": 128,
"unresolved": {
"reasons": 1,
"controllers": 0,
"payloads": 0,
"rawCalls": 1
},
"settled": {
"reasons": 1,
"controllers": 0,
"payloads": 0
},
"dropped": {
"reasons": 0,
"controllers": 0,
"payloads": 0
}
},
{
"amplify": true,
"abortedWaits": 128,
"unresolved": {
"reasons": 1,
"controllers": 0,
"payloads": 1,
"rawCalls": 1
},
"settled": {
"reasons": 1,
"controllers": 0,
"payloads": 1
},
"dropped": {
"reasons": 0,
"controllers": 0,
"payloads": 0
}
}
],
"controls": {
"lateResultDelivered": true,
"settledResultDelivered": true,
"oneRawCall": 1,
"rawRejectionPreserved": true,
"preAbortedRawCalls": 0,
"allAbortListenersRemoved": true,
"arbitraryAbortReasonsPreserved": 4,
"liveSiblingSurvivesAbort": true
},
"settlementOrder": [
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 1,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 2,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 3,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 4,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 5,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 1,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 2,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 3,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 4,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 5,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 1,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 2,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 3,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 4,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 5,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 1,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 2,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 3,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 4,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 5,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
}
],
"importedHashes": {
"src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a"
},
"bundleSha256": "f895e71ca0f2102e7ff36fa133ae2065fa9810769d7ee6059c826f1a72bcc122"
},
"after": {
"cases": [
{
"amplify": false,
"abortedWaits": 128,
"unresolved": {
"reasons": 1,
"controllers": 0,
"payloads": 0,
"rawCalls": 1
},
"settled": {
"reasons": 1,
"controllers": 0,
"payloads": 0
},
"dropped": {
"reasons": 0,
"controllers": 0,
"payloads": 0
}
},
{
"amplify": true,
"abortedWaits": 128,
"unresolved": {
"reasons": 1,
"controllers": 0,
"payloads": 1,
"rawCalls": 1
},
"settled": {
"reasons": 1,
"controllers": 0,
"payloads": 1
},
"dropped": {
"reasons": 0,
"controllers": 0,
"payloads": 0
}
}
],
"controls": {
"lateResultDelivered": true,
"settledResultDelivered": true,
"oneRawCall": 1,
"rawRejectionPreserved": true,
"preAbortedRawCalls": 0,
"allAbortListenersRemoved": true,
"arbitraryAbortReasonsPreserved": 4,
"liveSiblingSurvivesAbort": true
},
"settlementOrder": [
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 1,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 2,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 3,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 4,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": false,
"ticks": 5,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 1,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 2,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 3,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 4,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": true,
"rejectRaw": true,
"ticks": 5,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 1,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 2,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 3,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 4,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": false,
"ticks": 5,
"outcome": {
"status": "fulfilled",
"value": "raw success"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 0,
"outcome": {
"status": "rejected",
"reason": "caller aborted"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 1,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 2,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 3,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 4,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
},
{
"startedBefore": false,
"rejectRaw": true,
"ticks": 5,
"outcome": {
"status": "rejected",
"reason": "raw failure"
}
}
],
"importedHashes": {
"src/shared/wsl-paths.ts": "1d9dcf5a1ff6693c02ff60a6b074eb2ebb7e83bda12aceba7e0b208d410b6d1a"
},
"bundleSha256": "04324b281a776aa1018995a36d11ff7cf56b4dd737761c3d5b0fb1ebc0047652"
}
}
@@ -0,0 +1,242 @@
const assert = require('node:assert/strict')
const { createHash } = require('node:crypto')
const { readFileSync, writeFileSync } = require('node:fs')
const { resolve } = require('node:path')
const Module = require('node:module')
const { getEventListeners } = require('node:events')
const { build } = require('esbuild')
const { root, before, after, hashes } = require('./sources.cjs')()
const settlementOrder = require('./settlement-order.cjs')
assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1')
assert.equal(typeof global.gc, 'function')
const sourcePath = 'src/main/rate-limits/auth-filesystem-operation.ts'
const entry = resolve(root, sourcePath)
let candidate = false
let createAuthFilesystemOperation
const turn = () => new Promise((resolveTurn) => setImmediate(resolveTurn))
async function collect() {
for (let index = 0; index < 5; index++) {
await turn()
global.gc()
}
await turn()
}
const count = (refs) => refs.reduce((total, ref) => total + Number(ref.deref() !== undefined), 0)
async function abandonedWait(operation, index, amplify) {
const controller = new AbortController()
const reason = new Error(`synthetic expired poll ${index}`)
// Payload amplifies the retained rejection object; normal timeout errors are much smaller.
if (amplify) {
reason.auditPayload = new Uint8Array(64 * 1024)
reason.auditPayload.fill(index & 255)
}
const references = {
reason: new WeakRef(reason),
controller: new WeakRef(controller),
...(amplify ? { payload: new WeakRef(reason.auditPayload) } : {})
}
const waiting = operation.wait(controller.signal)
controller.abort(reason)
await waiting.catch(() => {})
assert.equal(getEventListeners(controller.signal, 'abort').length, 0)
return references
}
function snapshot(refs) {
return {
reasons: count(refs.map((ref) => ref.reason)),
controllers: count(refs.map((ref) => ref.controller)),
payloads: count(refs.flatMap((ref) => (ref.payload ? [ref.payload] : [])))
}
}
async function retention(amplify) {
let settleRaw
let rawCalls = 0
let operation = createAuthFilesystemOperation('/synthetic-local-auth', () => {
rawCalls++
return new Promise((resolveRaw) => {
settleRaw = resolveRaw
})
})
await turn()
assert.equal(rawCalls, 1)
const refs = []
for (let index = 0; index < 128; index++) {
refs.push(await abandonedWait(operation, index, amplify))
}
await collect()
const unresolved = { ...snapshot(refs), rawCalls }
if (candidate) {
assert.equal(unresolved.reasons, 1)
}
settleRaw('finished')
await operation.result
await collect()
const settled = snapshot(refs)
assert.equal(settled.reasons, 1)
operation = null
settleRaw = null
await collect()
const dropped = snapshot(refs)
assert.deepEqual(dropped, { reasons: 0, controllers: 0, payloads: 0 })
return { amplify, abortedWaits: refs.length, unresolved, settled, dropped }
}
async function controls() {
let rawCalls = 0
let finish
const operation = createAuthFilesystemOperation('/synthetic-auth-controls', () => {
rawCalls++
return new Promise((resolveRaw) => {
finish = resolveRaw
})
})
const expired = new AbortController()
const expiredReason = new Error('expired first poll')
const abortedWait = operation.wait(expired.signal)
await turn()
expired.abort(expiredReason)
await assert.rejects(abortedWait, (reason) => reason === expiredReason)
const later = new AbortController()
const lateWait = operation.wait(later.signal)
finish('late raw result')
assert.equal(await lateWait, 'late raw result')
assert.equal(rawCalls, 1)
assert.equal(await operation.wait(later.signal), 'late raw result')
assert.equal(getEventListeners(expired.signal, 'abort').length, 0)
assert.equal(getEventListeners(later.signal, 'abort').length, 0)
let rejectedCalls = 0
const rejectedReason = new Error('raw rejected')
const rejected = createAuthFilesystemOperation('/synthetic-auth-rejected', async () => {
rejectedCalls++
throw rejectedReason
})
await assert.rejects(
rejected.wait(new AbortController().signal),
(reason) => reason === rejectedReason
)
let preAbortedCalls = 0
const preAborted = createAuthFilesystemOperation('/synthetic-auth-pre-aborted', async () => {
preAbortedCalls++
return 'unexpected'
})
const priorAbort = new AbortController()
priorAbort.abort(expiredReason)
await assert.rejects(preAborted.wait(priorAbort.signal), (reason) => reason === expiredReason)
await assert.rejects(preAborted.result, (reason) => reason === expiredReason)
assert.equal(preAbortedCalls, 0)
let finishReasons
const reasonOperation = createAuthFilesystemOperation(
'/synthetic-auth-reasons',
() =>
new Promise((resolveRaw) => {
finishReasons = resolveRaw
})
)
await turn()
const reasons = [false, 0, 'string abort', { code: 'custom' }]
for (const reason of reasons) {
const controller = new AbortController()
const pending = reasonOperation.wait(controller.signal)
controller.abort(reason)
await assert.rejects(pending, (observed) => observed === reason)
assert.equal(getEventListeners(controller.signal, 'abort').length, 0)
}
const activeController = new AbortController()
const cancelledController = new AbortController()
const active = reasonOperation.wait(activeController.signal)
const cancelled = reasonOperation.wait(cancelledController.signal)
cancelledController.abort(expiredReason)
await assert.rejects(cancelled, (reason) => reason === expiredReason)
finishReasons('active result')
assert.equal(await active, 'active result')
assert.equal(getEventListeners(activeController.signal, 'abort').length, 0)
return {
lateResultDelivered: true,
settledResultDelivered: true,
oneRawCall: rawCalls,
rawRejectionPreserved: rejectedCalls === 1,
preAbortedRawCalls: preAbortedCalls,
allAbortListenersRemoved: true,
arbitraryAbortReasonsPreserved: reasons.length,
liveSiblingSurvivesAbort: true
}
}
async function phase(sources, fixed) {
candidate = fixed
const built = await build({
entryPoints: [entry],
bundle: true,
platform: 'node',
format: 'cjs',
write: false,
metafile: true,
logLevel: 'silent',
plugins: [
{
name: 'hash-fenced-proof-source',
setup(api) {
api.onLoad(
{ filter: /(?:auth-filesystem-operation|promise-settlement-waiters)\.ts$/ },
(args) =>
sources.has(args.path)
? {
contents: sources.get(args.path),
loader: 'ts',
resolveDir: resolve(args.path, '..')
}
: undefined
)
}
}
]
})
const bundled = built.outputFiles[0].text
const moduleOwner = new Module(entry, module)
moduleOwner.filename = entry
moduleOwner.paths = module.paths
moduleOwner._compile(bundled, entry)
createAuthFilesystemOperation = moduleOwner.exports.createAuthFilesystemOperation
const importedHashes = Object.fromEntries(
Object.keys(built.metafile.inputs)
.filter((path) => !sources.has(resolve(root, path)))
.map((path) => [
path,
createHash('sha256')
.update(readFileSync(resolve(root, path)))
.digest('hex')
])
)
return {
cases: [await retention(false), await retention(true)],
controls: await controls(),
settlementOrder: await settlementOrder(createAuthFilesystemOperation),
importedHashes,
bundleSha256: createHash('sha256').update(bundled).digest('hex')
}
}
async function run() {
const result = {
sourceHashes: hashes,
runtime: process.versions,
scenario:
'128 aborted waits on one already-started unresolved operation; amplified arm has 64 KiB synthetic payload per abort Error; real native filesystem stall not reproduced',
before: await phase(before, false),
after: await phase(after, true)
}
assert.deepEqual(result.after.settlementOrder, result.before.settlementOrder)
const output = process.argv[2]
? resolve(process.argv[2])
: resolve(__dirname, `${process.versions.electron ? 'electron-' : 'node-'}results.json`)
writeFileSync(output, `${JSON.stringify(result, null, 2)}\n`)
console.log(JSON.stringify(result, null, 2))
}
const deadline = setTimeout(() => {
console.error('Auth wait proof exceeded 15 seconds')
process.exit(1)
}, 15_000)
run()
.catch((error) => {
console.error(error)
process.exitCode = 1
})
.finally(() => clearTimeout(deadline))
@@ -0,0 +1,37 @@
const turn = () => new Promise((resolveTurn) => setImmediate(resolveTurn))
module.exports = async function settlementOrder(create) {
const cases = []
for (const startedBefore of [true, false]) {
for (const rejectRaw of [false, true]) {
for (let ticks = 0; ticks < 6; ticks++) {
let settle
const operation = create(
'synthetic-auth-order',
() =>
new Promise((resolveRaw, failRaw) => {
settle = () => (rejectRaw ? failRaw('raw failure') : resolveRaw('raw success'))
})
)
await turn()
const controller = new AbortController()
const start = () =>
operation.wait(controller.signal).then(
(value) => ({ status: 'fulfilled', value }),
(reason) => ({ status: 'rejected', reason })
)
let waiting = startedBefore ? start() : null
settle()
for (let index = 0; index < ticks; index++) {
await Promise.resolve()
}
if (!startedBefore) {
waiting = start()
}
controller.abort('caller aborted')
cases.push({ startedBefore, rejectRaw, ticks, outcome: await waiting })
}
}
}
return cases
}
@@ -0,0 +1,11 @@
{
"baselineHashes": {
"src/main/rate-limits/auth-filesystem-operation.ts": "581d7045220e92602ca83ec335576889dcba5ada4a699e866ea1dba10afbecac",
"src/shared/promise-settlement-waiters.ts": "b0b60bae6dcca4bb4f335ef7dda250f4350aa5b0306d3595b887cc4af4493060"
},
"checkedMainRevision": "77cd61df396f25ec91ee2d5ddcbd1f55aa94f818",
"sharedTestBaselineSha256": "1e274da93106f31b9b57aa48fd965a4d81c8adf4ebe1bfa20a196aa3e529a73a",
"authSourceIdenticalNamedRefs": ["origin/main", "v1.4.198"],
"registrySourceIdenticalNamedRefs": ["origin/main"],
"historicalRuntimeReproduced": false
}
@@ -0,0 +1,29 @@
const assert = require('node:assert/strict')
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const { resolve } = require('node:path')
const { applyPatch, parsePatch, reversePatch } = require('diff')
module.exports = function loadSources() {
const root = resolve(__dirname, '../../..')
const expected = JSON.parse(readFileSync(resolve(__dirname, 'source-versions.json'), 'utf8'))
const parsed = parsePatch(readFileSync(resolve(__dirname, 'fix.patch'), 'utf8'))
const before = new Map()
const after = new Map()
const hashes = {}
assert.equal(parsed.length, 2)
for (const patch of parsed) {
const path = patch.newFileName.replace(/^b\//, '')
assert.ok(Object.hasOwn(expected.baselineHashes, path), `Unexpected patch path: ${path}`)
const absolute = resolve(root, path)
const current = readFileSync(absolute, 'utf8')
const baseline = applyPatch(current, reversePatch(patch))
assert.notEqual(baseline, false, `Source changed; review fix.patch: ${path}`)
const hash = (source) => createHash('sha256').update(source).digest('hex')
assert.equal(hash(baseline), expected.baselineHashes[path], `Baseline drift: ${path}`)
before.set(absolute, baseline)
after.set(absolute, current)
hashes[path] = { before: hash(baseline), after: hash(current) }
}
return { root, before, after, hashes }
}
@@ -0,0 +1,61 @@
{
"backgroundLaunch": true,
"authAndRegistryTests": {
"before": { "passed": 48, "failed": 2, "exitCode": 1 },
"after": { "passed": 50, "failed": 0, "exitCode": 0 },
"newCases": 18,
"expectedBeforeFailures": [
"PromiseSettlementWaiters preserves abort scheduling with abortInMicrotask=true",
"shared auth filesystem wait lifetime releases aborted poll reasons while one native operation remains needed"
],
"paths": [
"src/main/rate-limits/auth-filesystem-operation.test.ts",
"src/main/rate-limits/auth-filesystem-operation-retention.test.ts",
"src/main/rate-limits/codex-auth-presence.test.ts",
"src/main/rate-limits/kimi-fetcher-wsl-home.test.ts",
"src/main/rate-limits/kimi-fetcher.test.ts",
"src/shared/promise-settlement-waiters.test.ts"
]
},
"existingRegistryConsumers": {
"passed": 39,
"failed": 0,
"exitCode": 0,
"paths": [
"src/relay/relay-watcher-setup-wait.test.ts",
"src/relay/relay-filesystem-watch-registry.test.ts",
"src/main/providers/ssh-filesystem-provider-watch-waiters.test.ts",
"src/main/runtime/file-watcher-host.test.ts",
"src/main/ipc/runtime-watcher-pending-assignment.test.ts",
"src/main/ipc/parcel-watcher-supervisor-capacity-wait.test.ts"
]
},
"typechecks": {
"command": "node config/scripts/run-typecheck-projects-in-parallel.mjs",
"projects": [
"config/tsconfig.node.json",
"config/tsconfig.tc.cli.json",
"config/tsconfig.tc.web.json"
],
"exitCode": 0
},
"focusedOxlint": { "ordinaryExitCode": 0, "typeAwareExitCode": 0 },
"formatCheckExitCode": 0,
"changedCodeQuality": {
"base": "2fccacadbe23",
"changedFiles": 297,
"newFindings": 0,
"exitCode": 0
},
"proof": {
"node": "26.6.0",
"electron": "43.7.0",
"electronNode": "24.21.0",
"bothExitCode": 0,
"orderingCasesPerRuntime": 24,
"beforeAfterOrderingEqual": true,
"rawFilesystemStall": "injected pending promise, not an affected-host capture",
"amplifiedBytesPerCase": 8388608,
"ordinaryErrorBytes": "not measured"
}
}
@@ -0,0 +1,77 @@
# Closed browser dispatcher retains completed results behind pending native work
Before the fix, completed command results stayed in a closed browser dispatcher until its final handler settled. The fix releases settled cache records at close and drops newly settled records while closed. Pending native handlers, page authority, and executor teardown keep their existing lifetime.
## Actual paths and bounds
Source references in this section describe the hash-fenced baseline. `BrowserClientHostCommandDispatcher.dispatch` refuses every command once closed, before authority or duplicate lookup (`browser-client-host-command-dispatcher.ts:7779`). `close` aborts active work and cancels queued work, but retains its pages and cached completed records when the join returns false (`:156179`). `finishHandler` clears those owners only after the last native handler settles (`:268272`). A newly settled cancellation record is also cached while a sibling remains active (`:297302`).
`BrowserClientHostCommandResultCache.clear` drops only its record-to-page index. PageState.records and PageState.sequencesByCommandId also own the records; clearing just that index does not release result graphs. Existing `releasePage` uses exact settled-record eviction to remove both indexes (`browser-client-host-command-result-cache.ts:2755`).
Defaults (`browser-client-host-command-state.ts:713`) are 256 pages, 256 active commands, 8 concurrent handlers, 32 queued/page, 64 cached results/page, 1,024 cached results total, and a 5,000 ms close/retirement join. Automation result schema allows at most 768 KiB of JSON-serialized value (`browser-client-automation-protocol.ts:5,8999,116131`). These are count/serialized-value limits, not a guaranteed heap/RSS size. The audit uses 32 tiny results, one native wait, and one canceled tiny queued input; it does not allocate near those maxima.
Production composition calls dispatcher close via `PairedRuntimeBrowserClientHost.closeHost` (`paired-runtime-browser-client-host.ts:165180`). If close times out, actual `closeBrowserClientHostComposition` defers executor close behind `whenHandlersSettled` (`paired-runtime-browser-client-host-teardown.ts:3756`). Keeping handler/page/native authority alive is intentional. Completed results cannot serve new or duplicate closed requests and need not share that lifetime.
## Ordinary handler time boundaries
- The navigation command checks cancellation before starting, then calls `routeWebContents.navigateGuest` (`browser-client-page-command-execution.ts:2040`). The actual registry delegates to `navigateBrowserRouteGuest`, which awaits native `guest.loadURL` (`browser-route-guest-lifecycle.ts:99123`) without adding a JS deadline or taking the AbortSignal. Native completion/rejection remains its settlement owner.
- Automation checks cancellation before execution, registers the exact guest, and forwards the signal into RPC (`browser-client-page-automation-runtime.ts:4257`; startup `main-process-ready-runtime.ts:6177`). Core handlers such as browser.snapshot destructure runtime and call its method without observing that signal (`runtime/rpc/methods/browser-core.ts:3943`).
- The ordinary agent-browser helper execution has a 90-second default subprocess timeout (`agent-browser-bridge-types.ts:6`; `agent-browser-bridge-raw-process.ts:2036`), with overrides for some operations. The agent-bridge embedded goto wrapper separately has a 30-second navigation timeout. Those deadlines are not a universal bound on all handler phases, and the direct route navigate path above does not use that wrapper.
The condition is a handler that outlives the dispatcher's five-second join. No affected-host occurrence, natural indefinite stall, or incident attribution has been established.
## Bounded actual-source proof
`scenario.test.mjs` uses the actual dispatcher, page executor, automation runtime, browser.snapshot RPC descriptor, native-navigation wrapper, and composition teardown function. Existing page-executor harness supplies renderer/session/route ports. The runtime's browserSnapshot/native loadURL are small controlled ports; no Electron window, OS child, real web request, or network is used. Logger/mock result arrays do not own the produced payloads: the automation output and dispatcher handler are plain functions, and each completed value is observed only through WeakRef after the helper returns.
Both Node 26.6 and Electron 43.7 / Node 24.21 pass all 4 cases before and after the two-file fix. A 15 ms join override keeps the proof bounded; the native port is explicitly resolved in finally and all native custody eventually settles.
| Observation | Before | Fixed |
| ---------------------------------------------------------------------- | -----: | ----: |
| Completed small payload objects alive after timed-out close | 32 | 0 |
| Cached records after close (32 results + create + queued cancellation) | 34 | 0 |
| Canceled queued input still reachable | yes | no |
| Running native handlers after close | 1 | 1 |
| Page/executor and route/session custody retained | yes | yes |
| Close repeated before native settlement | false | false |
| whenClosed pending before native settlement | yes | yes |
| Payload objects alive after explicit native settlement | 0 | 0 |
| First late cancellation cached while sibling remains pending | 1 | 0 |
Open request replay preserves the exact same Promise and runs once. Closed duplicates fail with dispatcher_closed. Normal native resolution and rejection both complete the existing settlement path. Executor close and route/session release happen only after native settlement in both variants.
The initial candidate compatibility run passed 78 tests in 5 files, including its three lifecycle cases and existing dispatcher, page executor, paired-runtime composition, and paired-runtime host tests. The permanent retention suite adds two object-lifetime regressions: releasing completed results while native navigation stays pending, and releasing one late closed record while a sibling handler remains active. Final validation passed 77 tests across 5 production suites, Node typechecking, and all five explicit quality scans over product and artifact code. Reversing the patch produces the two expected lifetime failures while all 16 existing dispatcher tests pass. Commands and outcomes are recorded in `validation.json`.
## Fix scope
`fix.patch` changes only:
- `src/main/browser/browser-client-host-command-dispatcher.ts`: release each page's already-settled cache during close, after cancellation; discard newly settled records instead of caching them when closed.
- `src/main/browser/browser-client-host-command-result-cache.ts`: accept an optional `retain` flag in `record`, defaulting to the existing caching behavior. When false, existing identity-checked eviction drops the settled record before any cache admission.
Active/cancelling records, pages, native promises, abort behavior, join timing, FIFO, generation/authority checks, and the closed-settlement promise remain owned exactly as before. No row/byte cap changes. The host and executor continue waiting for their existing settlement owners.
## retirePage is separate
`selectCommandPage` rejects retiring and retired generations before `findExistingCommand` (`browser-client-host-command-page.ts:3443`). Thus retired duplicate replay is already unavailable, even though the cache remains until forget/replacement. The control confirms that behavior and verifies explicit forget releases the cache and preserves the stale-generation floor. This fix leaves that existing retirement policy unchanged; freeing results on page retirement is a separate possible follow-up, especially while executor cleanup is still pending. Do not assume a live duplicate replay contract where the actual admission path rejects first.
## Reproduction and source fences
```sh
ORCA_BACKGROUND_LAUNCH=1 ORCA_BROWSER_CACHE_VARIANT=before pnpm exec vitest run --config docs/audits/browser-closed-result-retention/vitest.config.mjs docs/audits/browser-closed-result-retention/scenario.test.mjs
ORCA_BACKGROUND_LAUNCH=1 ORCA_BROWSER_CACHE_VARIANT=fixed pnpm exec vitest run --config docs/audits/browser-closed-result-retention/vitest.config.mjs docs/audits/browser-closed-result-retention/scenario.test.mjs
```
For Electron run the installed binary with `ELECTRON_RUN_AS_NODE=1` and `ORCA_BACKGROUND_LAUNCH=1`, passing node_modules/vitest/vitest.mjs and the same arguments. Reports are separate per runtime/variant; set `ORCA_BROWSER_CACHE_OUTPUT` to another file path to preserve captured reports. `sources.cjs` reverses `fix.patch` in memory and checks exact baseline/fixed hashes plus 21 caller/dependency hashes. The config loads those sources at their real production module IDs without changing checkout files. A synthetic CRLF control checks all 24 source/patch reads against canonical LF hashes. Both variants use the same controlled producer and lifecycle ports.
`source-versions.json` records 23 canonical-LF source/caller hashes. All 23 match named main checkpoint 291b4ddd6f1c1af480169885e0fda7f9c78ff053; 21 match v1.4.198. Both fixed source baselines match both named versions. The proof executes current source/dependencies, not a historical application binary.
## Permanent regression validation
```sh
ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-client-host-command-retention.test.ts src/main/browser/browser-client-host-command-dispatcher.test.ts src/main/browser/browser-client-page-command-executor.test.ts src/main/browser/paired-runtime-browser-client-host-composition.test.ts src/main/browser/paired-runtime-browser-client-host.test.ts
ORCA_BACKGROUND_LAUNCH=1 ORCA_BROWSER_CACHE_VARIANT=before pnpm exec vitest run --config docs/audits/browser-closed-result-retention/vitest.config.mjs src/main/browser/browser-client-host-command-retention.test.ts src/main/browser/browser-client-host-command-dispatcher.test.ts
ORCA_BACKGROUND_LAUNCH=1 pnpm tc:node
```
The baseline regression command intentionally fails the two new lifetime assertions. The source and object counts prove a code mechanism, not incident-specific browser use, native stall duration, aggregate app RSS, or attribution to #19831.
@@ -0,0 +1,138 @@
{
"sources": {
"src/main/browser/browser-client-host-command-dispatcher.ts": {
"before": "f8c610a54f7d16c59043250dde5ef399f70e1a974c9492d09d403138e46c1da3",
"after": "0e4074a4cb7379de58d75dd93e3f74090dea18a944bd4f038730cf19f6e68a98"
},
"src/main/browser/browser-client-host-command-result-cache.ts": {
"before": "9ec3dad78c83f864f74957cad766cbcf41863e92d81d7e7a471702b0f9ab2f34",
"after": "f06bd1f31be9ffcbb750f242bacd0c2f16222cde7d011010acbd46ceb70ea83f"
},
"src/main/browser/browser-client-host-command-state.ts": {
"before": "a5f6b7a3737f86b2953fc43fa0e3ed5faaa141814d5f94fd192da1bb510f355f",
"after": "a5f6b7a3737f86b2953fc43fa0e3ed5faaa141814d5f94fd192da1bb510f355f"
},
"src/main/browser/browser-client-host-command-page.ts": {
"before": "f5ca7f90511ee0290d2a1d0b3c5c39d3c9841ebcf90a5564bce83fbc9aca06bb",
"after": "f5ca7f90511ee0290d2a1d0b3c5c39d3c9841ebcf90a5564bce83fbc9aca06bb"
},
"src/main/browser/browser-client-host-command-join.ts": {
"before": "5bf1a38231100cc416176ef2052bdff7b3fca91b6f6dbefcd222b92be59cbb2f",
"after": "5bf1a38231100cc416176ef2052bdff7b3fca91b6f6dbefcd222b92be59cbb2f"
},
"src/main/browser/browser-client-page-command-executor.ts": {
"before": "d249de82f89365371fa4f3b457e0da552910819555a5a0bced0c95010ccacb01",
"after": "d249de82f89365371fa4f3b457e0da552910819555a5a0bced0c95010ccacb01"
},
"src/main/browser/browser-client-page-command-execution.ts": {
"before": "82b7c87d17b34c267ebfdf3d6ee25058cbd05dbc163ac24c43aefed00f0e3c6e",
"after": "82b7c87d17b34c267ebfdf3d6ee25058cbd05dbc163ac24c43aefed00f0e3c6e"
},
"src/main/browser/browser-client-page-command-executor-test-harness.ts": {
"before": "b43ec764e4d094070fab51381ceb24487999dca97b4a38131be08fab0c5cbe24",
"after": "b43ec764e4d094070fab51381ceb24487999dca97b4a38131be08fab0c5cbe24"
},
"src/main/browser/browser-client-page-automation-runtime.ts": {
"before": "7b1c0c527a87b8c22ea8290a2e1efc7ca659f2b4a5e2bd7c740fb5c22a2a6319",
"after": "7b1c0c527a87b8c22ea8290a2e1efc7ca659f2b4a5e2bd7c740fb5c22a2a6319"
},
"src/main/browser/browser-route-guest-lifecycle.ts": {
"before": "52adbea664bcfc5d774f129d442f4fc36d4a1e1280a073053fc09ae026ca83cb",
"after": "52adbea664bcfc5d774f129d442f4fc36d4a1e1280a073053fc09ae026ca83cb"
},
"src/main/browser/browser-route-webcontents-registry.ts": {
"before": "132c6740539aaeb9b1ceb3c108d2aebfb9e8e5c4ce3ff7cd1191e5b45dff8cad",
"after": "132c6740539aaeb9b1ceb3c108d2aebfb9e8e5c4ce3ff7cd1191e5b45dff8cad"
},
"src/main/browser/paired-runtime-browser-client-host.ts": {
"before": "797c0130e30c5f40551f5d392326fff1ac8752e262653b3f855dfd7df9fa0038",
"after": "797c0130e30c5f40551f5d392326fff1ac8752e262653b3f855dfd7df9fa0038"
},
"src/main/browser/paired-runtime-browser-client-host-composition.ts": {
"before": "f8b55a24035e929664f3c5f61ee8a81ee560250379dcf64b39d35e1b60628b74",
"after": "f8b55a24035e929664f3c5f61ee8a81ee560250379dcf64b39d35e1b60628b74"
},
"src/main/browser/paired-runtime-browser-client-host-teardown.ts": {
"before": "4d9eb53c81ddfa3cb8181735b08c2b90a380bebab1f173ae5d8da9f719671f59",
"after": "4d9eb53c81ddfa3cb8181735b08c2b90a380bebab1f173ae5d8da9f719671f59"
},
"src/main/browser/paired-runtime-browser-client-host-runtime.ts": {
"before": "2d0650d0a5ebe88f1185ab5676762ed93188e7c28a69508137ed7cfa36e3832c",
"after": "2d0650d0a5ebe88f1185ab5676762ed93188e7c28a69508137ed7cfa36e3832c"
},
"src/main/runtime/rpc/methods/browser-core.ts": {
"before": "fdc2afeece6689644fbd607a59e484ef9d2ac6db7404e3378f2db10e16984f6d",
"after": "fdc2afeece6689644fbd607a59e484ef9d2ac6db7404e3378f2db10e16984f6d"
},
"src/main/runtime/runtime-browser-commands-active-screencasts-by-page-id.ts": {
"before": "0af16d6fe883f6af79754e064b77a1b9ab8e80953657434eacc88cc1c523e038",
"after": "0af16d6fe883f6af79754e064b77a1b9ab8e80953657434eacc88cc1c523e038"
},
"src/main/browser/agent-browser-bridge-types.ts": {
"before": "3ea69d15aef862807108835d1cbd536275a105a96bdc86f3be95dfbed55ab45f",
"after": "3ea69d15aef862807108835d1cbd536275a105a96bdc86f3be95dfbed55ab45f"
},
"src/main/browser/agent-browser-bridge-raw-process.ts": {
"before": "d0e9929c4a4571d8752efa5e4063500efa75fefd228eb6cb1ea6092748befb0c",
"after": "d0e9929c4a4571d8752efa5e4063500efa75fefd228eb6cb1ea6092748befb0c"
},
"src/main/browser/agent-browser-bridge-core-commands.ts": {
"before": "d587a7e7c3bd368c9003802607edd20def98e64442cd6d5914779c633feebd03",
"after": "d587a7e7c3bd368c9003802607edd20def98e64442cd6d5914779c633feebd03"
},
"src/main/startup/main-process-ready-runtime.ts": {
"before": "5bb3615e9dc430e0a9ab61cb3d70e57ecbfa4c5ea82d5414fc82d93e81e9819b",
"after": "5bb3615e9dc430e0a9ab61cb3d70e57ecbfa4c5ea82d5414fc82d93e81e9819b"
},
"src/shared/browser-client-host-protocol.ts": {
"before": "712fe3eaed30c2de322466f316c966e526dcb60cf387cc0f6ba48012c70a7956",
"after": "712fe3eaed30c2de322466f316c966e526dcb60cf387cc0f6ba48012c70a7956"
},
"src/shared/browser-client-automation-protocol.ts": {
"before": "f93331c39fd0f8e08518d7a11a461bf5108c1a65be8f213ab2e2eff2ab977e58",
"after": "f93331c39fd0f8e08518d7a11a461bf5108c1a65be8f213ab2e2eff2ab977e58"
}
},
"runtime": {
"node": "24.21.0",
"electron": "43.7.0",
"v8": "15.0.245.31-electron.0"
},
"variant": "before",
"cases": [
{
"kind": "native-navigation-close",
"completedPayloads": 32,
"heldNativePorts": 1,
"joinTimeoutOverrideMs": 15,
"retainedPayloadsAfterClose": 32,
"cachedResultsAfterClose": 34,
"cancelledQueuedInputRetained": true,
"signalAborted": true,
"executorCustodyPreserved": true,
"routeLeasePreserved": true,
"closedDuplicateRejected": true,
"secondCloseSettled": false,
"retainedAfterNativeSettlement": 0,
"executorClosedAfterNativeSettlement": true
},
{
"kind": "late-sibling-settlement",
"oneHandlerStillOwned": true,
"cachedAfterFirstSettlement": 1,
"closedSettlementStillPending": true
},
{
"kind": "open-replay-and-retire-contract",
"openPromiseIdentityPreserved": true,
"retiredDuplicateRejected": true,
"retireCachePolicyUnchanged": true,
"explicitForgetReleasedCache": true
},
{
"kind": "synthetic-crlf-source-control",
"canonicalHashesMatch": true,
"reads": 24
}
]
}
@@ -0,0 +1,138 @@
{
"sources": {
"src/main/browser/browser-client-host-command-dispatcher.ts": {
"before": "f8c610a54f7d16c59043250dde5ef399f70e1a974c9492d09d403138e46c1da3",
"after": "0e4074a4cb7379de58d75dd93e3f74090dea18a944bd4f038730cf19f6e68a98"
},
"src/main/browser/browser-client-host-command-result-cache.ts": {
"before": "9ec3dad78c83f864f74957cad766cbcf41863e92d81d7e7a471702b0f9ab2f34",
"after": "f06bd1f31be9ffcbb750f242bacd0c2f16222cde7d011010acbd46ceb70ea83f"
},
"src/main/browser/browser-client-host-command-state.ts": {
"before": "a5f6b7a3737f86b2953fc43fa0e3ed5faaa141814d5f94fd192da1bb510f355f",
"after": "a5f6b7a3737f86b2953fc43fa0e3ed5faaa141814d5f94fd192da1bb510f355f"
},
"src/main/browser/browser-client-host-command-page.ts": {
"before": "f5ca7f90511ee0290d2a1d0b3c5c39d3c9841ebcf90a5564bce83fbc9aca06bb",
"after": "f5ca7f90511ee0290d2a1d0b3c5c39d3c9841ebcf90a5564bce83fbc9aca06bb"
},
"src/main/browser/browser-client-host-command-join.ts": {
"before": "5bf1a38231100cc416176ef2052bdff7b3fca91b6f6dbefcd222b92be59cbb2f",
"after": "5bf1a38231100cc416176ef2052bdff7b3fca91b6f6dbefcd222b92be59cbb2f"
},
"src/main/browser/browser-client-page-command-executor.ts": {
"before": "d249de82f89365371fa4f3b457e0da552910819555a5a0bced0c95010ccacb01",
"after": "d249de82f89365371fa4f3b457e0da552910819555a5a0bced0c95010ccacb01"
},
"src/main/browser/browser-client-page-command-execution.ts": {
"before": "82b7c87d17b34c267ebfdf3d6ee25058cbd05dbc163ac24c43aefed00f0e3c6e",
"after": "82b7c87d17b34c267ebfdf3d6ee25058cbd05dbc163ac24c43aefed00f0e3c6e"
},
"src/main/browser/browser-client-page-command-executor-test-harness.ts": {
"before": "b43ec764e4d094070fab51381ceb24487999dca97b4a38131be08fab0c5cbe24",
"after": "b43ec764e4d094070fab51381ceb24487999dca97b4a38131be08fab0c5cbe24"
},
"src/main/browser/browser-client-page-automation-runtime.ts": {
"before": "7b1c0c527a87b8c22ea8290a2e1efc7ca659f2b4a5e2bd7c740fb5c22a2a6319",
"after": "7b1c0c527a87b8c22ea8290a2e1efc7ca659f2b4a5e2bd7c740fb5c22a2a6319"
},
"src/main/browser/browser-route-guest-lifecycle.ts": {
"before": "52adbea664bcfc5d774f129d442f4fc36d4a1e1280a073053fc09ae026ca83cb",
"after": "52adbea664bcfc5d774f129d442f4fc36d4a1e1280a073053fc09ae026ca83cb"
},
"src/main/browser/browser-route-webcontents-registry.ts": {
"before": "132c6740539aaeb9b1ceb3c108d2aebfb9e8e5c4ce3ff7cd1191e5b45dff8cad",
"after": "132c6740539aaeb9b1ceb3c108d2aebfb9e8e5c4ce3ff7cd1191e5b45dff8cad"
},
"src/main/browser/paired-runtime-browser-client-host.ts": {
"before": "797c0130e30c5f40551f5d392326fff1ac8752e262653b3f855dfd7df9fa0038",
"after": "797c0130e30c5f40551f5d392326fff1ac8752e262653b3f855dfd7df9fa0038"
},
"src/main/browser/paired-runtime-browser-client-host-composition.ts": {
"before": "f8b55a24035e929664f3c5f61ee8a81ee560250379dcf64b39d35e1b60628b74",
"after": "f8b55a24035e929664f3c5f61ee8a81ee560250379dcf64b39d35e1b60628b74"
},
"src/main/browser/paired-runtime-browser-client-host-teardown.ts": {
"before": "4d9eb53c81ddfa3cb8181735b08c2b90a380bebab1f173ae5d8da9f719671f59",
"after": "4d9eb53c81ddfa3cb8181735b08c2b90a380bebab1f173ae5d8da9f719671f59"
},
"src/main/browser/paired-runtime-browser-client-host-runtime.ts": {
"before": "2d0650d0a5ebe88f1185ab5676762ed93188e7c28a69508137ed7cfa36e3832c",
"after": "2d0650d0a5ebe88f1185ab5676762ed93188e7c28a69508137ed7cfa36e3832c"
},
"src/main/runtime/rpc/methods/browser-core.ts": {
"before": "fdc2afeece6689644fbd607a59e484ef9d2ac6db7404e3378f2db10e16984f6d",
"after": "fdc2afeece6689644fbd607a59e484ef9d2ac6db7404e3378f2db10e16984f6d"
},
"src/main/runtime/runtime-browser-commands-active-screencasts-by-page-id.ts": {
"before": "0af16d6fe883f6af79754e064b77a1b9ab8e80953657434eacc88cc1c523e038",
"after": "0af16d6fe883f6af79754e064b77a1b9ab8e80953657434eacc88cc1c523e038"
},
"src/main/browser/agent-browser-bridge-types.ts": {
"before": "3ea69d15aef862807108835d1cbd536275a105a96bdc86f3be95dfbed55ab45f",
"after": "3ea69d15aef862807108835d1cbd536275a105a96bdc86f3be95dfbed55ab45f"
},
"src/main/browser/agent-browser-bridge-raw-process.ts": {
"before": "d0e9929c4a4571d8752efa5e4063500efa75fefd228eb6cb1ea6092748befb0c",
"after": "d0e9929c4a4571d8752efa5e4063500efa75fefd228eb6cb1ea6092748befb0c"
},
"src/main/browser/agent-browser-bridge-core-commands.ts": {
"before": "d587a7e7c3bd368c9003802607edd20def98e64442cd6d5914779c633feebd03",
"after": "d587a7e7c3bd368c9003802607edd20def98e64442cd6d5914779c633feebd03"
},
"src/main/startup/main-process-ready-runtime.ts": {
"before": "5bb3615e9dc430e0a9ab61cb3d70e57ecbfa4c5ea82d5414fc82d93e81e9819b",
"after": "5bb3615e9dc430e0a9ab61cb3d70e57ecbfa4c5ea82d5414fc82d93e81e9819b"
},
"src/shared/browser-client-host-protocol.ts": {
"before": "712fe3eaed30c2de322466f316c966e526dcb60cf387cc0f6ba48012c70a7956",
"after": "712fe3eaed30c2de322466f316c966e526dcb60cf387cc0f6ba48012c70a7956"
},
"src/shared/browser-client-automation-protocol.ts": {
"before": "f93331c39fd0f8e08518d7a11a461bf5108c1a65be8f213ab2e2eff2ab977e58",
"after": "f93331c39fd0f8e08518d7a11a461bf5108c1a65be8f213ab2e2eff2ab977e58"
}
},
"runtime": {
"node": "26.6.0",
"electron": null,
"v8": "14.6.202.34-node.26"
},
"variant": "before",
"cases": [
{
"kind": "native-navigation-close",
"completedPayloads": 32,
"heldNativePorts": 1,
"joinTimeoutOverrideMs": 15,
"retainedPayloadsAfterClose": 32,
"cachedResultsAfterClose": 34,
"cancelledQueuedInputRetained": true,
"signalAborted": true,
"executorCustodyPreserved": true,
"routeLeasePreserved": true,
"closedDuplicateRejected": true,
"secondCloseSettled": false,
"retainedAfterNativeSettlement": 0,
"executorClosedAfterNativeSettlement": true
},
{
"kind": "late-sibling-settlement",
"oneHandlerStillOwned": true,
"cachedAfterFirstSettlement": 1,
"closedSettlementStillPending": true
},
{
"kind": "open-replay-and-retire-contract",
"openPromiseIdentityPreserved": true,
"retiredDuplicateRejected": true,
"retireCachePolicyUnchanged": true,
"explicitForgetReleasedCache": true
},
{
"kind": "synthetic-crlf-source-control",
"canonicalHashesMatch": true,
"reads": 24
}
]
}
@@ -0,0 +1,16 @@
--- a/src/main/browser/browser-client-host-command-dispatcher.ts
+++ b/src/main/browser/browser-client-host-command-dispatcher.ts
@@ -165,0 +166 @@
+ this.resultCache.releasePage(page)
@@ -300 +301 @@
- this.resultCache.record(page, record)
+ this.resultCache.record(page, record, !this.closed)
--- a/src/main/browser/browser-client-host-command-result-cache.ts
+++ b/src/main/browser/browser-client-host-command-result-cache.ts
@@ -11 +11,5 @@
- record(page: PageState, record: CommandRecord): void {
+ record(page: PageState, record: CommandRecord, retain = true): void {
+ if (!retain) {
+ this.evict(page, record.event.commandSequence, record)
+ return
+ }
@@ -0,0 +1,138 @@
{
"sources": {
"src/main/browser/browser-client-host-command-dispatcher.ts": {
"before": "f8c610a54f7d16c59043250dde5ef399f70e1a974c9492d09d403138e46c1da3",
"after": "0e4074a4cb7379de58d75dd93e3f74090dea18a944bd4f038730cf19f6e68a98"
},
"src/main/browser/browser-client-host-command-result-cache.ts": {
"before": "9ec3dad78c83f864f74957cad766cbcf41863e92d81d7e7a471702b0f9ab2f34",
"after": "f06bd1f31be9ffcbb750f242bacd0c2f16222cde7d011010acbd46ceb70ea83f"
},
"src/main/browser/browser-client-host-command-state.ts": {
"before": "a5f6b7a3737f86b2953fc43fa0e3ed5faaa141814d5f94fd192da1bb510f355f",
"after": "a5f6b7a3737f86b2953fc43fa0e3ed5faaa141814d5f94fd192da1bb510f355f"
},
"src/main/browser/browser-client-host-command-page.ts": {
"before": "f5ca7f90511ee0290d2a1d0b3c5c39d3c9841ebcf90a5564bce83fbc9aca06bb",
"after": "f5ca7f90511ee0290d2a1d0b3c5c39d3c9841ebcf90a5564bce83fbc9aca06bb"
},
"src/main/browser/browser-client-host-command-join.ts": {
"before": "5bf1a38231100cc416176ef2052bdff7b3fca91b6f6dbefcd222b92be59cbb2f",
"after": "5bf1a38231100cc416176ef2052bdff7b3fca91b6f6dbefcd222b92be59cbb2f"
},
"src/main/browser/browser-client-page-command-executor.ts": {
"before": "d249de82f89365371fa4f3b457e0da552910819555a5a0bced0c95010ccacb01",
"after": "d249de82f89365371fa4f3b457e0da552910819555a5a0bced0c95010ccacb01"
},
"src/main/browser/browser-client-page-command-execution.ts": {
"before": "82b7c87d17b34c267ebfdf3d6ee25058cbd05dbc163ac24c43aefed00f0e3c6e",
"after": "82b7c87d17b34c267ebfdf3d6ee25058cbd05dbc163ac24c43aefed00f0e3c6e"
},
"src/main/browser/browser-client-page-command-executor-test-harness.ts": {
"before": "b43ec764e4d094070fab51381ceb24487999dca97b4a38131be08fab0c5cbe24",
"after": "b43ec764e4d094070fab51381ceb24487999dca97b4a38131be08fab0c5cbe24"
},
"src/main/browser/browser-client-page-automation-runtime.ts": {
"before": "7b1c0c527a87b8c22ea8290a2e1efc7ca659f2b4a5e2bd7c740fb5c22a2a6319",
"after": "7b1c0c527a87b8c22ea8290a2e1efc7ca659f2b4a5e2bd7c740fb5c22a2a6319"
},
"src/main/browser/browser-route-guest-lifecycle.ts": {
"before": "52adbea664bcfc5d774f129d442f4fc36d4a1e1280a073053fc09ae026ca83cb",
"after": "52adbea664bcfc5d774f129d442f4fc36d4a1e1280a073053fc09ae026ca83cb"
},
"src/main/browser/browser-route-webcontents-registry.ts": {
"before": "132c6740539aaeb9b1ceb3c108d2aebfb9e8e5c4ce3ff7cd1191e5b45dff8cad",
"after": "132c6740539aaeb9b1ceb3c108d2aebfb9e8e5c4ce3ff7cd1191e5b45dff8cad"
},
"src/main/browser/paired-runtime-browser-client-host.ts": {
"before": "797c0130e30c5f40551f5d392326fff1ac8752e262653b3f855dfd7df9fa0038",
"after": "797c0130e30c5f40551f5d392326fff1ac8752e262653b3f855dfd7df9fa0038"
},
"src/main/browser/paired-runtime-browser-client-host-composition.ts": {
"before": "f8b55a24035e929664f3c5f61ee8a81ee560250379dcf64b39d35e1b60628b74",
"after": "f8b55a24035e929664f3c5f61ee8a81ee560250379dcf64b39d35e1b60628b74"
},
"src/main/browser/paired-runtime-browser-client-host-teardown.ts": {
"before": "4d9eb53c81ddfa3cb8181735b08c2b90a380bebab1f173ae5d8da9f719671f59",
"after": "4d9eb53c81ddfa3cb8181735b08c2b90a380bebab1f173ae5d8da9f719671f59"
},
"src/main/browser/paired-runtime-browser-client-host-runtime.ts": {
"before": "2d0650d0a5ebe88f1185ab5676762ed93188e7c28a69508137ed7cfa36e3832c",
"after": "2d0650d0a5ebe88f1185ab5676762ed93188e7c28a69508137ed7cfa36e3832c"
},
"src/main/runtime/rpc/methods/browser-core.ts": {
"before": "fdc2afeece6689644fbd607a59e484ef9d2ac6db7404e3378f2db10e16984f6d",
"after": "fdc2afeece6689644fbd607a59e484ef9d2ac6db7404e3378f2db10e16984f6d"
},
"src/main/runtime/runtime-browser-commands-active-screencasts-by-page-id.ts": {
"before": "0af16d6fe883f6af79754e064b77a1b9ab8e80953657434eacc88cc1c523e038",
"after": "0af16d6fe883f6af79754e064b77a1b9ab8e80953657434eacc88cc1c523e038"
},
"src/main/browser/agent-browser-bridge-types.ts": {
"before": "3ea69d15aef862807108835d1cbd536275a105a96bdc86f3be95dfbed55ab45f",
"after": "3ea69d15aef862807108835d1cbd536275a105a96bdc86f3be95dfbed55ab45f"
},
"src/main/browser/agent-browser-bridge-raw-process.ts": {
"before": "d0e9929c4a4571d8752efa5e4063500efa75fefd228eb6cb1ea6092748befb0c",
"after": "d0e9929c4a4571d8752efa5e4063500efa75fefd228eb6cb1ea6092748befb0c"
},
"src/main/browser/agent-browser-bridge-core-commands.ts": {
"before": "d587a7e7c3bd368c9003802607edd20def98e64442cd6d5914779c633feebd03",
"after": "d587a7e7c3bd368c9003802607edd20def98e64442cd6d5914779c633feebd03"
},
"src/main/startup/main-process-ready-runtime.ts": {
"before": "5bb3615e9dc430e0a9ab61cb3d70e57ecbfa4c5ea82d5414fc82d93e81e9819b",
"after": "5bb3615e9dc430e0a9ab61cb3d70e57ecbfa4c5ea82d5414fc82d93e81e9819b"
},
"src/shared/browser-client-host-protocol.ts": {
"before": "712fe3eaed30c2de322466f316c966e526dcb60cf387cc0f6ba48012c70a7956",
"after": "712fe3eaed30c2de322466f316c966e526dcb60cf387cc0f6ba48012c70a7956"
},
"src/shared/browser-client-automation-protocol.ts": {
"before": "f93331c39fd0f8e08518d7a11a461bf5108c1a65be8f213ab2e2eff2ab977e58",
"after": "f93331c39fd0f8e08518d7a11a461bf5108c1a65be8f213ab2e2eff2ab977e58"
}
},
"runtime": {
"node": "24.21.0",
"electron": "43.7.0",
"v8": "15.0.245.31-electron.0"
},
"variant": "fixed",
"cases": [
{
"kind": "native-navigation-close",
"completedPayloads": 32,
"heldNativePorts": 1,
"joinTimeoutOverrideMs": 15,
"retainedPayloadsAfterClose": 0,
"cachedResultsAfterClose": 0,
"cancelledQueuedInputRetained": false,
"signalAborted": true,
"executorCustodyPreserved": true,
"routeLeasePreserved": true,
"closedDuplicateRejected": true,
"secondCloseSettled": false,
"retainedAfterNativeSettlement": 0,
"executorClosedAfterNativeSettlement": true
},
{
"kind": "late-sibling-settlement",
"oneHandlerStillOwned": true,
"cachedAfterFirstSettlement": 0,
"closedSettlementStillPending": true
},
{
"kind": "open-replay-and-retire-contract",
"openPromiseIdentityPreserved": true,
"retiredDuplicateRejected": true,
"retireCachePolicyUnchanged": true,
"explicitForgetReleasedCache": true
},
{
"kind": "synthetic-crlf-source-control",
"canonicalHashesMatch": true,
"reads": 24
}
]
}
@@ -0,0 +1,138 @@
{
"sources": {
"src/main/browser/browser-client-host-command-dispatcher.ts": {
"before": "f8c610a54f7d16c59043250dde5ef399f70e1a974c9492d09d403138e46c1da3",
"after": "0e4074a4cb7379de58d75dd93e3f74090dea18a944bd4f038730cf19f6e68a98"
},
"src/main/browser/browser-client-host-command-result-cache.ts": {
"before": "9ec3dad78c83f864f74957cad766cbcf41863e92d81d7e7a471702b0f9ab2f34",
"after": "f06bd1f31be9ffcbb750f242bacd0c2f16222cde7d011010acbd46ceb70ea83f"
},
"src/main/browser/browser-client-host-command-state.ts": {
"before": "a5f6b7a3737f86b2953fc43fa0e3ed5faaa141814d5f94fd192da1bb510f355f",
"after": "a5f6b7a3737f86b2953fc43fa0e3ed5faaa141814d5f94fd192da1bb510f355f"
},
"src/main/browser/browser-client-host-command-page.ts": {
"before": "f5ca7f90511ee0290d2a1d0b3c5c39d3c9841ebcf90a5564bce83fbc9aca06bb",
"after": "f5ca7f90511ee0290d2a1d0b3c5c39d3c9841ebcf90a5564bce83fbc9aca06bb"
},
"src/main/browser/browser-client-host-command-join.ts": {
"before": "5bf1a38231100cc416176ef2052bdff7b3fca91b6f6dbefcd222b92be59cbb2f",
"after": "5bf1a38231100cc416176ef2052bdff7b3fca91b6f6dbefcd222b92be59cbb2f"
},
"src/main/browser/browser-client-page-command-executor.ts": {
"before": "d249de82f89365371fa4f3b457e0da552910819555a5a0bced0c95010ccacb01",
"after": "d249de82f89365371fa4f3b457e0da552910819555a5a0bced0c95010ccacb01"
},
"src/main/browser/browser-client-page-command-execution.ts": {
"before": "82b7c87d17b34c267ebfdf3d6ee25058cbd05dbc163ac24c43aefed00f0e3c6e",
"after": "82b7c87d17b34c267ebfdf3d6ee25058cbd05dbc163ac24c43aefed00f0e3c6e"
},
"src/main/browser/browser-client-page-command-executor-test-harness.ts": {
"before": "b43ec764e4d094070fab51381ceb24487999dca97b4a38131be08fab0c5cbe24",
"after": "b43ec764e4d094070fab51381ceb24487999dca97b4a38131be08fab0c5cbe24"
},
"src/main/browser/browser-client-page-automation-runtime.ts": {
"before": "7b1c0c527a87b8c22ea8290a2e1efc7ca659f2b4a5e2bd7c740fb5c22a2a6319",
"after": "7b1c0c527a87b8c22ea8290a2e1efc7ca659f2b4a5e2bd7c740fb5c22a2a6319"
},
"src/main/browser/browser-route-guest-lifecycle.ts": {
"before": "52adbea664bcfc5d774f129d442f4fc36d4a1e1280a073053fc09ae026ca83cb",
"after": "52adbea664bcfc5d774f129d442f4fc36d4a1e1280a073053fc09ae026ca83cb"
},
"src/main/browser/browser-route-webcontents-registry.ts": {
"before": "132c6740539aaeb9b1ceb3c108d2aebfb9e8e5c4ce3ff7cd1191e5b45dff8cad",
"after": "132c6740539aaeb9b1ceb3c108d2aebfb9e8e5c4ce3ff7cd1191e5b45dff8cad"
},
"src/main/browser/paired-runtime-browser-client-host.ts": {
"before": "797c0130e30c5f40551f5d392326fff1ac8752e262653b3f855dfd7df9fa0038",
"after": "797c0130e30c5f40551f5d392326fff1ac8752e262653b3f855dfd7df9fa0038"
},
"src/main/browser/paired-runtime-browser-client-host-composition.ts": {
"before": "f8b55a24035e929664f3c5f61ee8a81ee560250379dcf64b39d35e1b60628b74",
"after": "f8b55a24035e929664f3c5f61ee8a81ee560250379dcf64b39d35e1b60628b74"
},
"src/main/browser/paired-runtime-browser-client-host-teardown.ts": {
"before": "4d9eb53c81ddfa3cb8181735b08c2b90a380bebab1f173ae5d8da9f719671f59",
"after": "4d9eb53c81ddfa3cb8181735b08c2b90a380bebab1f173ae5d8da9f719671f59"
},
"src/main/browser/paired-runtime-browser-client-host-runtime.ts": {
"before": "2d0650d0a5ebe88f1185ab5676762ed93188e7c28a69508137ed7cfa36e3832c",
"after": "2d0650d0a5ebe88f1185ab5676762ed93188e7c28a69508137ed7cfa36e3832c"
},
"src/main/runtime/rpc/methods/browser-core.ts": {
"before": "fdc2afeece6689644fbd607a59e484ef9d2ac6db7404e3378f2db10e16984f6d",
"after": "fdc2afeece6689644fbd607a59e484ef9d2ac6db7404e3378f2db10e16984f6d"
},
"src/main/runtime/runtime-browser-commands-active-screencasts-by-page-id.ts": {
"before": "0af16d6fe883f6af79754e064b77a1b9ab8e80953657434eacc88cc1c523e038",
"after": "0af16d6fe883f6af79754e064b77a1b9ab8e80953657434eacc88cc1c523e038"
},
"src/main/browser/agent-browser-bridge-types.ts": {
"before": "3ea69d15aef862807108835d1cbd536275a105a96bdc86f3be95dfbed55ab45f",
"after": "3ea69d15aef862807108835d1cbd536275a105a96bdc86f3be95dfbed55ab45f"
},
"src/main/browser/agent-browser-bridge-raw-process.ts": {
"before": "d0e9929c4a4571d8752efa5e4063500efa75fefd228eb6cb1ea6092748befb0c",
"after": "d0e9929c4a4571d8752efa5e4063500efa75fefd228eb6cb1ea6092748befb0c"
},
"src/main/browser/agent-browser-bridge-core-commands.ts": {
"before": "d587a7e7c3bd368c9003802607edd20def98e64442cd6d5914779c633feebd03",
"after": "d587a7e7c3bd368c9003802607edd20def98e64442cd6d5914779c633feebd03"
},
"src/main/startup/main-process-ready-runtime.ts": {
"before": "5bb3615e9dc430e0a9ab61cb3d70e57ecbfa4c5ea82d5414fc82d93e81e9819b",
"after": "5bb3615e9dc430e0a9ab61cb3d70e57ecbfa4c5ea82d5414fc82d93e81e9819b"
},
"src/shared/browser-client-host-protocol.ts": {
"before": "712fe3eaed30c2de322466f316c966e526dcb60cf387cc0f6ba48012c70a7956",
"after": "712fe3eaed30c2de322466f316c966e526dcb60cf387cc0f6ba48012c70a7956"
},
"src/shared/browser-client-automation-protocol.ts": {
"before": "f93331c39fd0f8e08518d7a11a461bf5108c1a65be8f213ab2e2eff2ab977e58",
"after": "f93331c39fd0f8e08518d7a11a461bf5108c1a65be8f213ab2e2eff2ab977e58"
}
},
"runtime": {
"node": "26.6.0",
"electron": null,
"v8": "14.6.202.34-node.26"
},
"variant": "fixed",
"cases": [
{
"kind": "native-navigation-close",
"completedPayloads": 32,
"heldNativePorts": 1,
"joinTimeoutOverrideMs": 15,
"retainedPayloadsAfterClose": 0,
"cachedResultsAfterClose": 0,
"cancelledQueuedInputRetained": false,
"signalAborted": true,
"executorCustodyPreserved": true,
"routeLeasePreserved": true,
"closedDuplicateRejected": true,
"secondCloseSettled": false,
"retainedAfterNativeSettlement": 0,
"executorClosedAfterNativeSettlement": true
},
{
"kind": "late-sibling-settlement",
"oneHandlerStillOwned": true,
"cachedAfterFirstSettlement": 0,
"closedSettlementStillPending": true
},
{
"kind": "open-replay-and-retire-contract",
"openPromiseIdentityPreserved": true,
"retiredDuplicateRejected": true,
"retireCachePolicyUnchanged": true,
"explicitForgetReleasedCache": true
},
{
"kind": "synthetic-crlf-source-control",
"canonicalHashesMatch": true,
"reads": 24
}
]
}
@@ -0,0 +1,330 @@
import { afterEach, expect, it, vi } from 'vitest'
import { writeFileSync, readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
const { loadSources } = createRequire(import.meta.url)('./sources.cjs')
const sourceInfo = loadSources()
import { BrowserClientHostCommandDispatcher } from '../../../src/main/browser/browser-client-host-command-dispatcher'
import {
createHarness,
createCommand
} from '../../../src/main/browser/browser-client-page-command-executor-test-harness'
import { BrowserClientPageAutomationRuntime } from '../../../src/main/browser/browser-client-page-automation-runtime'
import { navigateBrowserRouteGuest } from '../../../src/main/browser/browser-route-guest-lifecycle'
import { closeBrowserClientHostComposition } from '../../../src/main/browser/paired-runtime-browser-client-host-teardown'
import { BROWSER_CORE_METHODS } from '../../../src/main/runtime/rpc/methods/browser-core'
const fixed = process.env.ORCA_BROWSER_CACHE_VARIANT !== 'before'
const variant = fixed ? 'fixed' : 'before',
reports = []
const authority = {
authorityRuntimeId: 'runtime-a',
authorityEpoch: 'epoch-a',
browserHostClientId: 'client-a',
browserHostGeneration: 3,
pageCommandProtocolVersion: 1
}
function gate() {
let resolve, reject
const promise = new Promise((yes, no) => {
resolve = yes
reject = no
})
return { promise, resolve, reject }
}
async function collect() {
for (let turn = 0; turn < 8; turn++) {
await new Promise(setImmediate)
global.gc()
}
}
function alive(refs) {
return refs.filter((ref) => ref.deref()).length
}
function command(sequence, body, page = 'page-a', generation = 7) {
return createCommand('createPage', {
browserPageId: page,
pageHostGeneration: generation,
commandSequence: sequence,
commandId: `${page}-${generation}-${sequence}`,
command: body
})
}
function cached(dispatcher) {
return [...dispatcher.pages.values()].reduce((sum, page) => sum + page.settledSequences.length, 0)
}
function queueUnstartedPayload(dispatcher) {
const payload = { queued: 'small-command-input' }
return {
ref: new WeakRef(payload),
promise: dispatcher.dispatch(
command(35, { type: 'automation', method: 'browser.snapshot', params: { payload } })
)
}
}
async function appendSnapshot(dispatcher, index) {
const result = await dispatcher.dispatch(
command(index + 2, { type: 'automation', method: 'browser.snapshot', params: {} })
)
expect(result.status).toBe('completed')
return new WeakRef(result.value)
}
afterEach(() => {
vi.restoreAllMocks()
writeFileSync(
process.env.ORCA_BROWSER_CACHE_OUTPUT ??
`docs/audits/browser-closed-result-retention/${variant}-${process.versions.electron ? 'electron' : 'node'}-results.json`,
`${JSON.stringify(
{
sources: sourceInfo.hashes,
runtime: {
node: process.versions.node,
electron: process.versions.electron ?? null,
v8: process.versions.v8
},
variant,
cases: reports
},
null,
2
)}\n`
)
})
it('keeps completed actual automation results behind one pending native navigation after close timeout', async () => {
const h = createHarness(),
native = gate(),
entered = gate()
let signal,
ordinal = 0,
executorClosed = false,
deferredClose
const snapshot = BROWSER_CORE_METHODS.find((method) => method.name === 'browser.snapshot')
const automation = new BrowserClientPageAutomationRuntime({
browserManager: {
getGuestWebContentsId: () => 41,
registerGuest: () => true,
unregisterGuest() {}
},
getAgentBrowserBridge: () => null,
executeRpc: (_method, params, contextSignal) =>
snapshot.handler(params, {
signal: contextSignal,
runtime: {
browserSnapshot: async () => ({ title: `ordinary-result-${ordinal++}`, items: [1, 2, 3] })
}
})
})
h.dependencies.executeAutomation = (input, contextSignal) =>
automation.execute(input, contextSignal)
h.dependencies.retireAutomation = (input) => automation.retire(input)
h.dependencies.routeWebContents.navigateGuest = (claim, url) =>
navigateBrowserRouteGuest(
claim.registration,
url,
{
registration: claim.registration,
navigationGranted: true,
guest: {
loadURL: () => {
entered.resolve()
return native.promise
}
}
},
() => true
)
const dispatcher = new BrowserClientHostCommandDispatcher({
authority,
handler: (event, contextSignal) => {
if (event.command.type === 'navigate') {
signal = contextSignal
}
return h.executor.handle(event, contextSignal)
},
joinTimeoutMs: 15
})
await dispatcher.dispatch(createCommand('createPage'))
const refs = []
for (let index = 0; index < 32; index++) {
refs.push(await appendSnapshot(dispatcher, index))
}
await collect()
expect(alive(refs)).toBe(32)
const pending = dispatcher.dispatch(
command(34, { type: 'navigate', url: 'https://example.invalid/held' })
)
await entered.promise
const queued = queueUnstartedPayload(dispatcher)
h.executor.fenceNavigation()
const closing = closeBrowserClientHostComposition({
host: { close: () => dispatcher.close(), whenHandlersSettled: () => dispatcher.whenClosed() },
executor: {
async close() {
executorClosed = true
await h.executor.close()
}
},
routeSets: { async close() {} },
error: new Error('controlled disconnect'),
deferExecutorClose: (close) => {
deferredClose = close
},
reportCleanupError: (error) => {
throw error
}
})
try {
expect(await closing).toBe(false)
expect(await pending).toMatchObject({
status: 'failed',
errorCode: 'browser_host_command_cancelled'
})
expect(await queued.promise).toMatchObject({
status: 'failed',
errorCode: 'browser_host_command_cancelled'
})
expect(signal.aborted).toBe(true)
expect(executorClosed).toBe(false)
expect(h.executor.hasPage('page-a', 7)).toBe(true)
expect(h.route.release).not.toHaveBeenCalled()
expect(h.routeSession.release).not.toHaveBeenCalled()
expect(() => dispatcher.dispatch(createCommand('createPage'))).toThrow('dispatcher_closed')
expect(await dispatcher.close()).toBe(false)
let settled = false
void dispatcher.whenClosed().then(() => {
settled = true
})
await collect()
expect(settled).toBe(false)
const retained = alive(refs),
cachedResults = cached(dispatcher)
expect(retained).toBe(fixed ? 0 : 32)
expect(cachedResults).toBe(fixed ? 0 : 34)
expect(Boolean(queued.ref.deref())).toBe(!fixed)
expect(dispatcher.runningHandlers).toBe(1)
reports.push({
kind: 'native-navigation-close',
completedPayloads: 32,
heldNativePorts: 1,
joinTimeoutOverrideMs: 15,
retainedPayloadsAfterClose: retained,
cachedResultsAfterClose: cachedResults,
cancelledQueuedInputRetained: Boolean(queued.ref.deref()),
signalAborted: true,
executorCustodyPreserved: true,
routeLeasePreserved: true,
closedDuplicateRejected: true,
secondCloseSettled: false
})
} finally {
native.resolve()
await dispatcher.whenClosed()
await deferredClose
await h.executor.close()
}
await collect()
expect(alive(refs)).toBe(0)
expect(executorClosed).toBe(true)
expect(h.route.release).toHaveBeenCalledOnce()
expect(h.routeSession.release).toHaveBeenCalledOnce()
reports.at(-1).retainedAfterNativeSettlement = alive(refs)
reports.at(-1).executorClosedAfterNativeSettlement = true
})
it('does not retain late completed cancellation records while a sibling native handler remains owned', async () => {
const first = gate(),
second = gate()
const dispatcher = new BrowserClientHostCommandDispatcher({
authority,
joinTimeoutMs: 15,
handler: (event) => (event.browserPageId === 'page-a' ? first.promise : second.promise)
})
const firstResult = dispatcher.dispatch(
command(
1,
{ type: 'createPage', browserProfileId: 'profile-a', executionHostKey: 'execution-host-a' },
'page-a'
)
)
const secondResult = dispatcher.dispatch(
command(
1,
{ type: 'createPage', browserProfileId: 'profile-a', executionHostKey: 'execution-host-a' },
'page-b'
)
)
expect(await dispatcher.close()).toBe(false)
expect(await firstResult).toMatchObject({ errorCode: 'browser_host_command_cancelled' })
expect(await secondResult).toMatchObject({ errorCode: 'browser_host_command_cancelled' })
first.resolve({ status: 'completed', value: { late: 'ignored' } })
await new Promise(setImmediate)
expect(dispatcher.runningHandlers).toBe(1)
expect(cached(dispatcher)).toBe(fixed ? 0 : 1)
expect(dispatcher.pages.get('page-a').records.size).toBe(fixed ? 0 : 1)
let settled = false
void dispatcher.whenClosed().then(() => {
settled = true
})
await new Promise(setImmediate)
expect(settled).toBe(false)
reports.push({
kind: 'late-sibling-settlement',
oneHandlerStillOwned: true,
cachedAfterFirstSettlement: cached(dispatcher),
closedSettlementStillPending: true
})
second.reject(new Error('controlled native failure'))
await dispatcher.whenClosed()
expect(dispatcher.runningHandlers).toBe(0)
expect(dispatcher.pages.size).toBe(0)
})
it('preserves open replay and generation fencing independently of closed cache release', async () => {
let calls = 0
const dispatcher = new BrowserClientHostCommandDispatcher({
authority,
handler: () => {
calls++
return { status: 'completed', value: { ordinary: true } }
}
})
const event = command(1, {
type: 'createPage',
browserProfileId: 'profile-a',
executionHostKey: 'execution-host-a'
})
const original = dispatcher.dispatch(event),
duplicate = dispatcher.dispatch(event)
expect(duplicate).toBe(original)
await original
expect(dispatcher.dispatch(event)).toBe(original)
expect(calls).toBe(1)
expect(await dispatcher.retirePage('page-a', 7)).toBe(true)
expect(() => dispatcher.dispatch(event)).toThrow('generation_stale')
expect(cached(dispatcher)).toBe(1)
expect(dispatcher.forgetPage('page-a', 7)).toBe(true)
expect(cached(dispatcher)).toBe(0)
expect(() => dispatcher.dispatch(event)).toThrow('generation_stale')
expect(await dispatcher.close()).toBe(true)
reports.push({
kind: 'open-replay-and-retire-contract',
openPromiseIdentityPreserved: true,
retiredDuplicateRejected: true,
retireCachePolicyUnchanged: true,
explicitForgetReleasedCache: true
})
})
it('loads identical canonical hashes from synthetic CRLF source and patch reads', () => {
let reads = 0
const crlf = loadSources({
readText: (filename) => {
reads++
return readFileSync(filename, 'utf8').replace(/\r?\n/g, '\r\n')
}
})
expect(crlf.hashes).toEqual(sourceInfo.hashes)
expect([...crlf.before.entries()]).toEqual([...sourceInfo.before.entries()])
expect([...crlf.after.entries()]).toEqual([...sourceInfo.after.entries()])
reports.push({ kind: 'synthetic-crlf-source-control', canonicalHashesMatch: true, reads })
})
@@ -0,0 +1,405 @@
{
"canonicalLF": true,
"sources": [
{
"path": "src/main/browser/browser-client-host-command-dispatcher.ts",
"workingSha256": "f8c610a54f7d16c59043250dde5ef399f70e1a974c9492d09d403138e46c1da3",
"lineCount": 315,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "f8c610a54f7d16c59043250dde5ef399f70e1a974c9492d09d403138e46c1da3",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "f8c610a54f7d16c59043250dde5ef399f70e1a974c9492d09d403138e46c1da3",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/browser-client-host-command-result-cache.ts",
"workingSha256": "9ec3dad78c83f864f74957cad766cbcf41863e92d81d7e7a471702b0f9ab2f34",
"lineCount": 51,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "9ec3dad78c83f864f74957cad766cbcf41863e92d81d7e7a471702b0f9ab2f34",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "9ec3dad78c83f864f74957cad766cbcf41863e92d81d7e7a471702b0f9ab2f34",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/browser-client-host-command-state.ts",
"workingSha256": "a5f6b7a3737f86b2953fc43fa0e3ed5faaa141814d5f94fd192da1bb510f355f",
"lineCount": 171,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "a5f6b7a3737f86b2953fc43fa0e3ed5faaa141814d5f94fd192da1bb510f355f",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "a5f6b7a3737f86b2953fc43fa0e3ed5faaa141814d5f94fd192da1bb510f355f",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/browser-client-host-command-page.ts",
"workingSha256": "f5ca7f90511ee0290d2a1d0b3c5c39d3c9841ebcf90a5564bce83fbc9aca06bb",
"lineCount": 220,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "f5ca7f90511ee0290d2a1d0b3c5c39d3c9841ebcf90a5564bce83fbc9aca06bb",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "f5ca7f90511ee0290d2a1d0b3c5c39d3c9841ebcf90a5564bce83fbc9aca06bb",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/browser-client-host-command-join.ts",
"workingSha256": "5bf1a38231100cc416176ef2052bdff7b3fca91b6f6dbefcd222b92be59cbb2f",
"lineCount": 21,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "5bf1a38231100cc416176ef2052bdff7b3fca91b6f6dbefcd222b92be59cbb2f",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "5bf1a38231100cc416176ef2052bdff7b3fca91b6f6dbefcd222b92be59cbb2f",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/browser-client-page-command-executor.ts",
"workingSha256": "d249de82f89365371fa4f3b457e0da552910819555a5a0bced0c95010ccacb01",
"lineCount": 319,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "d249de82f89365371fa4f3b457e0da552910819555a5a0bced0c95010ccacb01",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "d249de82f89365371fa4f3b457e0da552910819555a5a0bced0c95010ccacb01",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/browser-client-page-command-execution.ts",
"workingSha256": "82b7c87d17b34c267ebfdf3d6ee25058cbd05dbc163ac24c43aefed00f0e3c6e",
"lineCount": 114,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "82b7c87d17b34c267ebfdf3d6ee25058cbd05dbc163ac24c43aefed00f0e3c6e",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "82b7c87d17b34c267ebfdf3d6ee25058cbd05dbc163ac24c43aefed00f0e3c6e",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/browser-client-page-command-executor-test-harness.ts",
"workingSha256": "b43ec764e4d094070fab51381ceb24487999dca97b4a38131be08fab0c5cbe24",
"lineCount": 145,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "b43ec764e4d094070fab51381ceb24487999dca97b4a38131be08fab0c5cbe24",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "b43ec764e4d094070fab51381ceb24487999dca97b4a38131be08fab0c5cbe24",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/browser-client-page-automation-runtime.ts",
"workingSha256": "7b1c0c527a87b8c22ea8290a2e1efc7ca659f2b4a5e2bd7c740fb5c22a2a6319",
"lineCount": 141,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "7b1c0c527a87b8c22ea8290a2e1efc7ca659f2b4a5e2bd7c740fb5c22a2a6319",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "7b1c0c527a87b8c22ea8290a2e1efc7ca659f2b4a5e2bd7c740fb5c22a2a6319",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/browser-route-guest-lifecycle.ts",
"workingSha256": "52adbea664bcfc5d774f129d442f4fc36d4a1e1280a073053fc09ae026ca83cb",
"lineCount": 172,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "52adbea664bcfc5d774f129d442f4fc36d4a1e1280a073053fc09ae026ca83cb",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "52adbea664bcfc5d774f129d442f4fc36d4a1e1280a073053fc09ae026ca83cb",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/browser-route-webcontents-registry.ts",
"workingSha256": "132c6740539aaeb9b1ceb3c108d2aebfb9e8e5c4ce3ff7cd1191e5b45dff8cad",
"lineCount": 325,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "132c6740539aaeb9b1ceb3c108d2aebfb9e8e5c4ce3ff7cd1191e5b45dff8cad",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "132c6740539aaeb9b1ceb3c108d2aebfb9e8e5c4ce3ff7cd1191e5b45dff8cad",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/paired-runtime-browser-client-host.ts",
"workingSha256": "797c0130e30c5f40551f5d392326fff1ac8752e262653b3f855dfd7df9fa0038",
"lineCount": 193,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "797c0130e30c5f40551f5d392326fff1ac8752e262653b3f855dfd7df9fa0038",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "797c0130e30c5f40551f5d392326fff1ac8752e262653b3f855dfd7df9fa0038",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/paired-runtime-browser-client-host-composition.ts",
"workingSha256": "f8b55a24035e929664f3c5f61ee8a81ee560250379dcf64b39d35e1b60628b74",
"lineCount": 323,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "f8b55a24035e929664f3c5f61ee8a81ee560250379dcf64b39d35e1b60628b74",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "f8b55a24035e929664f3c5f61ee8a81ee560250379dcf64b39d35e1b60628b74",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/paired-runtime-browser-client-host-teardown.ts",
"workingSha256": "4d9eb53c81ddfa3cb8181735b08c2b90a380bebab1f173ae5d8da9f719671f59",
"lineCount": 68,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "4d9eb53c81ddfa3cb8181735b08c2b90a380bebab1f173ae5d8da9f719671f59",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "4d9eb53c81ddfa3cb8181735b08c2b90a380bebab1f173ae5d8da9f719671f59",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/paired-runtime-browser-client-host-runtime.ts",
"workingSha256": "2d0650d0a5ebe88f1185ab5676762ed93188e7c28a69508137ed7cfa36e3832c",
"lineCount": 327,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "2d0650d0a5ebe88f1185ab5676762ed93188e7c28a69508137ed7cfa36e3832c",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "2d0650d0a5ebe88f1185ab5676762ed93188e7c28a69508137ed7cfa36e3832c",
"matchesWorking": true
}
}
},
{
"path": "src/main/runtime/rpc/methods/browser-core.ts",
"workingSha256": "fdc2afeece6689644fbd607a59e484ef9d2ac6db7404e3378f2db10e16984f6d",
"lineCount": 293,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "fdc2afeece6689644fbd607a59e484ef9d2ac6db7404e3378f2db10e16984f6d",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "c847be873f4ce19b04796be962985a0234f159da2b38881fb8f6db1a1cbf720b",
"matchesWorking": false
}
}
},
{
"path": "src/main/runtime/runtime-browser-commands-active-screencasts-by-page-id.ts",
"workingSha256": "0af16d6fe883f6af79754e064b77a1b9ab8e80953657434eacc88cc1c523e038",
"lineCount": 209,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "0af16d6fe883f6af79754e064b77a1b9ab8e80953657434eacc88cc1c523e038",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "0af16d6fe883f6af79754e064b77a1b9ab8e80953657434eacc88cc1c523e038",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/agent-browser-bridge-types.ts",
"workingSha256": "3ea69d15aef862807108835d1cbd536275a105a96bdc86f3be95dfbed55ab45f",
"lineCount": 65,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "3ea69d15aef862807108835d1cbd536275a105a96bdc86f3be95dfbed55ab45f",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "3ea69d15aef862807108835d1cbd536275a105a96bdc86f3be95dfbed55ab45f",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/agent-browser-bridge-raw-process.ts",
"workingSha256": "d0e9929c4a4571d8752efa5e4063500efa75fefd228eb6cb1ea6092748befb0c",
"lineCount": 108,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "d0e9929c4a4571d8752efa5e4063500efa75fefd228eb6cb1ea6092748befb0c",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "d0e9929c4a4571d8752efa5e4063500efa75fefd228eb6cb1ea6092748befb0c",
"matchesWorking": true
}
}
},
{
"path": "src/main/browser/agent-browser-bridge-core-commands.ts",
"workingSha256": "d587a7e7c3bd368c9003802607edd20def98e64442cd6d5914779c633feebd03",
"lineCount": 169,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "d587a7e7c3bd368c9003802607edd20def98e64442cd6d5914779c633feebd03",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "06d6a61e431680ebd89e9a29b18e2198da3d84df6398b234fa8a255a6fcedf8a",
"matchesWorking": false
}
}
},
{
"path": "src/main/startup/main-process-ready-runtime.ts",
"workingSha256": "5bb3615e9dc430e0a9ab61cb3d70e57ecbfa4c5ea82d5414fc82d93e81e9819b",
"lineCount": 156,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "5bb3615e9dc430e0a9ab61cb3d70e57ecbfa4c5ea82d5414fc82d93e81e9819b",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "5bb3615e9dc430e0a9ab61cb3d70e57ecbfa4c5ea82d5414fc82d93e81e9819b",
"matchesWorking": true
}
}
},
{
"path": "src/shared/browser-client-host-protocol.ts",
"workingSha256": "712fe3eaed30c2de322466f316c966e526dcb60cf387cc0f6ba48012c70a7956",
"lineCount": 343,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "712fe3eaed30c2de322466f316c966e526dcb60cf387cc0f6ba48012c70a7956",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "712fe3eaed30c2de322466f316c966e526dcb60cf387cc0f6ba48012c70a7956",
"matchesWorking": true
}
}
},
{
"path": "src/shared/browser-client-automation-protocol.ts",
"workingSha256": "f93331c39fd0f8e08518d7a11a461bf5108c1a65be8f213ab2e2eff2ab977e58",
"lineCount": 129,
"namedRefs": {
"mainCheckpoint": {
"ref": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"sha256": "f93331c39fd0f8e08518d7a11a461bf5108c1a65be8f213ab2e2eff2ab977e58",
"matchesWorking": true
},
"v1.4.198": {
"ref": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"sha256": "f93331c39fd0f8e08518d7a11a461bf5108c1a65be8f213ab2e2eff2ab977e58",
"matchesWorking": true
}
}
}
],
"baselineHashes": {
"src/main/browser/browser-client-host-command-dispatcher.ts": "f8c610a54f7d16c59043250dde5ef399f70e1a974c9492d09d403138e46c1da3",
"src/main/browser/browser-client-host-command-result-cache.ts": "9ec3dad78c83f864f74957cad766cbcf41863e92d81d7e7a471702b0f9ab2f34"
},
"fixedHashes": {
"src/main/browser/browser-client-host-command-dispatcher.ts": "0e4074a4cb7379de58d75dd93e3f74090dea18a944bd4f038730cf19f6e68a98",
"src/main/browser/browser-client-host-command-result-cache.ts": "f06bd1f31be9ffcbb750f242bacd0c2f16222cde7d011010acbd46ceb70ea83f"
},
"auditedHead": "6ab6802df90553d2a26e9a4c7a519ae11d4b5e09"
}
@@ -0,0 +1,42 @@
const assert = require('node:assert/strict')
const { readFileSync } = require('node:fs')
const { createHash } = require('node:crypto')
const { resolve } = require('node:path')
const { applyPatch, parsePatch, reversePatch } = require('diff')
const canonicalLf = (text) => text.replace(/\r\n/g, '\n')
const sha256 = (text) => createHash('sha256').update(text).digest('hex')
function loadSources({ readText = (filename) => readFileSync(filename, 'utf8') } = {}) {
const root = resolve(__dirname, '../../..')
const expected = JSON.parse(readFileSync(resolve(__dirname, 'source-versions.json'), 'utf8'))
const patches = parsePatch(canonicalLf(readText(resolve(__dirname, 'fix.patch'))))
const before = new Map()
const after = new Map()
const hashes = {}
assert.equal(patches.length, 2)
for (const patch of patches) {
const path = patch.newFileName.replace(/^b\//, '')
assert.ok(Object.hasOwn(expected.baselineHashes, path), `Unexpected patch path: ${path}`)
const absolute = resolve(root, path)
const current = canonicalLf(readText(absolute))
const baseline = applyPatch(current, reversePatch(patch))
assert.notEqual(baseline, false, `Patch no longer reverses: ${path}`)
assert.equal(sha256(current), expected.fixedHashes[path], `Fixed source drift: ${path}`)
assert.equal(sha256(baseline), expected.baselineHashes[path], `Baseline source drift: ${path}`)
before.set(absolute, baseline)
after.set(absolute, current)
hashes[path] = { before: sha256(baseline), after: sha256(current) }
}
for (const source of expected.sources) {
if (Object.hasOwn(hashes, source.path)) {
continue
}
const text = canonicalLf(readText(resolve(root, source.path)))
assert.equal(sha256(text), source.workingSha256, `Caller source drift: ${source.path}`)
hashes[source.path] = { before: sha256(text), after: sha256(text) }
}
return { root, before, after, hashes }
}
module.exports = { loadSources }
@@ -0,0 +1,228 @@
{
"tests": {
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-client-host-command-retention.test.ts src/main/browser/browser-client-host-command-dispatcher.test.ts src/main/browser/browser-client-page-command-executor.test.ts src/main/browser/paired-runtime-browser-client-host-composition.test.ts src/main/browser/paired-runtime-browser-client-host.test.ts",
"passed": 77,
"files": 5,
"newRegressionCases": 2,
"exitCode": 0
},
"baselineOverlay": {
"command": "ORCA_BACKGROUND_LAUNCH=1 ORCA_BROWSER_CACHE_VARIANT=before pnpm exec vitest run --config docs/audits/browser-closed-result-retention/vitest.config.mjs src/main/browser/browser-client-host-command-retention.test.ts src/main/browser/browser-client-host-command-dispatcher.test.ts",
"passed": 16,
"expectedFailed": 2,
"failures": [
"32 completed result objects remain reachable while native navigation stays pending.",
"The first late closed input remains reachable while a sibling handler stays pending."
],
"exitCode": 1
},
"typecheck": {
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm tc:node",
"exitCode": 0
},
"publicationQuality": {
"scans": [
{
"label": "code quality",
"command": [
"pnpm",
"exec",
"oxlint",
"--no-ignore",
"--deny-warnings",
"--report-unused-disable-directives-severity",
"warn",
"src/main/browser/browser-client-host-command-dispatcher.ts",
"src/main/browser/browser-client-host-command-result-cache.ts",
"src/main/browser/browser-client-host-command-retention.test.ts",
"docs/audits/browser-closed-result-retention/sources.cjs",
"docs/audits/browser-closed-result-retention/vitest.config.mjs",
"docs/audits/browser-closed-result-retention/scenario.test.mjs"
],
"exitCode": 0
},
{
"label": "casting code quality",
"command": [
"pnpm",
"exec",
"oxlint",
"--no-ignore",
"--deny-warnings",
"--config",
"config/oxlint-code-quality-casting.json",
"src/main/browser/browser-client-host-command-dispatcher.ts",
"src/main/browser/browser-client-host-command-result-cache.ts",
"src/main/browser/browser-client-host-command-retention.test.ts",
"docs/audits/browser-closed-result-retention/sources.cjs",
"docs/audits/browser-closed-result-retention/vitest.config.mjs",
"docs/audits/browser-closed-result-retention/scenario.test.mjs"
],
"exitCode": 0
},
{
"label": "type-aware code quality",
"command": [
"pnpm",
"exec",
"oxlint",
"--no-ignore",
"--deny-warnings",
"--type-aware",
"--config",
"config/oxlint-code-quality-type-aware.json",
"src/main/browser/browser-client-host-command-dispatcher.ts",
"src/main/browser/browser-client-host-command-result-cache.ts",
"src/main/browser/browser-client-host-command-retention.test.ts",
"docs/audits/browser-closed-result-retention/sources.cjs",
"docs/audits/browser-closed-result-retention/vitest.config.mjs",
"docs/audits/browser-closed-result-retention/scenario.test.mjs"
],
"exitCode": 0
},
{
"label": "React Doctor",
"command": [
"pnpm",
"exec",
"oxlint",
"--no-ignore",
"--deny-warnings",
"--config",
"config/oxlint-react-doctor.json",
"src/main/browser/browser-client-host-command-dispatcher.ts",
"src/main/browser/browser-client-host-command-result-cache.ts",
"src/main/browser/browser-client-host-command-retention.test.ts",
"docs/audits/browser-closed-result-retention/sources.cjs",
"docs/audits/browser-closed-result-retention/vitest.config.mjs",
"docs/audits/browser-closed-result-retention/scenario.test.mjs"
],
"exitCode": 0
},
{
"label": "design system",
"command": [
"pnpm",
"exec",
"oxlint",
"--no-ignore",
"--deny-warnings",
"--config",
"config/oxlint-design-system.json",
"src/main/browser/browser-client-host-command-dispatcher.ts",
"src/main/browser/browser-client-host-command-result-cache.ts",
"src/main/browser/browser-client-host-command-retention.test.ts",
"docs/audits/browser-closed-result-retention/sources.cjs",
"docs/audits/browser-closed-result-retention/vitest.config.mjs",
"docs/audits/browser-closed-result-retention/scenario.test.mjs"
],
"exitCode": 0
}
]
},
"changedQuality": {
"command": "ORCA_BACKGROUND_LAUNCH=1 ORCA_CODE_QUALITY_BASE=HEAD pnpm run check:code-quality:changed",
"exitCode": 0,
"note": "The five explicit-file scans include all six TS/CJS/MJS publication paths. The ordinary changed gate does not see ignored new artifacts before staging."
},
"proofs": {
"runs": [
{
"runtime": "node",
"variant": "before",
"command": [
"pnpm",
"exec",
"vitest",
"run",
"--config",
"docs/audits/browser-closed-result-retention/vitest.config.mjs",
"docs/audits/browser-closed-result-retention/scenario.test.mjs"
],
"exitCode": 0
},
{
"runtime": "node",
"variant": "fixed",
"command": [
"pnpm",
"exec",
"vitest",
"run",
"--config",
"docs/audits/browser-closed-result-retention/vitest.config.mjs",
"docs/audits/browser-closed-result-retention/scenario.test.mjs"
],
"exitCode": 0
},
{
"runtime": "electron",
"variant": "before",
"command": [
"node_modules/electron/dist/Electron.app/Contents/MacOS/Electron",
"node_modules/vitest/vitest.mjs",
"run",
"--config",
"docs/audits/browser-closed-result-retention/vitest.config.mjs",
"docs/audits/browser-closed-result-retention/scenario.test.mjs"
],
"exitCode": 0
},
{
"runtime": "electron",
"variant": "fixed",
"command": [
"node_modules/electron/dist/Electron.app/Contents/MacOS/Electron",
"node_modules/vitest/vitest.mjs",
"run",
"--config",
"docs/audits/browser-closed-result-retention/vitest.config.mjs",
"docs/audits/browser-closed-result-retention/scenario.test.mjs"
],
"exitCode": 0
}
],
"casesPerVariantPerRuntime": 4,
"variants": ["before", "fixed"],
"node": "26.6.0",
"electron": "43.7.0",
"electronNode": "24.21.0",
"controlledPendingNativePorts": 1,
"smallCompletedResults": 32,
"crlfReadControl": 24,
"environment": {
"ORCA_BACKGROUND_LAUNCH": "1",
"ELECTRON_RUN_AS_NODE": "1 for Electron runs",
"ORCA_BROWSER_CACHE_VARIANT": "before or fixed"
},
"outputOverride": "ORCA_BROWSER_CACHE_OUTPUT"
},
"sourceParity": {
"canonicalLF": true,
"mainCheckpoint": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"historicalVersion": "v1.4.198",
"historicalRef": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"namedBaselineTargetMatches": 2,
"mainCheckpointCitedSourceMatches": 23,
"historicalCitedSourceMatches": 21,
"citedSourceCount": 23,
"hashCoverage": "Two product targets plus 21 cited caller/dependency modules, not all transitive imports.",
"historicalApplicationReplay": false
},
"scope": {
"trigger": "A handler outlives the dispatcher close join (default 5 seconds).",
"nativeSettlementAuthorityPreserved": true,
"retirePageCachePolicyChanged": false,
"incidentAttribution": false,
"measuredRSS": false
},
"format": "All 14 publication files except fix.patch checked with oxfmt stdin mode; a second pass produced identical bytes.",
"gitDiffCheckExitCode": 0,
"publicationWhitespace": {
"commandTemplate": "git diff --no-index --check <empty-file> <promoted-path>",
"files": 14,
"expectedExitCode": 1,
"diagnostics": 0,
"note": "The complete content of every publication path is checked, including ignored new artifacts. Exit 1 only means the content differs from an empty file. fix.patch uses zero-context hunks."
}
}
@@ -0,0 +1,30 @@
import { resolve } from 'node:path'
import { createRequire } from 'node:module'
import { defineConfig, mergeConfig } from 'vitest/config'
import base from '../../../config/vitest.config.ts'
const { loadSources } = createRequire(import.meta.url)('./sources.cjs')
const { before, after } = loadSources()
const sources = process.env.ORCA_BROWSER_CACHE_VARIANT === 'before' ? before : after
const config = mergeConfig(
base,
defineConfig({
plugins: [
{
name: 'closed-browser-cache-source-overlay',
enforce: 'pre',
transform(_code, id) {
const source = sources.get(resolve(id.split('?')[0]))
return source === undefined ? undefined : { code: source, map: null }
}
}
]
})
)
config.test.include = [
'docs/audits/browser-closed-result-retention/scenario.test.mjs',
'src/main/browser/browser-client-host-command-retention.test.ts',
'src/main/browser/browser-client-host-command-dispatcher.test.ts'
]
config.test.maxWorkers = 1
export default config
@@ -0,0 +1,40 @@
# Destroyed browser guests retain main-process callbacks
An embedded browser guest's `destroyed` event called `cleanupGuestPolicyAttachment`. That removed its primary page-to-WebContents lookup but left four per-page cleanup callbacks that capture the destroyed WebContents wrapper, plus renderer/workspace/worktree/profile metadata. `unregisterAll` subsequently iterated only the now-empty primary lookup: three callback maps and the renderer/workspace metadata survived that cleanup too.
Renderer reload can destroy guests without each page sending explicit unregister IPC. A later close of a restored, unmounted page does not send that IPC either: `destroyPersistentWebview` returns early when its renderer registry has no guest. Explicit unregister correctly releases these resources; same-page re-registration also replaces its callbacks. The defect affects destroyed owners that do not take either path.
The fix routes destruction through the existing `unregisterGuest` with a guest-retirement reason only when that exact guest still owns the primary page ID. Already bound downloads retain their existing renderer routing until they settle; explicit page close still cancels them. Unregistered guests and popups retain policy-only cleanup. A stale callback cannot unregister a replacement. Normal renderer-process loss keeps its live WebContents and metadata for reload recovery; a fresh guest registration supplies its ownership metadata again. Shared browser sessions and sibling pages are untouched.
## Download lifetime correction
Review found that the initial fix treated guest destruction as logical page closure and canceled bound downloads. An exact-source before/after control confirmed that difference with an EventEmitter guest and controlled DownloadItem. Guest retirement now releases guest-owned callbacks while preserving ongoing page downloads, their destinations and cancel authorization. A retained numeric renderer route drains after the last download settles, provided no replacement guest, other download or newer routing owner needs it.
Nine additional controls cover progress and completion/error delivery, explicit close after guest destruction, multiple downloads, replacement guests/routing, repeated guest destruction and renderer loss. Together with four existing browser suites, 55 tests pass; Node typecheck and ordinary/anti-slop lint pass. These controls do not establish native Chromium download survival after destruction on each operating system. No download capacity or wire format changes.
## Reproduce
With dependencies already installed, run from the repository root:
```sh
ORCA_BACKGROUND_LAUNCH=1 node docs/audits/browser-destroyed-guest-retention/reproduce.mjs
```
The script runs the actual manager and guest callback installers with EventEmitter WebContents fixtures. It removes only the destruction guard in memory for the baseline, then runs the same nine tests on the fixed source. A temporary test observer records actual map sizes before each assertion. It launches no Orca window or browser process, limits each worker to 512 MiB and each run to 60 seconds, uses the shared process launcher, and removes temporary files. Results include source hashes and runtime provenance.
| After 1,000 distinct guest destructions | Before | Fixed |
| -------------------------------------------------------------------------- | -----: | ----: |
| Primary guest lookup | 0 | 0 |
| Each context-menu / grab-shortcut / app-shortcut / wheel cleanup map | 1,000 | 0 |
| Each renderer / workspace / worktree / profile map | 1,000 | 0 |
| Policy cleanup map | 0 | 0 |
| Each context-menu / grab-shortcut / app-shortcut map after `unregisterAll` | 1,000 | 0 |
| Renderer and workspace maps after `unregisterAll` | 1,000 | 0 |
Baseline: six tests pass, three fail. Fixed: all nine pass. Controls cover explicit unregister, same-ID replacement with a captured old callback, popup and pre-registration destruction, renderer-process recovery, fresh guest registration, and two pages sharing one browser session profile. The selected existing browser-manager and offscreen lifecycle suites also passed: 64 tests across seven files including the new suite.
## Version and limits
Targeted source reads of `v1.4.198` confirm the same destroyed-event policy-only cleanup, map ownership, and `unregisterAll` omission. The executable comparison uses current production source; it does not launch the historical app. This is a retaining path present in the version reported by #19831/#19768. It does not establish that either incident followed this destruction sequence, or measure native memory retained by a destroyed WebContents. The 1,000 iterations measure retained callbacks and metadata, not 1,000 surviving Chromium processes or a gigabyte allocation.
Adjacent audit negatives: explicit page close removes the renderer guest registry and main registration; worktree switching deliberately parks guests under the existing hidden-worktree retention policy; offscreen creation is synchronously indexed with shutdown admission and exact-window teardown; client-hosted async page creation checks availability after acquisitions and cleans canceled owners. PDF capture rejects late disconnected-client completion, its stream buffers have a five-minute TTL, and existing screenshot commands have deadlines. No additional native screenshot hang or unbounded native-page acquisition was reproduced. The separate late renderer registration reply can restore small page-ID metadata after close; it is outside this main-process fix.
@@ -0,0 +1,168 @@
import { createHash } from 'node:crypto'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { build } from 'esbuild'
if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') {
throw new Error('Run with ORCA_BACKGROUND_LAUNCH=1.')
}
const root = fileURLToPath(new URL('../../../', import.meta.url))
const productionPath = 'src/main/browser/browser-manager-guest-navigation-policy.ts'
const testPath = 'src/main/browser/browser-manager-destroyed-guest.test.ts'
const fixturePath = 'src/main/browser/browser-manager-destroyed-guest-test-fixture.ts'
const current = await readFile(resolve(root, productionPath), 'utf8')
const fix = ` const browserTabId = this.tabIdByWebContentsId.get(guest.id)
// A destroyed primary guest also owns per-page callbacks that capture its WebContents.
if (browserTabId && this.webContentsIdByTabId.get(browserTabId) === guest.id) {
this.unregisterGuest(browserTabId, 'guest-destroyed')
return
}
`
if (current.split(fix).length !== 2) {
throw new Error('Expected exactly one primary-guest destruction guard; review the transform.')
}
const baseline = current.replace(fix, '')
const test = await readFile(resolve(root, testPath), 'utf8')
const observe = ' const counts = manager.retainedCounts()\n'
if (test.split(observe).length !== 2) {
throw new Error('Expected exactly one retained-count observer; review the transform.')
}
const observedTest = `import { appendFileSync } from 'node:fs'\n${test.replace(
observe,
`${observe} appendFileSync(process.env.ORCA_BROWSER_GUEST_COUNTS_PATH, JSON.stringify({ test: expect.getState().currentTestName, counts }) + '\\n')\n`
)}`
const sha256 = (source) => createHash('sha256').update(source).digest('hex')
const sourceHashes = {
[productionPath]: { before: sha256(baseline), after: sha256(current) },
[testPath]: { current: sha256(test), observed: sha256(observedTest) },
[fixturePath]: { current: sha256(await readFile(resolve(root, fixturePath))) }
}
for (const path of [
'src/main/browser/browser-manager-state.ts',
'src/main/browser/browser-manager-registration.ts',
'src/main/browser/browser-manager-download-lifecycle.ts'
]) {
sourceHashes[path] = { current: sha256(await readFile(resolve(root, path))) }
}
const scratch = await mkdtemp(join(tmpdir(), 'orca-browser-destroyed-guest-'))
const require = createRequire(import.meta.url)
let runnerModuleId
try {
const runnerPath = join(scratch, 'run-process.cjs')
await build({
absWorkingDir: root,
entryPoints: [resolve(root, 'src/shared/child-process/run-process.ts')],
outfile: runnerPath,
bundle: true,
platform: 'node',
format: 'cjs',
logLevel: 'silent'
})
runnerModuleId = require.resolve(runnerPath)
const { runProcess } = require(runnerModuleId)
const configImport = JSON.stringify(pathToFileURL(resolve(root, 'config/vitest.config.ts')).href)
async function run(label, production) {
const config = join(scratch, `${label}.config.mjs`)
const report = join(scratch, `${label}.json`)
const countsPath = join(scratch, `${label}.counts.jsonl`)
const sources = {
[resolve(root, productionPath).replaceAll('\\', '/')]: production,
[resolve(root, testPath).replaceAll('\\', '/')]: observedTest
}
await writeFile(
config,
`import base from ${configImport};
const sources = ${JSON.stringify(sources)};
export default {...base, test: {...base.test, include: [${JSON.stringify(testPath)}], maxWorkers: 1}, plugins: [{
name: 'browser-destroyed-guest-audit', enforce: 'pre',
transform(_code, id) {
const source = sources[id.replaceAll('\\\\', '/').split('?')[0]];
return source === undefined ? null : {code: source, map: null};
}
}]};\n`
)
const result = await runProcess({
program: process.execPath,
args: [
resolve(root, 'node_modules/vitest/vitest.mjs'),
'run',
'--config',
config,
'--reporter=json',
`--outputFile=${report}`
],
cwd: root,
env: {
...process.env,
NODE_OPTIONS: '--max-old-space-size=512',
ORCA_BROWSER_GUEST_COUNTS_PATH: countsPath
},
timeoutMs: 60_000,
maxOutputBytes: 2 * 1024 * 1024
})
let parsed
try {
parsed = JSON.parse(await readFile(report, 'utf8'))
} catch (error) {
throw new Error(`${label} failed: ${result.stderr || result.stdout}`, { cause: error })
}
const counts = (await readFile(countsPath, 'utf8'))
.trim()
.split('\n')
.map((line) => JSON.parse(line))
return {
exitCode: result.code,
passed: parsed.numPassedTests,
failed: parsed.numFailedTests,
counts,
failedCases: parsed.testResults.flatMap((suite) =>
suite.assertionResults
.filter((assertion) => assertion.status === 'failed')
.map((assertion) => assertion.fullName)
)
}
}
const before = await run('before', baseline)
const after = await run('after', current)
const passed =
before.passed === 6 &&
before.failed === 3 &&
after.passed === 9 &&
after.failed === 0 &&
before.counts.length === 9 &&
after.counts.length === 9 &&
before.counts[0].counts.contextMenus === 1000 &&
before.counts[1].counts.contextMenus === 1000 &&
after.counts[0].counts.contextMenus === 0 &&
after.counts[1].counts.contextMenus === 0
console.log(
JSON.stringify(
{
comparison:
'Actual BrowserManager registration, destroyed-event handler, callback maps and unregisterAll; Electron methods use EventEmitter fixtures. Baseline removes only the exact-primary destruction cleanup in a temporary source transform.',
provenance: { node: process.version, platform: process.platform, arch: process.arch },
sourceHashes,
before,
after,
passed
},
null,
2
)
)
if (!passed) {
process.exitCode = 1
}
} finally {
if (runnerModuleId) {
delete require.cache[runnerModuleId]
}
await rm(scratch, { recursive: true, force: true })
}
@@ -0,0 +1,321 @@
{
"comparison": "Actual BrowserManager registration, destroyed-event handler, callback maps and unregisterAll; Electron methods use EventEmitter fixtures. Baseline removes only the exact-primary destruction cleanup in a temporary source transform.",
"provenance": {
"node": "v26.6.0",
"platform": "darwin",
"arch": "arm64"
},
"sourceHashes": {
"src/main/browser/browser-manager-guest-navigation-policy.ts": {
"before": "35aee2b5665a49535897fd0589853248f902061f77b3e142f94e90eabeb7332c",
"after": "741ac6d9f30fcf82993fa5c11f40093ba8a75483866407776c983538453f9b32"
},
"src/main/browser/browser-manager-destroyed-guest.test.ts": {
"current": "25806188a208952a1bebd042ec0dc4552784179ed3b738a5d5336789604ef314",
"observed": "8e59825237dea1b53294361122f0ea681947d4e6e9ea8fa771b5e15e080d88a6"
},
"src/main/browser/browser-manager-destroyed-guest-test-fixture.ts": {
"current": "4e6bf3589a3888860e6cfa8b6c83e8e50b4344650a5aaa85f221930ee8f9c0fd"
},
"src/main/browser/browser-manager-state.ts": {
"current": "1d82e875461984b3bee2d9dc9b477be7518e9394ace77b24e9b513766cc8a210"
},
"src/main/browser/browser-manager-registration.ts": {
"current": "55d66285b1d3ad29a7be596b1ca3536f333f2e3b2580e4d2a41090928ca8edd1"
},
"src/main/browser/browser-manager-download-lifecycle.ts": {
"current": "8c8d0dd488d31f6098f7ea1000ed8d6ffd8b23704a097db974207c9732d77c2a"
}
},
"before": {
"exitCode": 1,
"passed": 6,
"failed": 3,
"counts": [
{
"test": "browser guest destruction ownership > releases registered callbacks and ownership after 1000 distinct guest destructions",
"counts": {
"guests": 0,
"contextMenus": 1000,
"grabShortcuts": 1000,
"appShortcuts": 1000,
"wheelHandlers": 1000,
"renderers": 1000,
"workspaces": 1000,
"worktrees": 1000,
"profiles": 1000,
"policies": 0
}
},
{
"test": "browser guest destruction ownership > leaves no dead-guest callbacks for the window-close unregisterAll path",
"counts": {
"guests": 0,
"contextMenus": 1000,
"grabShortcuts": 1000,
"appShortcuts": 1000,
"wheelHandlers": 0,
"renderers": 1000,
"workspaces": 1000,
"worktrees": 0,
"profiles": 0,
"policies": 0
}
},
{
"test": "browser guest destruction ownership > keeps explicit unregister before destruction idempotent",
"counts": {
"guests": 0,
"contextMenus": 0,
"grabShortcuts": 0,
"appShortcuts": 0,
"wheelHandlers": 0,
"renderers": 0,
"workspaces": 0,
"worktrees": 0,
"profiles": 0,
"policies": 0
}
},
{
"test": "browser guest destruction ownership > does not let a captured old destroyed callback retire a replacement guest",
"counts": {
"guests": 1,
"contextMenus": 1,
"grabShortcuts": 1,
"appShortcuts": 1,
"wheelHandlers": 1,
"renderers": 1,
"workspaces": 1,
"worktrees": 1,
"profiles": 1,
"policies": 1
}
},
{
"test": "browser guest destruction ownership > cleans a popup without retiring its live primary page",
"counts": {
"guests": 1,
"contextMenus": 1,
"grabShortcuts": 1,
"appShortcuts": 1,
"wheelHandlers": 1,
"renderers": 1,
"workspaces": 1,
"worktrees": 1,
"profiles": 1,
"policies": 1
}
},
{
"test": "browser guest destruction ownership > cleans policies for a guest destroyed before registration",
"counts": {
"guests": 0,
"contextMenus": 0,
"grabShortcuts": 0,
"appShortcuts": 0,
"wheelHandlers": 0,
"renderers": 0,
"workspaces": 0,
"worktrees": 0,
"profiles": 0,
"policies": 0
}
},
{
"test": "browser guest destruction ownership > preserves live guest ownership when its renderer process needs reload recovery",
"counts": {
"guests": 1,
"contextMenus": 1,
"grabShortcuts": 1,
"appShortcuts": 1,
"wheelHandlers": 1,
"renderers": 1,
"workspaces": 1,
"worktrees": 1,
"profiles": 1,
"policies": 1
}
},
{
"test": "browser guest destruction ownership > rebuilds ownership when a restored page registers its fresh guest",
"counts": {
"guests": 1,
"contextMenus": 1,
"grabShortcuts": 1,
"appShortcuts": 1,
"wheelHandlers": 1,
"renderers": 1,
"workspaces": 1,
"worktrees": 1,
"profiles": 1,
"policies": 1
}
},
{
"test": "browser guest destruction ownership > preserves a sibling page using the same browser session profile",
"counts": {
"guests": 1,
"contextMenus": 2,
"grabShortcuts": 2,
"appShortcuts": 2,
"wheelHandlers": 2,
"renderers": 2,
"workspaces": 2,
"worktrees": 2,
"profiles": 2,
"policies": 1
}
}
],
"failedCases": [
"browser guest destruction ownership releases registered callbacks and ownership after 1000 distinct guest destructions",
"browser guest destruction ownership leaves no dead-guest callbacks for the window-close unregisterAll path",
"browser guest destruction ownership preserves a sibling page using the same browser session profile"
]
},
"after": {
"exitCode": 0,
"passed": 9,
"failed": 0,
"counts": [
{
"test": "browser guest destruction ownership > releases registered callbacks and ownership after 1000 distinct guest destructions",
"counts": {
"guests": 0,
"contextMenus": 0,
"grabShortcuts": 0,
"appShortcuts": 0,
"wheelHandlers": 0,
"renderers": 0,
"workspaces": 0,
"worktrees": 0,
"profiles": 0,
"policies": 0
}
},
{
"test": "browser guest destruction ownership > leaves no dead-guest callbacks for the window-close unregisterAll path",
"counts": {
"guests": 0,
"contextMenus": 0,
"grabShortcuts": 0,
"appShortcuts": 0,
"wheelHandlers": 0,
"renderers": 0,
"workspaces": 0,
"worktrees": 0,
"profiles": 0,
"policies": 0
}
},
{
"test": "browser guest destruction ownership > keeps explicit unregister before destruction idempotent",
"counts": {
"guests": 0,
"contextMenus": 0,
"grabShortcuts": 0,
"appShortcuts": 0,
"wheelHandlers": 0,
"renderers": 0,
"workspaces": 0,
"worktrees": 0,
"profiles": 0,
"policies": 0
}
},
{
"test": "browser guest destruction ownership > does not let a captured old destroyed callback retire a replacement guest",
"counts": {
"guests": 1,
"contextMenus": 1,
"grabShortcuts": 1,
"appShortcuts": 1,
"wheelHandlers": 1,
"renderers": 1,
"workspaces": 1,
"worktrees": 1,
"profiles": 1,
"policies": 1
}
},
{
"test": "browser guest destruction ownership > cleans a popup without retiring its live primary page",
"counts": {
"guests": 1,
"contextMenus": 1,
"grabShortcuts": 1,
"appShortcuts": 1,
"wheelHandlers": 1,
"renderers": 1,
"workspaces": 1,
"worktrees": 1,
"profiles": 1,
"policies": 1
}
},
{
"test": "browser guest destruction ownership > cleans policies for a guest destroyed before registration",
"counts": {
"guests": 0,
"contextMenus": 0,
"grabShortcuts": 0,
"appShortcuts": 0,
"wheelHandlers": 0,
"renderers": 0,
"workspaces": 0,
"worktrees": 0,
"profiles": 0,
"policies": 0
}
},
{
"test": "browser guest destruction ownership > preserves live guest ownership when its renderer process needs reload recovery",
"counts": {
"guests": 1,
"contextMenus": 1,
"grabShortcuts": 1,
"appShortcuts": 1,
"wheelHandlers": 1,
"renderers": 1,
"workspaces": 1,
"worktrees": 1,
"profiles": 1,
"policies": 1
}
},
{
"test": "browser guest destruction ownership > rebuilds ownership when a restored page registers its fresh guest",
"counts": {
"guests": 1,
"contextMenus": 1,
"grabShortcuts": 1,
"appShortcuts": 1,
"wheelHandlers": 1,
"renderers": 1,
"workspaces": 1,
"worktrees": 1,
"profiles": 1,
"policies": 1
}
},
{
"test": "browser guest destruction ownership > preserves a sibling page using the same browser session profile",
"counts": {
"guests": 1,
"contextMenus": 1,
"grabShortcuts": 1,
"appShortcuts": 1,
"wheelHandlers": 1,
"renderers": 1,
"workspaces": 1,
"worktrees": 1,
"profiles": 1,
"policies": 1
}
}
],
"failedCases": []
},
"passed": true
}
@@ -0,0 +1,30 @@
# Late browser registration replies restore retired renderer state
`createBrowserPageWebviewGuestSession` awaited `registerGuest` IPC and then wrote the returned guest ID into the renderer's persistent `registeredWebContentsIds` map. An explicit close could remove the webview and map entry before that reply arrived; a delayed success restored the retired entry. An older reply could also overwrite the ID of a replacement guest. Its follow-on callbacks could synchronize an obsolete annotation bridge or mutate recovery state after the listener session was disposed. Separately, recovery validation could issue repair IPC after its initial registration query outlived that owner.
The fix checks the existing recovery disposal state, current listener ref, persistent registry identity, and captured WebContents ID before accepting a reply or running those continuations. It makes no new registry and sends no late unregister IPC. A current hidden guest still accepts successful registration. When a persistent guest remounts, the new session's existing `validateAfterResume` path retries registration if the old reply was ignored. Current unsuccessful replies and current repair retain their prior behavior.
## Reproduce
With dependencies already installed:
```sh
ORCA_BACKGROUND_LAUNCH=1 node docs/audits/browser-registration-reply-retention/reproduce.mjs
```
This runs the actual renderer session, recovery controller, and persistent guest registry against headless DOM fixtures and deferred IPC replies. The baseline reverses only the included production patch in memory. A temporary observer records map/callback counts after all replies settle. The script uses the shared process launcher, 512 MiB workers, a 60-second deadline, and temporary files removed in `finally`. No Orca window, native guest, or remote host is launched.
| After 1,000 explicit guest closes and delayed successful replies | Before | Fixed |
| ---------------------------------------------------------------- | -----: | ----: |
| Live webviews | 0 | 0 |
| Retained registration entries | 1,000 | 0 |
| Late annotation synchronizations | 1,000 | 0 |
| Unregister calls | 1,000 | 1,000 |
The baseline fails ten tests and passes six controls; fixed source passes all 16. Cases cover distinct closed IDs, replacement elements, a changed guest ID on the same element, a remount reusing the same element/ref, disposed and moved refs, registry removal before listener disposal, a throwing identity getter, hidden current guests, successful/inconclusive replies, and late versus current repair. The repair-completion case verifies that an old success cannot clear a newer guest's recovery error. All host-guest suites also pass: 196 tests across 23 files, including recovery, viewport, registry, worktree retention, and paintability. The web typecheck passes.
## Version and limits
Targeted reads of `v1.4.198` confirm the same unconditional registration setter, post-reply callbacks, post-query repair, and close-time map deletion. This establishes a renderer retaining path in the reported version, not that #19831 or #19768 exercised it. Each retained entry is a page ID and numeric guest ID. This proof does not show a surviving native browser process or explain gigabyte-scale memory growth. The independent main-process destroyed-guest callback retention has its own fix and proof.
The registration reply is the only production setter of `registeredWebContentsIds`; explicit close and replacement remove its key. Following callers found no second setter that could recreate this same metadata after removal. The annotation callback uses current page routing, which is why skipping a stale callback is necessary without issuing cleanup against a replacement.
@@ -0,0 +1,126 @@
diff --git a/src/renderer/src/components/browser-pane/host-guest/browser-page-guest-recovery.ts b/src/renderer/src/components/browser-pane/host-guest/browser-page-guest-recovery.ts
index 45f3b354b5..b6e30917d1 100644
--- a/src/renderer/src/components/browser-pane/host-guest/browser-page-guest-recovery.ts
+++ b/src/renderer/src/components/browser-pane/host-guest/browser-page-guest-recovery.ts
@@ -21,6 +21,7 @@ type BrowserPageGuestRecoveryOptions = {
export type BrowserPageGuestRecovery = {
confirmRegistration: () => void
dispose: () => void
+ isDisposed: () => boolean
finish: () => boolean
recoverRenderer: () => void
retryRecovery: () => void
@@ -263,6 +264,7 @@ export function createBrowserPageGuestRecovery(
clearValidationRetry()
clearValidationTimeout()
},
+ isDisposed: () => disposed,
finish,
recoverRenderer,
retryRecovery: () => {
diff --git a/src/renderer/src/components/browser-pane/host-guest/browser-page-webview-guest-session.ts b/src/renderer/src/components/browser-pane/host-guest/browser-page-webview-guest-session.ts
index 4623b817b2..dd5e243ba2 100644
--- a/src/renderer/src/components/browser-pane/host-guest/browser-page-webview-guest-session.ts
+++ b/src/renderer/src/components/browser-pane/host-guest/browser-page-webview-guest-session.ts
@@ -15,7 +15,11 @@ import {
type BrowserPageGuestRecovery
} from './browser-page-guest-recovery'
import { browserPageZoomLevelToPercent, setBrowserPageZoomLevel } from './browser-page-zoom'
-import { registeredWebContentsIds, replacePersistentWebview } from './webview-registry'
+import {
+ registeredWebContentsIds,
+ replacePersistentWebview,
+ webviewRegistry
+} from './webview-registry'
import { browserPageExists } from '../describe-page/browser-page-load-error'
import type {
BrowserPageRecoveryNavigationValidation,
@@ -80,11 +84,21 @@ export function createBrowserPageWebviewGuestSession({
webContentsId: number
promise: Promise<boolean | null>
} | null = null
- const registerGuest = (): Promise<boolean | null> => {
- let webContentsId: number
+ const readWebContentsId = (): number | null => {
try {
- webContentsId = webview.getWebContentsId()
+ return webview.getWebContentsId()
} catch {
+ return null
+ }
+ }
+ const ownsGuest = (webContentsId: number | null): boolean =>
+ webContentsId !== null &&
+ !guestRecovery.isDisposed() &&
+ webviewRef.current === webview &&
+ webviewRegistry.get(browserTabId) === webview &&
+ readWebContentsId() === webContentsId
+ const registerGuest = (webContentsId: number | null): Promise<boolean | null> => {
+ if (webContentsId === null || !ownsGuest(webContentsId)) {
return Promise.resolve(null)
}
if (registrationInFlight?.webContentsId === webContentsId) {
@@ -99,6 +113,9 @@ export function createBrowserPageWebviewGuestSession({
webContentsId
})
.then((registered) => {
+ if (!ownsGuest(webContentsId)) {
+ return null
+ }
if (registered) {
registeredWebContentsIds.set(browserTabId, webContentsId)
return true
@@ -146,22 +163,26 @@ export function createBrowserPageWebviewGuestSession({
return null
}
if (registeredWebContentsIds.get(browserTabId) !== webContentsId) {
- return registerGuest()
+ return registerGuest(webContentsId)
}
const registered = await window.api.browser.isGuestRegistered({
browserPageId: browserTabId,
webContentsId
})
+ if (!ownsGuest(webContentsId)) {
+ return null
+ }
if (registered) {
return true
}
- return window.api.browser.repairGuestRegistration({
+ const repaired = await window.api.browser.repairGuestRegistration({
browserPageId: browserTabId,
workspaceId,
worktreeId,
sessionProfileId,
webContentsId
})
+ return ownsGuest(webContentsId) ? repaired : null
},
replaceGuest: () => replacePersistentWebview(browserTabId),
onReplacementReady: () => setGuestRecoveryGeneration((generation) => generation + 1),
@@ -184,7 +205,11 @@ export function createBrowserPageWebviewGuestSession({
const handleDidAttach = (): void => {
// Why: register at attach since cert failures can precede dom-ready; the dom-ready path stays an idempotent fallback.
- void registerGuest().then((registered) => {
+ const webContentsId = readWebContentsId()
+ void registerGuest(webContentsId).then((registered) => {
+ if (!ownsGuest(webContentsId)) {
+ return
+ }
if (registered === true) {
guestRecovery.confirmRegistration()
}
@@ -207,7 +232,10 @@ export function createBrowserPageWebviewGuestSession({
const queuedAnnotationViewportBridgeSync =
liveWebContentsId === null || registeredWebContentsIds.get(browserTabId) !== liveWebContentsId
if (queuedAnnotationViewportBridgeSync) {
- void registerGuest().then((registered) => {
+ void registerGuest(liveWebContentsId).then((registered) => {
+ if (!ownsGuest(liveWebContentsId)) {
+ return
+ }
const completedRecovery = guestRecovery.finish()
if (registered === true) {
guestRecovery.confirmRegistration()
@@ -0,0 +1,153 @@
import { createHash } from 'node:crypto'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { applyPatch, parsePatch, reversePatch } from 'diff'
import { build } from 'esbuild'
if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') {
throw new Error('Run with ORCA_BACKGROUND_LAUNCH=1.')
}
const root = fileURLToPath(new URL('../../../', import.meta.url))
const patch = await readFile(new URL('./fix.patch', import.meta.url), 'utf8')
const beforeSources = {}
const sourceHashes = {}
const sha256 = (source) => createHash('sha256').update(source).digest('hex')
for (const parsed of parsePatch(patch)) {
const path = parsed.newFileName.replace(/^b\//, '')
const absolute = resolve(root, path)
const current = await readFile(absolute, 'utf8')
const before = applyPatch(current, reversePatch(parsed))
if (before === false) {
throw new Error(`Source changed; review the proof patch: ${path}`)
}
beforeSources[absolute.replaceAll('\\', '/')] = before
sourceHashes[path] = { before: sha256(before), after: sha256(current) }
}
const testPath =
'src/renderer/src/components/browser-pane/host-guest/browser-page-registration-ownership.test.ts'
const test = await readFile(resolve(root, testPath), 'utf8')
const countAssertion = ' expect(webviewRegistry.size).toBe(0)\n'
if (test.split(countAssertion).length !== 2) {
throw new Error('Expected exactly one closed-guest count assertion; review the observer.')
}
const observedTest = `import { writeFileSync } from 'node:fs'\n${test.replace(
countAssertion,
` writeFileSync(process.env.ORCA_BROWSER_REGISTRATION_COUNTS_PATH, JSON.stringify({ liveWebviews: webviewRegistry.size, registeredGuestIds: registeredWebContentsIds.size, lateAnnotationSyncs: sessions.reduce((count, page) => count + page.sync.mock.calls.length, 0), unregisterCalls: unregister.mock.calls.length }))\n${countAssertion}`
)}`
sourceHashes[testPath] = { current: sha256(test), observed: sha256(observedTest) }
const scratch = await mkdtemp(join(tmpdir(), 'orca-browser-registration-reply-'))
const require = createRequire(import.meta.url)
let runnerModuleId
try {
const runnerPath = join(scratch, 'run-process.cjs')
await build({
absWorkingDir: root,
entryPoints: [resolve(root, 'src/shared/child-process/run-process.ts')],
outfile: runnerPath,
bundle: true,
platform: 'node',
format: 'cjs',
logLevel: 'silent'
})
runnerModuleId = require.resolve(runnerPath)
const { runProcess } = require(runnerModuleId)
const configImport = JSON.stringify(pathToFileURL(resolve(root, 'config/vitest.config.ts')).href)
async function run(label, productionSources) {
const config = join(scratch, `${label}.config.mjs`)
const report = join(scratch, `${label}.json`)
const countsPath = join(scratch, `${label}.counts.json`)
const sources = {
...productionSources,
[resolve(root, testPath).replaceAll('\\', '/')]: observedTest
}
await writeFile(
config,
`import base from ${configImport};
const sources = ${JSON.stringify(sources)};
export default {...base, test: {...base.test, include: [${JSON.stringify(testPath)}], maxWorkers: 1}, plugins: [{
name: 'browser-registration-reply-audit', enforce: 'pre',
transform(_code, id) {
const source = sources[id.replaceAll('\\\\', '/').split('?')[0]];
return source === undefined ? null : {code: source, map: null};
}
}]};\n`
)
const result = await runProcess({
program: process.execPath,
args: [
resolve(root, 'node_modules/vitest/vitest.mjs'),
'run',
'--config',
config,
'--reporter=json',
`--outputFile=${report}`
],
cwd: root,
env: {
...process.env,
NODE_OPTIONS: '--max-old-space-size=512',
ORCA_BROWSER_REGISTRATION_COUNTS_PATH: countsPath
},
timeoutMs: 60_000,
maxOutputBytes: 2 * 1024 * 1024
})
let parsed
try {
parsed = JSON.parse(await readFile(report, 'utf8'))
} catch (error) {
throw new Error(`${label} failed: ${result.stderr || result.stdout}`, { cause: error })
}
return {
exitCode: result.code,
passed: parsed.numPassedTests,
failed: parsed.numFailedTests,
after1000ClosedGuests: JSON.parse(await readFile(countsPath, 'utf8')),
failedCases: parsed.testResults.flatMap((suite) =>
suite.assertionResults
.filter((assertion) => assertion.status === 'failed')
.map((assertion) => assertion.fullName)
)
}
}
const before = await run('before', beforeSources)
const after = await run('after', {})
const passed =
before.passed === 6 &&
before.failed === 10 &&
after.passed === 16 &&
after.failed === 0 &&
before.after1000ClosedGuests.liveWebviews === 0 &&
before.after1000ClosedGuests.registeredGuestIds === 1000 &&
after.after1000ClosedGuests.registeredGuestIds === 0 &&
after.after1000ClosedGuests.unregisterCalls === 1000
console.log(
JSON.stringify(
{
comparison:
'Actual renderer guest session, recovery controller and persistent guest registry with deferred IPC replies; baseline reverses only fix.patch in a temporary source transform.',
provenance: { node: process.version, platform: process.platform, arch: process.arch },
sourceHashes,
before,
after,
passed
},
null,
2
)
)
if (!passed) {
process.exitCode = 1
}
} finally {
if (runnerModuleId) {
delete require.cache[runnerModuleId]
}
await rm(scratch, { recursive: true, force: true })
}
@@ -0,0 +1,58 @@
{
"comparison": "Actual renderer guest session, recovery controller and persistent guest registry with deferred IPC replies; baseline reverses only fix.patch in a temporary source transform.",
"provenance": {
"node": "v26.6.0",
"platform": "darwin",
"arch": "arm64"
},
"sourceHashes": {
"src/renderer/src/components/browser-pane/host-guest/browser-page-guest-recovery.ts": {
"before": "044280571771c4e90d07f5f5426878539f2b1729d9a4482c59d8fd236e7d4177",
"after": "2efdea1c4223b2f4114548a7f6ec4b576e1c6bde7819a07dc096d5167c2ba42c"
},
"src/renderer/src/components/browser-pane/host-guest/browser-page-webview-guest-session.ts": {
"before": "2fe17f0fe4f8ef6ca7cff7ca5731d9d725dbf4b5e7944264e30f12241317fc6d",
"after": "a78cc98873e93834bf07b47bbf5d92da888dfbccef06551aa6ac3c8f6e9f29f8"
},
"src/renderer/src/components/browser-pane/host-guest/browser-page-registration-ownership.test.ts": {
"current": "992b4730cd5546720b8b52366955386ad900d057f95c13beb6b92e5369bf5c1e",
"observed": "ebc38ebb195295170e8c355ecda539f3f2e75bb80548dbf4e97a17a7cd88c661"
}
},
"before": {
"exitCode": 1,
"passed": 6,
"failed": 10,
"after1000ClosedGuests": {
"liveWebviews": 0,
"registeredGuestIds": 1000,
"lateAnnotationSyncs": 1000,
"unregisterCalls": 1000
},
"failedCases": [
"renderer registration completion ownership does not restore 1000 closed IDs from delayed successful replies",
"renderer registration completion ownership keeps the replacement ID after an older reply arrives",
"renderer registration completion ownership keeps a new ID when the same DOM webview swaps its guest",
"renderer registration completion ownership a new session retries a disposed session registration on the same persistent guest and ref",
"renderer registration completion ownership a disposed listener cannot act after the same guest and ref are reused",
"renderer registration completion ownership does not restore a registry-removed guest before listener disposal runs",
"renderer registration completion ownership does not restore metadata when the current listener ref has moved",
"renderer registration completion ownership ignores a reply after reading the guest identity starts throwing",
"renderer registration completion ownership does not issue repair after a pending validation outlives its owner",
"renderer registration completion ownership does not clear a newer guest recovery error from a pending old repair reply"
]
},
"after": {
"exitCode": 0,
"passed": 16,
"failed": 0,
"after1000ClosedGuests": {
"liveWebviews": 0,
"registeredGuestIds": 0,
"lateAnnotationSyncs": 0,
"unregisterCalls": 1000
},
"failedCases": []
},
"passed": true
}
@@ -0,0 +1,44 @@
# Retired browser viewport operation ownership
The viewport operation captures a guest ID, then awaits CDP commands. Closing a tab deletes its viewport state, but the old continuation can subsequently recreate the UA-intent entry. A failed UA clear can also restore the old value over a replacement guest's completed desktop preset, or a late clear can delete the replacement's mobile intent.
The correction reuses that captured guest ID at three mutation boundaries: before publishing an applied preset's UA intent, before reading/deleting a cleared preset's intent, and before failed-clear rollback. Same-owner rollback, native UA profiles, navigation behavior, and the per-tab promise chain are preserved.
## Evidence
The regression fixture calls the actual manager, registration, unregistration, and viewport implementation. Electron WebContents and pending debugger replies are controlled ports; no native browser or window is launched.
- Baseline: **7 failing ownership regressions, 5 passing controls**.
- Fixed: **12/12 ownership cases**, plus **30 existing viewport, navigation, partial-failure, and UA cases**.
- Sixteen pending UA-clear rejections after `unregisterAll` leave **16 retired UA entries before, zero after**. Registration, preset, and promise maps remain empty.
- Other regressions cover closed-tab late success, failed-clear rollback, mobile/desktop replacement, and native-to-default profile replacement.
- Controls preserve ordinary serialized mobile/desktop/null operations, both native-profile presets, same-owner rollback, and the replacement promise tail while old queued operations settle.
- An independent reviewer ran all 12 candidate cases and reviewed the three mutation guards before promotion.
The retained entries are tab ID strings and booleans. This does **not** demonstrate retained native WebContents, a process RSS slope, or gigabyte-scale memory growth. In-flight CDP work still owns its continuation until it settles. Positive and negative post-close command replies are injected schedules, not an affected-host trace.
## Ordinary callers and compatibility
The renderer requests overrides when the user selects a viewport preset and on guest `dom-ready`, including null presets. The trusted IPC handler validates dimensions before calling this manager. Navigation later reads the UA-intent map, so stale replacement values can alter the standing mobile/desktop identity. The fixture does not execute the renderer or IPC producer.
Both local webview and host-side offscreen registrations use these maps. The correction changes no wire fields, protocol, execution-host ownership, native process lifecycle, folder/worktree handling, or UI layout. It only prevents an operation for a different guest from mutating the current registration's state.
`source-versions.json` records 11 paths at audit checkpoint `4a09b1d1`, independent main `291b4ddd`, and reported v1.4.198 `e0826956`. The viewport implementation, registration, registry declarations, IPC handler, and toolbar producer match all three. Ten sources match independent main and eight match v1.4.198. The guest-session producer contains an earlier audit fix; historical navigation and fixture sources differ. This is a current-dependency replay with the exact historical viewport source, not a historical app-binary replay.
The browsing activity in #19831 makes this path applicable in principle. The report does not establish the required overlap or tab count, and this small metadata mechanism does not account for its reported memory totals.
## Reproduction
From the worktree, run the fixed regression suite:
```sh
ORCA_BACKGROUND_LAUNCH=1 node node_modules/vitest/vitest.mjs run --config docs/audits/browser-viewport-owner-retention/vitest.config.mjs
```
Run the same tests with the exact baseline viewport implementation; exit status 1 and seven failed cases are expected:
```sh
ORCA_BACKGROUND_LAUNCH=1 ORCA_VIEWPORT_BASELINE=1 node node_modules/vitest/vitest.mjs run --config docs/audits/browser-viewport-owner-retention/vitest.config.mjs
```
The import overlay never rewrites product files. `baseline-source.txt` contains only the original viewport module; current support modules remain in use. `baseline-results.json`, `fixed-results.json`, and `validation.json` record the measured results and their scope.
@@ -0,0 +1,94 @@
{
"testFiles": 1,
"total": 12,
"passed": 5,
"failed": 7,
"cases": [
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership does not recreate closed-tab UA intent after a late touch completion",
"status": "failed",
"failures": [
"AssertionError: expected true to be undefined\n at ./src/main/browser/browser-manager-viewport-ownership.test.ts:109:37\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file://./node_modules/.pnpm/@vitest+runner@4.1.11/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"
]
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership does not restore closed-tab UA intent after a failed clear",
"status": "failed",
"failures": [
"AssertionError: expected true to be undefined\n at ./src/main/browser/browser-manager-viewport-ownership.test.ts:126:42\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file://./node_modules/.pnpm/@vitest+runner@4.1.11/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"
]
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership preserves replacement desktop intent after an old clear fails",
"status": "failed",
"failures": [
"AssertionError: expected true to be false // Object.is equality\n at ./src/main/browser/browser-manager-viewport-ownership.test.ts:142:42\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file://./node_modules/.pnpm/@vitest+runner@4.1.11/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"
]
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership preserves replacement mobile intent after an old clear resumes",
"status": "failed",
"failures": [
"AssertionError: expected false to be true // Object.is equality\n at ./src/main/browser/browser-manager-viewport-ownership.test.ts:160:42\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file://./node_modules/.pnpm/@vitest+runner@4.1.11/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"
]
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership same-owner clear failure still restores the legitimate earlier intent",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership old apply cannot overwrite a replacement guest desktop intent",
"status": "failed",
"failures": [
"AssertionError: expected true to be false // Object.is equality\n at ./src/main/browser/browser-manager-viewport-ownership.test.ts:185:41\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file://./node_modules/.pnpm/@vitest+runner@4.1.11/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"
]
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership an old native profile cannot write UA intent after replacement with a default profile",
"status": "failed",
"failures": [
"AssertionError: expected true to be undefined\n at ./src/main/browser/browser-manager-viewport-ownership.test.ts:197:49\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file://./node_modules/.pnpm/@vitest+runner@4.1.11/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"
]
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership old queued operations cannot remove or join a replacement promise tail",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership normal same-owner toggles preserve last-requested order and remove the promise tail",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership native UA mode remains unchanged with mobile=false",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership native UA mode remains unchanged with mobile=true",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership late rejected clears cannot repopulate all registries after unregisterAll",
"status": "failed",
"failures": [
"AssertionError: expected 16 to be +0 // Object.is equality\n at ./src/main/browser/browser-manager-viewport-ownership.test.ts:278:28\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file://./node_modules/.pnpm/@vitest+runner@4.1.11/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20"
]
}
]
}
@@ -0,0 +1,219 @@
import { webContents } from 'electron'
import {
BROWSER_ANNOTATION_VIEWPORT_BRIDGE_WORLD_ID,
buildBrowserAnnotationViewportBridgeScript,
type BrowserAnnotationViewportBridgeOptions
} from '../../shared/browser-annotation-viewport-bridge'
import type { BrowserViewportOverride } from '../../shared/browser-workspace-types'
import { googleAuthUserAgent, isGoogleAuthUrl } from './browser-google-auth-ua'
import { BrowserManagerDownloadLifecycle } from './browser-manager-download-lifecycle'
export abstract class BrowserManagerViewport extends BrowserManagerDownloadLifecycle {
// Why: guests are isolated from Orca's preload bridge, so main owns the devtools escape hatch after a tab→guest lookup.
async openDevTools(browserTabId: string): Promise<boolean> {
const webContentsId = this.webContentsIdByTabId.get(browserTabId)
if (!webContentsId) {
return false
}
const guest = webContents.fromId(webContentsId)
if (!guest || guest.isDestroyed()) {
// Why: a stale guest must clear every per-tab registry entry, not just the WebContents maps.
this.unregisterGuest(browserTabId)
return false
}
// Offscreen guests have no visible window on this desktop; detaching DevTools would open it
// on the host display with no route back to the remote client.
if (this.offscreenGuestIds.has(webContentsId)) {
return false
}
guest.openDevTools({ mode: 'detach' })
return true
}
// Why: emulate viewport via CDP; never detach the debugger here or the agent bridge's per-guest state is cleared.
async setViewportOverride(
browserTabId: string,
override: BrowserViewportOverride | null
): Promise<boolean> {
// Why: chain per-tab so rapid toggles don't interleave CDP commands and the last-requested override wins.
const expectedWebContentsId = this.webContentsIdByTabId.get(browserTabId)
if (expectedWebContentsId !== undefined) {
// Keep host panning available while CDP applies the requested dimensions. The guest id fence
// prevents this intent from leaking to a replacement guest; clearing the preset removes it.
this.viewportPresetActiveByTabId.set(browserTabId, {
guestWebContentsId: expectedWebContentsId,
active: override !== null
})
}
// The renderer resizes the host before CDP completes; discard the old geometry until it
// reports the new pane bounds so a pending preset cannot route wheel input using stale limits.
this.viewportScrollStateByTabId.delete(browserTabId)
const prev = this.viewportOpsByTabId.get(browserTabId) ?? Promise.resolve()
const next = prev
.catch(() => {})
.then(() => this.doSetViewportOverrideImpl(browserTabId, override, expectedWebContentsId))
this.viewportOpsByTabId.set(browserTabId, next)
try {
return await next
} finally {
// Why: only clear if we're still the tail; a later call may have replaced the entry, and deleting would break serialization.
if (this.viewportOpsByTabId.get(browserTabId) === next) {
this.viewportOpsByTabId.delete(browserTabId)
}
}
}
async setAnnotationViewportBridge(
browserTabId: string,
options: BrowserAnnotationViewportBridgeOptions,
resolveGuest: () => Electron.WebContents | null
): Promise<boolean> {
const prev = this.annotationViewportBridgeOpsByTabId.get(browserTabId) ?? Promise.resolve()
const next = prev
.catch(() => {})
.then(() => this.doSetAnnotationViewportBridgeImpl(options, resolveGuest))
this.annotationViewportBridgeOpsByTabId.set(browserTabId, next)
try {
return await next
} finally {
if (this.annotationViewportBridgeOpsByTabId.get(browserTabId) === next) {
this.annotationViewportBridgeOpsByTabId.delete(browserTabId)
}
}
}
// Why the caller resolves the guest: the same bridge serves browsing pages and workspace
// documents, which live in different halves of the page registry.
// Why a resolver and not the guest itself: this op may have waited behind another one, and a
// cross-process navigation meanwhile swaps the tab's contents without destroying the old one —
// injecting into the guest the request named would bridge a page nobody is looking at.
// Why no tab id: with teardown gone this reaches only the guest the resolver hands back, and
// taking an id it cannot act on would invite the next reader to act on it.
protected async doSetAnnotationViewportBridgeImpl(
options: BrowserAnnotationViewportBridgeOptions,
resolveGuest: () => Electron.WebContents | null
): Promise<boolean> {
// Why no teardown here: the resolver already unregisters a page whose guest died, and the only
// case it uniquely leaves is an ownership mismatch on a healthy page — where tearing down would
// cancel that page's in-flight downloads and grabs over a request that was merely misaddressed.
const guest = resolveGuest()
if (!guest || guest.isDestroyed()) {
return false
}
try {
// Why: run the scroll bridge in an isolated world so page scripts can't read the per-tab token or tamper with it.
await guest.executeJavaScriptInIsolatedWorld(
BROWSER_ANNOTATION_VIEWPORT_BRIDGE_WORLD_ID,
[{ code: buildBrowserAnnotationViewportBridgeScript(options) }],
false
)
return true
} catch {
return false
}
}
protected async doSetViewportOverrideImpl(
browserTabId: string,
override: BrowserViewportOverride | null,
expectedWebContentsId: number | undefined
): Promise<boolean> {
const webContentsId = this.webContentsIdByTabId.get(browserTabId)
if (!webContentsId || webContentsId !== expectedWebContentsId) {
return false
}
const guest = webContents.fromId(webContentsId)
if (!guest || guest.isDestroyed()) {
// Why: a stale guest must clear every per-tab registry entry, not just the WebContents maps.
this.unregisterGuest(browserTabId)
return false
}
try {
if (!guest.debugger.isAttached()) {
guest.debugger.attach('1.3')
}
} catch (err) {
// Why: attach throws if DevTools is open on the guest; log context so this failure mode is diagnosable.
console.warn('[browser-manager] setViewportOverride: failed to attach debugger', {
browserTabId,
webContentsId,
error: err instanceof Error ? err.message : String(err)
})
return false
}
const dbg = guest.debugger
try {
if (override) {
await dbg.sendCommand('Emulation.setDeviceMetricsOverride', {
width: override.width,
height: override.height,
deviceScaleFactor: override.deviceScaleFactor,
mobile: override.mobile
})
if (this.webContentsIdByTabId.get(browserTabId) === webContentsId) {
this.viewportPresetActiveByTabId.set(browserTabId, {
guestWebContentsId: webContentsId,
active: true
})
}
await dbg.sendCommand('Emulation.setTouchEmulationEnabled', {
enabled: override.mobile,
maxTouchPoints: override.mobile ? 5 : 0
})
// Why: viewport sizing must not override a profile's explicit native-UA identity.
if (this.userAgentModeByPageId.get(browserTabId) !== 'native') {
// Navigation must see the preset intent while the final CDP command is in flight.
this.viewportUaOverrideMobileByTabId.set(browserTabId, override.mobile)
// Why: same sender as the navigation path, so both resolve the tab's host identically.
await this.sendViewportUserAgentOverride(guest, override.mobile)
}
} else {
await dbg.sendCommand('Emulation.clearDeviceMetricsOverride', {})
if (this.webContentsIdByTabId.get(browserTabId) === webContentsId) {
this.viewportPresetActiveByTabId.set(browserTabId, {
guestWebContentsId: webContentsId,
active: false
})
}
await dbg.sendCommand('Emulation.setTouchEmulationEnabled', {
enabled: false,
maxTouchPoints: 0
})
const trackedMobile = this.viewportUaOverrideMobileByTabId.get(browserTabId)
// A navigation after this point must not re-install the override behind the clear.
this.viewportUaOverrideMobileByTabId.delete(browserTabId)
try {
if (this.authUserAgentOverrideStateByGuestId.has(guest.id)) {
const url = this.resolveTabNavigationUrl(guest)
const restored = await this.applyAuthUserAgentOverrideOverCdp(
guest,
false,
url,
isGoogleAuthUrl(url) ? googleAuthUserAgent() : guest.session.getUserAgent()
)
if (!restored) {
throw new Error('Failed to preserve auth user agent')
}
} else {
// Why: passing an empty string restores the session default UA.
await dbg.sendCommand('Emulation.setUserAgentOverride', { userAgent: '' })
}
} catch (error) {
if (trackedMobile !== undefined) {
this.viewportUaOverrideMobileByTabId.set(browserTabId, trackedMobile)
}
throw error
}
}
if (this.webContentsIdByTabId.get(browserTabId) !== webContentsId) {
return false
}
return true
} catch {
return false
}
}
}
@@ -0,0 +1,18 @@
diff --git a/src/main/browser/browser-manager-viewport.ts b/src/main/browser/browser-manager-viewport.ts
index ce31dbe37e..3f1fbb68fb 100644
--- a/src/main/browser/browser-manager-viewport.ts
+++ b/src/main/browser/browser-manager-viewport.ts
@@ -165,0 +166,3 @@ export abstract class BrowserManagerViewport extends BrowserManagerDownloadLifec
+ if (this.webContentsIdByTabId.get(browserTabId) !== webContentsId) {
+ return false
+ }
@@ -184,0 +188,3 @@ export abstract class BrowserManagerViewport extends BrowserManagerDownloadLifec
+ if (this.webContentsIdByTabId.get(browserTabId) !== webContentsId) {
+ return false
+ }
@@ -205 +211,4 @@ export abstract class BrowserManagerViewport extends BrowserManagerDownloadLifec
- if (trackedMobile !== undefined) {
+ if (
+ trackedMobile !== undefined &&
+ this.webContentsIdByTabId.get(browserTabId) === webContentsId
+ ) {
@@ -0,0 +1,260 @@
{
"testFiles": 4,
"total": 42,
"passed": 42,
"failed": 0,
"cases": [
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride returns false when the tab is not registered",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride applies device metrics, touch emulation, and a mobile UA for mobile presets",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride keeps the session UA for native-mode profiles when mobile=false",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride keeps the session UA for native-mode profiles when mobile=true",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride presents the Firefox UA for a preset applied on a Google auth host (mobile=false)",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride presents the Firefox UA for a preset applied on a Google auth host (mobile=true)",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride re-issues the standing UA override when navigating onto and back off an auth host",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride does not leave the Chrome preset UA standing when a mobile preset lands mid-navigation onto an auth host",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride does not leave the Firefox UA standing when a preset lands mid-navigation off an auth host",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride falls back to the committed URL once a navigation commits or fails",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride does not let a superseded navigation failure revert a newer target",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride switches identity for a server redirect and restores it if the redirect fails",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride preserves the auth identity when a viewport preset is cleared after a redirect",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride does not inherit a mobile owner UA in a desktop popup",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride reapplies a preset when navigation starts during its final UA write",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride does not reinstall a preset while its final UA clear is in flight",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride keeps tracking the standing override when the CDP clear fails",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride does not touch the UA override on navigation when no preset is standing",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride stops re-issuing the UA override once the preset is cleared",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride leaves the UA override alone on navigation for native-UA profiles",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride clears device metrics and disables touch for override=null",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride attaches the debugger if not already attached and does not detach after",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-override.test.ts",
"title": "browserManager setViewportOverride returns false when debugger.attach throws (e.g. DevTools already open)",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership does not recreate closed-tab UA intent after a late touch completion",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership does not restore closed-tab UA intent after a failed clear",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership preserves replacement desktop intent after an old clear fails",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership preserves replacement mobile intent after an old clear resumes",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership same-owner clear failure still restores the legitimate earlier intent",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership old apply cannot overwrite a replacement guest desktop intent",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership an old native profile cannot write UA intent after replacement with a default profile",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership old queued operations cannot remove or join a replacement promise tail",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership normal same-owner toggles preserve last-requested order and remove the promise tail",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership native UA mode remains unchanged with mobile=false",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership native UA mode remains unchanged with mobile=true",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-ownership.test.ts",
"title": "browser viewport operation ownership late rejected clears cannot repopulate all registries after unregisterAll",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-partial-failure.test.ts",
"title": "browserManager viewport partial failure keeps wheel routing active when follow-up setup fails after metrics apply",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-manager-viewport-partial-failure.test.ts",
"title": "browserManager viewport partial failure keeps host panning available when metrics setup fails",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-viewport-user-agent.test.ts",
"title": "buildViewportUserAgentOverride presents the Firefox UA on Google auth hosts regardless of the preset",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-viewport-user-agent.test.ts",
"title": "buildViewportUserAgentOverride keeps the clean desktop UA off the auth hosts",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-viewport-user-agent.test.ts",
"title": "buildViewportUserAgentOverride splices the real Chrome major into the mobile UA and its client hints",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-viewport-user-agent.test.ts",
"title": "buildViewportUserAgentOverride falls back to a known Chrome major when the base UA carries none",
"status": "passed",
"failures": []
},
{
"file": "src/main/browser/browser-viewport-user-agent.test.ts",
"title": "buildViewportUserAgentOverride treats an unparseable URL as a non-auth host",
"status": "passed",
"failures": []
}
]
}
@@ -0,0 +1,98 @@
{
"refs": {
"audit": "4a09b1d108cfd8b57ffcc5d727b3a3bd71ae71fb",
"main": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"v1.4.198": "e0826956fcfc532f5a1e55b5e081f2e57e553c43"
},
"canonicalLineEndings": "LF",
"sources": [
{
"path": "src/main/browser/browser-manager-viewport.ts",
"sha256": {
"audit": "701f9ea1a4310e509f409e08ee4ea0539f820bd209ff68f41001b8c91e5470e4",
"main": "701f9ea1a4310e509f409e08ee4ea0539f820bd209ff68f41001b8c91e5470e4",
"v1.4.198": "701f9ea1a4310e509f409e08ee4ea0539f820bd209ff68f41001b8c91e5470e4"
}
},
{
"path": "src/main/browser/browser-manager-registration.ts",
"sha256": {
"audit": "12c149b1fb87b67b4192b6c2f23bd7f45b7df2d0c60368182f33b5a47b56f96f",
"main": "12c149b1fb87b67b4192b6c2f23bd7f45b7df2d0c60368182f33b5a47b56f96f",
"v1.4.198": "12c149b1fb87b67b4192b6c2f23bd7f45b7df2d0c60368182f33b5a47b56f96f"
}
},
{
"path": "src/main/browser/browser-manager-navigation.ts",
"sha256": {
"audit": "a4c0e169ac35725b02d050c95843721e4274775a5bcb9ef2a5b7c835c4d8c234",
"main": "a4c0e169ac35725b02d050c95843721e4274775a5bcb9ef2a5b7c835c4d8c234",
"v1.4.198": "c93c060896351b4bc23db628a732ef4db4acd5b26760e4565e6bf029d5cf7531"
}
},
{
"path": "src/main/browser/browser-manager-guest-policy.ts",
"sha256": {
"audit": "6d567b0c675091ec0a59d39ffe701fd36098ec2730b61ef0b80ab53962da6144",
"main": "6d567b0c675091ec0a59d39ffe701fd36098ec2730b61ef0b80ab53962da6144",
"v1.4.198": "6d567b0c675091ec0a59d39ffe701fd36098ec2730b61ef0b80ab53962da6144"
}
},
{
"path": "src/main/browser/browser-manager-state.ts",
"sha256": {
"audit": "ea6e847afe227b9acbf432d68a6f21f0d15247748fc8b4f769ab73498d6a13c5",
"main": "ea6e847afe227b9acbf432d68a6f21f0d15247748fc8b4f769ab73498d6a13c5",
"v1.4.198": "ea6e847afe227b9acbf432d68a6f21f0d15247748fc8b4f769ab73498d6a13c5"
}
},
{
"path": "src/main/browser/browser-manager-types.ts",
"sha256": {
"audit": "4d28f7c397aa82b067971ff769a44fff74adadceaac66774345a615f799bb64f",
"main": "4d28f7c397aa82b067971ff769a44fff74adadceaac66774345a615f799bb64f",
"v1.4.198": "4d28f7c397aa82b067971ff769a44fff74adadceaac66774345a615f799bb64f"
}
},
{
"path": "src/main/browser/browser-manager-viewport-test-fixtures.ts",
"sha256": {
"audit": "0ef61054ae131db056b5268c6ed4f417b4486784d58d2c0d7d2285ad785666ee",
"main": "0ef61054ae131db056b5268c6ed4f417b4486784d58d2c0d7d2285ad785666ee",
"v1.4.198": "36d29f1d78bd559af3b235acc9be8dafe763e3dfb9ea4367272db04449eca707"
}
},
{
"path": "src/main/browser/browser-manager-test-harness.ts",
"sha256": {
"audit": "66de235be9285ec2cd2a6d783c8e8a046d6106d4171aaf808b5d263c08a72038",
"main": "66de235be9285ec2cd2a6d783c8e8a046d6106d4171aaf808b5d263c08a72038",
"v1.4.198": "66de235be9285ec2cd2a6d783c8e8a046d6106d4171aaf808b5d263c08a72038"
}
},
{
"path": "src/main/ipc/browser-guest-view-ipc.ts",
"sha256": {
"audit": "da4a441ce5f0851f1a1c6ec38c872c187ec191a9acad340f05ac557534731959",
"main": "da4a441ce5f0851f1a1c6ec38c872c187ec191a9acad340f05ac557534731959",
"v1.4.198": "da4a441ce5f0851f1a1c6ec38c872c187ec191a9acad340f05ac557534731959"
}
},
{
"path": "src/renderer/src/components/browser-pane/host-guest/browser-page-webview-guest-session.ts",
"sha256": {
"audit": "a78cc98873e93834bf07b47bbf5d92da888dfbccef06551aa6ac3c8f6e9f29f8",
"main": "2fe17f0fe4f8ef6ca7cff7ca5731d9d725dbf4b5e7944264e30f12241317fc6d",
"v1.4.198": "6c089c0c8285b21b3f3c0b3e06bd297a25d941cfa8a4849d03fbd08a6c89c139"
}
},
{
"path": "src/renderer/src/components/browser-pane/assemble-chrome/BrowserToolbarMenu.tsx",
"sha256": {
"audit": "e05e7d8d532d2f43f13a8dc1035540638ab513c9f3d6bdbd1531cf3e0e9462e1",
"main": "e05e7d8d532d2f43f13a8dc1035540638ab513c9f3d6bdbd1531cf3e0e9462e1",
"v1.4.198": "e05e7d8d532d2f43f13a8dc1035540638ab513c9f3d6bdbd1531cf3e0e9462e1"
}
}
]
}
@@ -0,0 +1,36 @@
{
"scope": "Actual manager and lifecycle; controlled Electron/CDP ports; no native guest, heap/RSS or incident attribution",
"baseline": {
"total": 12,
"failedOwnershipCases": 7,
"passedControls": 5
},
"fixed": {
"total": 42,
"passed": 42,
"testFiles": 4
},
"independentReview": {
"candidateTestsPassed": 12,
"findings": "No blocker; three map mutation guards preserve current guest and promise ownership"
},
"typecheck": {
"node": "passed after correcting fixture-only protected-map reads and array typing",
"cli": "passed",
"web": "passed"
},
"quality": {
"fullFileScans": 5,
"codeFiles": 3,
"newDiagnostics": 0
},
"sourceSha256": {
"src/main/browser/browser-manager-viewport.ts": "a839fd89cc5e687782323036ca3a8dd9de79bd838e77dd863a5e1ae101424b4c",
"src/main/browser/browser-manager-viewport-ownership.test.ts": "ec17f2acd6b766166d13cafc2ecd33f1d4f0de7a4b4e8896d0746b2d528341da"
},
"limitations": [
"Pending CDP response schedules are injected, not an affected-host capture",
"Retired map values are booleans; native objects and process RSS were not measured",
"Historical viewport source is exact; surrounding dependencies execute current audit versions"
]
}
@@ -0,0 +1,36 @@
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import base from '../../../config/vitest.config.ts'
if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') {
throw new Error('Set ORCA_BACKGROUND_LAUNCH=1 for the viewport ownership replay')
}
const target = fileURLToPath(
new URL('../../../src/main/browser/browser-manager-viewport.ts', import.meta.url)
).replaceAll('\\', '/')
export default {
...base,
test: {
...base.test,
include: ['src/main/browser/browser-manager-viewport-ownership.test.ts']
},
plugins:
process.env.ORCA_VIEWPORT_BASELINE === '1'
? [
{
name: 'viewport-owner-baseline',
enforce: 'pre',
transform(_source, id) {
return id.replaceAll('\\', '/').split('?')[0] === target
? {
code: readFileSync(new URL('./baseline-source.txt', import.meta.url), 'utf8'),
map: null
}
: null
}
}
]
: []
}
@@ -0,0 +1,64 @@
# Retained Claude background-task text
The actual Claude task tracker retained oversized input strings through its
512-character description/name slices. Its live tasks, settled tasks, and
recently removed tasks can each retain those slices. The fix uses the existing
`ownRetainedString` at the shared text boundary; normalization, UTF-16 clipping,
task identity, publication, and lifecycle behavior stay the same.
This extends [ML-018 / #20960](https://github.com/stablyai/orca/pull/20960).
## Reproduce
```sh
ORCA_BACKGROUND_LAUNCH=1 node --expose-gc docs/audits/claude-task-retention/reproduce.cjs
```
The script bundles the actual tracker and its retention classes. Its baseline
removes only the new copy call in memory. It exercises flat strings, concatenated
strings, and JSON-parsed SDK-style frames; each input has a distinct task owner.
It measures after GC, then clears the tracker and yields before measuring cleanup.
[Results and bundle hashes](./results.json) preserve the complete run.
| JSON-parsed case | Input per task | Tasks | Visible text | Heap before | Heap after |
| ------------------------- | ---------------: | ----: | ----------------: | ----------: | ---------: |
| Live | 64 Ki characters | 32 | 16,384 characters | 2,125,672 | 43,536 |
| Settled | 64 Ki characters | 32 | 16,384 characters | 2,127,072 | 44,296 |
| Removed, awaiting outcome | 64 Ki characters | 32 | 0 characters | 2,108,136 | 25,360 |
| Live | 4 Mi characters | 8 | 4,096 characters | 33,562,624 | 11,048 |
| Settled | 4 Mi characters | 8 | 4,096 characters | 33,563,960 | 11,656 |
| Removed, awaiting outcome | 4 Mi characters | 8 | 0 characters | 33,558,584 | 7,008 |
Captured with Node v26.6.0 on macOS. Cleanup returned near the initial heap for
every case. Six regression tests retain the actual tracker through these three
lifetimes for both descriptions and names. Text behavior tests preserve whitespace
normalization, fallback names, and a clipped surrogate pair.
## Reachability and limits
`claude-stream-json-connection.ts` forwards SDK messages to the structured adapter,
whose `emit` calls `backgroundTasks.observe`. Installed SDK 0.3.251 uses Node
`readline` to assemble stdout records, parses each record with `JSON.parse`, then
yields it. The inspected path imposes no record or description length limit;
native read-chunk size does not cap an assembled JSON field. Descriptions are
declared as plain strings in `SDKTaskStartedMessage`.
The description slice and this SDK version also exist in `v1.4.198`; that tag
keeps the reader inline in `claude-background-task-tracker.ts`. The separate
settled/recently-removed retention and name-reader paths describe current code.
The current maps are count-bounded: at most 256 live, 256 settled, and 256 recently
removed entries per tracker. Settled context clears when no visible work remains;
recently removed context awaits an outcome, eviction, or explicit clearing.
Session end/close clears the tracker. Copy work is at most 512 UTF-16 code units
per retained field, and it does not reduce temporary parsing allocation.
These are synthetic oversized task fields, not evidence that an affected user
received such fields. The path concerns structured Claude sessions, not ordinary
terminal output or stderr. Neither #19831 nor #19768 establishes this trigger.
The separate digest-bounded subagent ID was also checked at actual consumers.
The mobile response sanitizer can temporarily retain the original until JSON
serialization flattens its concatenated ID. Worker transcript bounding already
serializes for its byte budget and released that parent in the probe. No durable
ID-owner leak was established, so that helper is unchanged.
@@ -0,0 +1,197 @@
const fs = require('node:fs')
const { build } = require('esbuild')
const assert = require('node:assert/strict')
const path = require('node:path')
const { tmpdir } = require('node:os')
const { createHash } = require('node:crypto')
const root = path.resolve(__dirname, '../../..')
const bundles = {}
assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1')
assert.equal(typeof global.gc, 'function')
async function loadTracker(fixed) {
const result = await build({
entryPoints: [path.join(root, 'src/main/claude/claude-background-task-tracker.ts')],
bundle: true,
write: false,
platform: 'node',
format: 'cjs',
target: 'node22',
plugins: fixed
? []
: [
{
name: 'baseline-without-task-text-copy',
setup(builder) {
builder.onLoad({ filter: /claude-background-task-frames\.ts$/ }, (args) => {
const source = fs.readFileSync(args.path, 'utf8')
const boundary = 'ownRetainedString(trimmed.slice(0, MAX_TASK_TEXT_LENGTH))'
assert.ok(
source.includes(boundary),
'The copy boundary changed; update the baseline transform'
)
return {
loader: 'ts',
contents: source.replace(boundary, 'trimmed.slice(0, MAX_TASK_TEXT_LENGTH)')
}
})
}
}
]
})
bundles[fixed ? 'after' : 'before'] = createHash('sha256')
.update(result.outputFiles[0].text)
.digest('hex')
const scratch = fs.mkdtempSync(path.join(tmpdir(), 'orca-claude-task-proof-'))
let moduleId
try {
const bundlePath = path.join(scratch, 'tracker.cjs')
fs.writeFileSync(bundlePath, result.outputFiles[0].text)
moduleId = require.resolve(bundlePath)
return require(moduleId).ClaudeBackgroundTaskTracker
} finally {
if (moduleId) {
delete require.cache[moduleId]
}
fs.rmSync(scratch, { recursive: true, force: true })
}
}
function collect() {
for (let i = 0; i < 5; i++) {
global.gc()
}
return process.memoryUsage().heapUsed
}
const settle = () => new Promise((resolve) => setImmediate(resolve))
function frame(index, size, ingress, field) {
const value = String.fromCharCode(65 + (index % 26)).repeat(size)
const message = {
type: 'system',
subtype: 'task_started',
task_id: `task-${index}`,
task_type: 'local_bash',
is_backgrounded: true,
[field]: value
}
if (ingress === 'json') {
return JSON.parse(JSON.stringify(message))
}
if (ingress === 'flat') {
value.charCodeAt(value.length - 1)
}
return message
}
function populate(Tracker, { count, size, ingress, retention, field }) {
const owner = new Tracker()
const keeper = {
type: 'system',
subtype: 'task_started',
task_id: 'keeper',
task_type: 'local_bash',
is_backgrounded: true
}
if (retention !== 'live') {
owner.observe(keeper)
}
for (let index = 0; index < count; index++) {
owner.observe(frame(index, size, ingress, field))
if (retention === 'settled') {
owner.observe({
type: 'system',
subtype: 'task_notification',
task_id: `task-${index}`,
status: 'completed'
})
}
}
if (retention === 'removed') {
owner.observe({ type: 'system', subtype: 'background_tasks_changed', tasks: [keeper] })
}
return owner
}
function logicalChars(owner) {
const state = owner.state
return [...(state?.tasks ?? []), ...(state?.settledTasks ?? [])].reduce(
(sum, task) => sum + (task.description?.length ?? 0) + (task.name?.length ?? 0),
0
)
}
async function main() {
const Before = await loadTracker(false)
const Fixed = await loadTracker(true)
for (const Tracker of [Before, Fixed]) {
const warm = populate(Tracker, {
count: 1,
size: 1024,
ingress: 'json',
retention: 'live',
field: 'description'
})
warm.clear()
}
const results = []
for (const [count, size] of [
[32, 64 * 1024],
[8, 4 * 1024 * 1024]
]) {
for (const ingress of ['flat', 'cons', 'json']) {
for (const retention of ['live', 'settled', 'removed']) {
for (const [phase, Tracker] of [
['before', Before],
['after', Fixed]
]) {
await settle()
const baseline = collect()
global.auditTaskOwner = populate(Tracker, {
count,
size,
ingress,
retention,
field: 'description'
})
await settle()
const retainedHeapBytes = collect() - baseline
const visibleTextChars = logicalChars(global.auditTaskOwner)
global.auditTaskOwner.clear()
global.auditTaskOwner = null
await settle()
const afterClearHeapBytes = collect() - baseline
if (phase === 'after') {
assert.ok(retainedHeapBytes < 1024 * 1024, 'A bounded task retained its parent frame')
} else {
assert.ok(
retainedHeapBytes > count * size * 0.75,
'Baseline no longer reproduces retention'
)
}
assert.ok(afterClearHeapBytes < 1024 * 1024, 'Tracker cleanup retained the fixture')
results.push({
count,
size,
ingress,
retention,
phase,
visibleTextChars,
retainedHeapBytes,
afterClearHeapBytes
})
}
}
}
}
console.log(
JSON.stringify({ node: process.version, platform: process.platform, bundles, results }, null, 2)
)
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
@@ -0,0 +1,370 @@
{
"node": "v26.6.0",
"platform": "darwin",
"bundles": {
"before": "95425d0894ed107d85a671238f0229e6db3e229c0fa94286d1bea1a52db7112f",
"after": "e1e5f58ddba3aeea88922be927509754b766247b81b6dcf7675b49a25c3a8d29"
},
"results": [
{
"count": 32,
"size": 65536,
"ingress": "flat",
"retention": "live",
"phase": "before",
"visibleTextChars": 16384,
"retainedHeapBytes": 2159976,
"afterClearHeapBytes": 32392
},
{
"count": 32,
"size": 65536,
"ingress": "flat",
"retention": "live",
"phase": "after",
"visibleTextChars": 16384,
"retainedHeapBytes": 97680,
"afterClearHeapBytes": 51600
},
{
"count": 32,
"size": 65536,
"ingress": "flat",
"retention": "settled",
"phase": "before",
"visibleTextChars": 16384,
"retainedHeapBytes": 2140416,
"afterClearHeapBytes": 15320
},
{
"count": 32,
"size": 65536,
"ingress": "flat",
"retention": "settled",
"phase": "after",
"visibleTextChars": 16384,
"retainedHeapBytes": 55816,
"afterClearHeapBytes": 12976
},
{
"count": 32,
"size": 65536,
"ingress": "flat",
"retention": "removed",
"phase": "before",
"visibleTextChars": 0,
"retainedHeapBytes": 2112656,
"afterClearHeapBytes": 5200
},
{
"count": 32,
"size": 65536,
"ingress": "flat",
"retention": "removed",
"phase": "after",
"visibleTextChars": 0,
"retainedHeapBytes": 36568,
"afterClearHeapBytes": 11832
},
{
"count": 32,
"size": 65536,
"ingress": "cons",
"retention": "live",
"phase": "before",
"visibleTextChars": 16384,
"retainedHeapBytes": 2125456,
"afterClearHeapBytes": -304
},
{
"count": 32,
"size": 65536,
"ingress": "cons",
"retention": "live",
"phase": "after",
"visibleTextChars": 16384,
"retainedHeapBytes": 42736,
"afterClearHeapBytes": -304
},
{
"count": 32,
"size": 65536,
"ingress": "cons",
"retention": "settled",
"phase": "before",
"visibleTextChars": 16384,
"retainedHeapBytes": 2127096,
"afterClearHeapBytes": 464
},
{
"count": 32,
"size": 65536,
"ingress": "cons",
"retention": "settled",
"phase": "after",
"visibleTextChars": 16384,
"retainedHeapBytes": 43896,
"afterClearHeapBytes": 368
},
{
"count": 32,
"size": 65536,
"ingress": "cons",
"retention": "removed",
"phase": "before",
"visibleTextChars": 0,
"retainedHeapBytes": 2123696,
"afterClearHeapBytes": 16216
},
{
"count": 32,
"size": 65536,
"ingress": "cons",
"retention": "removed",
"phase": "after",
"visibleTextChars": 0,
"retainedHeapBytes": 39672,
"afterClearHeapBytes": 15064
},
{
"count": 32,
"size": 65536,
"ingress": "json",
"retention": "live",
"phase": "before",
"visibleTextChars": 16384,
"retainedHeapBytes": 2125672,
"afterClearHeapBytes": -32
},
{
"count": 32,
"size": 65536,
"ingress": "json",
"retention": "live",
"phase": "after",
"visibleTextChars": 16384,
"retainedHeapBytes": 43536,
"afterClearHeapBytes": 544
},
{
"count": 32,
"size": 65536,
"ingress": "json",
"retention": "settled",
"phase": "before",
"visibleTextChars": 16384,
"retainedHeapBytes": 2127072,
"afterClearHeapBytes": 1424
},
{
"count": 32,
"size": 65536,
"ingress": "json",
"retention": "settled",
"phase": "after",
"visibleTextChars": 16384,
"retainedHeapBytes": 44296,
"afterClearHeapBytes": 1248
},
{
"count": 32,
"size": 65536,
"ingress": "json",
"retention": "removed",
"phase": "before",
"visibleTextChars": 0,
"retainedHeapBytes": 2108136,
"afterClearHeapBytes": 1072
},
{
"count": 32,
"size": 65536,
"ingress": "json",
"retention": "removed",
"phase": "after",
"visibleTextChars": 0,
"retainedHeapBytes": 25360,
"afterClearHeapBytes": 8832
},
{
"count": 8,
"size": 4194304,
"ingress": "flat",
"retention": "live",
"phase": "before",
"visibleTextChars": 4096,
"retainedHeapBytes": 33562624,
"afterClearHeapBytes": -320
},
{
"count": 8,
"size": 4194304,
"ingress": "flat",
"retention": "live",
"phase": "after",
"visibleTextChars": 4096,
"retainedHeapBytes": 11048,
"afterClearHeapBytes": -320
},
{
"count": 8,
"size": 4194304,
"ingress": "flat",
"retention": "settled",
"phase": "before",
"visibleTextChars": 4096,
"retainedHeapBytes": 33563960,
"afterClearHeapBytes": 912
},
{
"count": 8,
"size": 4194304,
"ingress": "flat",
"retention": "settled",
"phase": "after",
"visibleTextChars": 4096,
"retainedHeapBytes": 12384,
"afterClearHeapBytes": 464
},
{
"count": 8,
"size": 4194304,
"ingress": "flat",
"retention": "removed",
"phase": "before",
"visibleTextChars": 0,
"retainedHeapBytes": 33559744,
"afterClearHeapBytes": 1112
},
{
"count": 8,
"size": 4194304,
"ingress": "flat",
"retention": "removed",
"phase": "after",
"visibleTextChars": 0,
"retainedHeapBytes": 7512,
"afterClearHeapBytes": 552
},
{
"count": 8,
"size": 4194304,
"ingress": "cons",
"retention": "live",
"phase": "before",
"visibleTextChars": 4096,
"retainedHeapBytes": 33562624,
"afterClearHeapBytes": -384
},
{
"count": 8,
"size": 4194304,
"ingress": "cons",
"retention": "live",
"phase": "after",
"visibleTextChars": 4096,
"retainedHeapBytes": 11048,
"afterClearHeapBytes": -384
},
{
"count": 8,
"size": 4194304,
"ingress": "cons",
"retention": "settled",
"phase": "before",
"visibleTextChars": 4096,
"retainedHeapBytes": 33563960,
"afterClearHeapBytes": 784
},
{
"count": 8,
"size": 4194304,
"ingress": "cons",
"retention": "settled",
"phase": "after",
"visibleTextChars": 4096,
"retainedHeapBytes": 12384,
"afterClearHeapBytes": 416
},
{
"count": 8,
"size": 4194304,
"ingress": "cons",
"retention": "removed",
"phase": "before",
"visibleTextChars": 0,
"retainedHeapBytes": 32432128,
"afterClearHeapBytes": -1126504
},
{
"count": 8,
"size": 4194304,
"ingress": "cons",
"retention": "removed",
"phase": "after",
"visibleTextChars": 0,
"retainedHeapBytes": 7008,
"afterClearHeapBytes": 48
},
{
"count": 8,
"size": 4194304,
"ingress": "json",
"retention": "live",
"phase": "before",
"visibleTextChars": 4096,
"retainedHeapBytes": 33562624,
"afterClearHeapBytes": -384
},
{
"count": 8,
"size": 4194304,
"ingress": "json",
"retention": "live",
"phase": "after",
"visibleTextChars": 4096,
"retainedHeapBytes": 11048,
"afterClearHeapBytes": -384
},
{
"count": 8,
"size": 4194304,
"ingress": "json",
"retention": "settled",
"phase": "before",
"visibleTextChars": 4096,
"retainedHeapBytes": 33563960,
"afterClearHeapBytes": 432
},
{
"count": 8,
"size": 4194304,
"ingress": "json",
"retention": "settled",
"phase": "after",
"visibleTextChars": 4096,
"retainedHeapBytes": 11656,
"afterClearHeapBytes": -392
},
{
"count": 8,
"size": 4194304,
"ingress": "json",
"retention": "removed",
"phase": "before",
"visibleTextChars": 0,
"retainedHeapBytes": 33558584,
"afterClearHeapBytes": -48
},
{
"count": 8,
"size": 4194304,
"ingress": "json",
"retention": "removed",
"phase": "after",
"visibleTextChars": 0,
"retainedHeapBytes": 7008,
"afterClearHeapBytes": 48
}
]
}
@@ -0,0 +1,53 @@
# Codex prompt claims retained after turn completion
Confirmed cancellation keeps a prompt claim until its turn completes. If the prompt's lookup entries are evicted or replaced first, the old `clearTurn()` cannot find it. The separate claims map then retains the prompt until the whole session is cleared.
The fix includes claimed prompts in the existing exact-thread/turn cleanup. Existing turn matching and `forget()` object-identity checks preserve a replacement prompt's authority. Registry limits and cancellation timing are unchanged.
## Source ownership and reachability
1. `codex-structured-provider-events.ts:57` registers incoming prompt requests and publishes them through the translator. `codex-structured-session-acquire.ts:95` binds the translator's turn cleanup to the session registry.
2. `codex-structured-prompt-ownership.ts:33` acquires the claim. Confirmed cancellation deliberately leaves it owned; unsuccessful/unconfirmed cancellation releases it. The actual `CodexStructuredTurnCancellation` invokes the confirmation callback after the injected interrupt transport acknowledges success.
3. `codex-prompt-registry.ts:258` trims the address and journal-binding maps independently. Neither trim removes claims. Replacing the same journal address can similarly leave the old claim without a lookup entry.
4. A later `turn/completed` goes through `translateCodexNotification`, the journal translator and `settleCodexJournalTurn`. Accepted lifecycle settlement invokes `clearPromptTurn` at `codex-structured-journal-settlement.ts:170`.
5. The old cleanup enumerates only address/binding values. The fix also enumerates `claims.keys()`, still filtering by the exact thread/turn. `forget()` deletes replacement lookup entries only when they contain that same prompt object.
The safely expired owner is the claim for the terminal turn whose cleanup has been admitted. Live claims survive unrelated turn cleanup and registry eviction. A refused lifecycle settlement does not clear them.
## Reproduce
From the worktree root, using installed dependencies:
```sh
ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/codex-prompt-claim-retention/reproduce.cjs
```
For Electron, use its installed executable with `ELECTRON_RUN_AS_NODE=1`, the same flags and script. The final optional argument selects the report path; the default is `node-results.json` beside the script. No Electron window is created.
`sources.cjs` reverses `fix.patch` against current source and rejects a baseline hash mismatch. It neither reads a previous commit to reconstruct the implementation nor changes product files. The proof bundles actual source in memory. Each report records effective source and bundle hashes, dependency hashes and runtime versions. Only the requested report is written.
The fixture uses the actual registry, server-request delivery, cancellation ownership function, cancellation class, journal translator and delayed notification delivery. It injects an accepting journal sink, interrupt transport and primary-turn lookup. Prompts belong to child threads, so the production child-turn cancellation branch does not enumerate or terminate processes. Every injected process helper throws if unexpectedly called.
The sequence creates 32 ordinary small prompt objects, confirms their cancellations, admits 256 unrelated prompts to evict lookup entries, then completes the original exact turns. WeakRefs count prompt liveness after forced collections. No large payload is attached. The 20-second deadline and 192 MiB heap limit bound the proof.
## Results
Both [Node 26.6.0](./node-results.json) and [Electron 43.7.0 / Node 24.21.0](./electron-results.json) produced:
| Observation | Before | After |
| -------------------------------------------------------------------------- | -----: | ----: |
| Retained cancelled prompts after lookup eviction, before completion | 32 | 32 |
| Retained after exact turn completion | 32 | 0 |
| Retained after all lookup maps become empty | 32 | 0 |
| Retained after session clear | 0 | 0 |
| Old prompt retained after same-address replacement and old-turn completion | 1 | 0 |
Ordinary completion releases its prompt on both versions. Wrong-thread, wrong-turn and refused-completion controls preserve claims. The replacement prompt and its active claim remain valid after old-turn cleanup on both versions.
The four regression tests cover 32 evicted claims, replacement authority, a compatibility turn digest and session cleanup. Applying the reversed source produces three expected failures; the session-clear control passes. Existing prompt ownership/reply tests also pass on the reversed source. Current source passes 71 tests across six files plus Node, CLI and Web typechecks; [validation.json](./validation.json) records commands and other checks.
## Limits
This is a code-level lifetime defect. The request/completion ordering is deliberately injected; this is not a capture of Codex emitting that sequence or an affected host. Counts do not measure ordinary prompt bytes or establish a growth rate. It does not identify the cause of #19831 or any other incident.
[source-versions.json](./source-versions.json) records matching baseline source at the named main revision. No historical application runtime was reproduced. The fix uses existing turn ownership and identity checks; it adds no arbitrary eviction policy.
@@ -0,0 +1,24 @@
import { resolve } from 'node:path'
import { createRequire } from 'node:module'
import { defineConfig, mergeConfig } from 'vitest/config'
import baseConfig from '../../../config/vitest.config.ts'
const loadSources = createRequire(import.meta.url)(
resolve('docs/audits/codex-prompt-claim-retention/sources.cjs')
)
const { before } = loadSources()
export default mergeConfig(
baseConfig,
defineConfig({
plugins: [
{
name: 'codex-claim-before-fix',
enforce: 'pre',
transform(_code, id) {
const source = before.get(resolve(id.split('?')[0]))
return source === undefined ? undefined : { code: source, map: null }
}
}
]
})
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
--- a/src/main/codex/codex-prompt-registry.ts
+++ b/src/main/codex/codex-prompt-registry.ts
@@ -226,7 +226,7 @@
clearTurn(threadId: string, turnId: string): void {
const prompts = new Set(
- [...this.byAddress.values(), ...this.boundPrompts.values()].filter(
+ [...this.byAddress.values(), ...this.boundPrompts.values(), ...this.claims.keys()].filter(
(prompt) => prompt.threadId === threadId && codexPromptMatchesTurn(prompt, turnId)
)
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,111 @@
const assert = require('node:assert/strict')
const { createHash } = require('node:crypto')
const { readFileSync, writeFileSync } = require('node:fs')
const { resolve, relative } = require('node:path')
const esbuild = require('esbuild')
const Module = require('node:module')
assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1')
assert.equal(typeof global.gc, 'function')
const { root, before, after, hashes } = require('./sources.cjs')()
const sourcePath = 'src/main/codex/codex-prompt-registry.ts'
const source = before.get(resolve(root, sourcePath))
const candidate = after.get(resolve(root, sourcePath))
const hash = (value) => createHash('sha256').update(value).digest('hex')
const entry = `
export { CodexPromptRegistry } from './src/main/codex/codex-prompt-registry';
export { cancelCodexStructuredTurn } from './src/main/codex/codex-structured-prompt-ownership';
export { CodexStructuredTurnCancellation } from './src/main/codex/codex-structured-turn-cancellation';
export { createCodexJournalTranslator } from './src/main/codex/codex-structured-journal-translation';
export { deliverCodexServerRequest, translateCodexNotification } from './src/main/codex/codex-structured-provider-events';
`
async function build(mode) {
const result = await esbuild.build({
stdin: {
contents: entry,
resolveDir: root,
loader: 'ts',
sourcefile: 'codex-claim-proof-entry.ts'
},
absWorkingDir: root,
bundle: true,
platform: 'node',
format: 'cjs',
packages: 'external',
write: false,
metafile: true,
logLevel: 'silent',
plugins: [
{
name: 'candidate-only-in-memory',
setup(build) {
build.onLoad({ filter: /\/codex-prompt-registry\.ts$/ }, (args) => {
assert.equal(args.path, resolve(root, sourcePath))
return { contents: mode === 'candidate' ? candidate : source, loader: 'ts' }
})
}
}
]
})
const bundlePath = resolve(root, `codex-claim-${mode}-proof.cjs`)
const loaded = new Module(bundlePath, module)
loaded.filename = bundlePath
loaded.paths = Module._nodeModulePaths(root)
loaded._compile(result.outputFiles[0].text, bundlePath)
const dependencies = Object.keys(result.metafile.inputs)
.filter((path) => path.startsWith('src/'))
.map((path) => ({
path,
sha256: hash(
path === sourcePath
? mode === 'original'
? source
: candidate
: readFileSync(resolve(root, path))
)
}))
return { api: loaded.exports, bundleSha256: hash(result.outputFiles[0].contents), dependencies }
}
const run = require('./scenario.cjs')
async function main() {
const deadline = setTimeout(() => {
process.stderr.write('proof deadline\n')
process.exit(2)
}, 20_000)
const results = {}
const versions = {}
for (const mode of ['original', 'candidate']) {
const built = await build(mode)
results[mode] = await run(built.api, mode)
versions[mode] = { bundleSha256: built.bundleSha256, dependencies: built.dependencies }
}
clearTimeout(deadline)
const report = {
capturedAt: new Date().toISOString(),
runtime: process.versions,
scope:
'Actual registry, server-request translation, cancellation ownership/cancellation class, journal translator, delayed turn completion. Injected accepted sink, interrupt transport, and compaction lookup. No provider, process enumeration/termination, or host data. Bundles load in memory; only the requested report is written.',
sourceHashes: hashes,
countsOnly: true,
noPayloadAmplification: true,
results,
versions
}
const output = process.argv[2] ?? resolve(__dirname, 'node-results.json')
writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`)
process.stdout.write(
`${JSON.stringify(
{ output: relative(root, output), sourceHashes: report.sourceHashes, results },
null,
2
)}\n`
)
}
main().catch((error) => {
process.stderr.write(`${error.stack}\n`)
process.exit(1)
})
@@ -0,0 +1,215 @@
const assert = require('node:assert/strict')
const admitted = () => ({ accepted: true })
function fixture(api) {
const prompts = new api.CodexPromptRegistry()
const state = { prompts, requestCount: 0, lastBinding: null, blockCompletion: false }
const sink = {
appendItem() {},
appendTombstone() {},
publish() {},
tryAppendItem: admitted,
tryAppendTombstone: admitted,
tryAppendLifecycleBatch: (id) =>
state.blockCompletion && id.startsWith('turn-completed:')
? { accepted: false, reason: 'backpressure' }
: admitted(),
tryPublish: admitted
}
const translator = api.createCodexJournalTranslator({
sink,
sessionId: 'session',
primaryThreadId: () => 'primary',
bindPromptItemId: (id, thread, promptKey, turn) => {
prompts.bindJournalItemId(id, thread, promptKey, turn)
state.lastBinding = id
},
clearPromptTurn: (thread, turn) => prompts.clearTurn(thread, turn)
})
const session = {
threadId: 'primary',
prompts,
translator,
fence: 7,
acquisitionGeneration: 'generation',
ended: false,
connection: {
request: async (method) => {
assert.equal(method, 'turn/interrupt')
state.requestCount++
return {}
},
respondWithError() {
throw new Error('unexpected server refusal')
},
respond() {
throw new Error('unexpected prompt response')
}
}
}
const emit = (_session, event) => translator.handle(event)
const cancellation = new api.CodexStructuredTurnCancellation({
emit,
captureTurnProcesses: async () => {
throw new Error('no process enumeration allowed')
},
terminateTurnProcesses: async () => {
throw new Error('no process termination allowed')
}
})
cancellation.register(session)
return Object.assign(state, {
api,
session,
translator,
cancellation,
emit,
sessions: new Map([['session', session]]),
compactions: { providerTurnId: () => 'primary-turn' }
})
}
function register(
state,
serial,
thread = `child-${serial}`,
turn = `turn-${serial}`,
item = `item-${serial}`
) {
state.lastBinding = null
const admission = state.api.deliverCodexServerRequest(
'session',
state.session,
{
id: serial,
method: 'item/commandExecution/requestApproval',
params: { threadId: thread, turnId: turn, itemId: item, command: 'echo bounded-proof' }
},
state.emit
)
assert.equal(admission.accepted, true)
assert.equal(typeof state.lastBinding, 'string')
const prompt = state.prompts.find(state.lastBinding)
assert.ok(prompt)
return { ref: new WeakRef(prompt), id: state.lastBinding, thread, turn }
}
async function cancel(state, record) {
const result = await state.api.cancelCodexStructuredTurn({
sessions: state.sessions,
compactions: state.compactions,
cancellation: state.cancellation,
request: {
sessionId: 'session',
turnId: 'primary-turn',
fence: 7,
prompt: { itemId: record.id, kind: 'approval' }
}
})
assert.equal(result.cancelled, true)
}
function complete(state, thread, turn, expectedAccepted = true) {
const admission = state.api.translateCodexNotification({
sessionId: 'session',
session: state.session,
method: 'turn/completed',
params: { threadId: thread, turn: { id: turn, status: 'interrupted' } },
turnCancellation: state.cancellation,
emit: state.emit
})
assert.equal(admission.accepted, expectedAccepted)
}
async function alive(records) {
for (let round = 0; round < 8; round++) {
await new Promise(setImmediate)
global.gc()
}
return records.filter((record) => record.ref.deref() !== undefined).length
}
async function run(api, mode) {
const state = fixture(api)
const ordinary = register(state, 1)
await cancel(state, ordinary)
assert.equal(await alive([ordinary]), 1)
complete(state, ordinary.thread, ordinary.turn)
const ordinaryAfterCompletion = await alive([ordinary])
assert.equal(ordinaryAfterCompletion, 0)
const records = []
for (let index = 0; index < 32; index++) {
const record = register(state, index + 10)
await cancel(state, record)
records.push(record)
}
assert.equal(await alive(records), 32)
// Unrelated child traffic evicts old binding/address entries without ending their turns.
for (let index = 0; index < 256; index++) {
register(state, index + 1000, 'other-child', 'other-turn')
}
const sizesAfterEviction = state.prompts.sizes
for (const record of records) {
assert.equal(state.prompts.find(record.id), null)
}
const afterEvictionBeforeCompletion = await alive(records)
assert.equal(afterEvictionBeforeCompletion, 32)
complete(state, 'wrong-child', records[0].turn)
complete(state, records[0].thread, 'wrong-turn')
assert.equal(await alive(records), 32)
register(state, 5000, records[0].thread, records[0].turn)
state.blockCompletion = true
complete(state, records[0].thread, records[0].turn, false)
assert.equal(await alive(records), 32)
state.blockCompletion = false
for (const record of records) {
complete(state, record.thread, record.turn)
}
const afterExactTurnCompletion = await alive(records)
assert.equal(afterExactTurnCompletion, mode === 'original' ? 32 : 0)
complete(state, 'other-child', 'other-turn')
assert.deepEqual(state.prompts.sizes, { prompts: 0, journalBindings: 0 })
const afterAllLookupMapsEmpty = await alive(records)
assert.equal(afterAllLookupMapsEmpty, mode === 'original' ? 32 : 0)
state.prompts.clear()
const afterSessionClear = await alive(records)
assert.equal(afterSessionClear, 0)
// Replacing a journal address must not let old-turn completion clear the new prompt/claim.
const old = register(state, 2000, 'reuse-child', 'old-turn', 'reused-item')
await cancel(state, old)
const newer = register(state, 2001, 'reuse-child', 'new-turn', 'reused-item')
assert.equal(newer.id, old.id)
const replacementClaim = state.prompts.claimBound(newer.id)
assert.ok(replacementClaim)
complete(state, old.thread, old.turn)
assert.equal(
state.prompts.ownsBoundClaim(replacementClaim, newer.id, newer.thread, newer.turn),
true
)
const oldAfterReplacementCompletion = await alive([old])
assert.equal(oldAfterReplacementCompletion, mode === 'original' ? 1 : 0)
state.prompts.releaseClaim(replacementClaim)
complete(state, newer.thread, newer.turn)
state.prompts.clear()
state.translator.dispose()
return {
ordinaryAfterCompletion,
cancelledPrompts: 32,
sizesAfterEviction,
afterEvictionBeforeCompletion,
afterExactTurnCompletion,
afterAllLookupMapsEmpty,
afterSessionClear,
oldAfterReplacementCompletion,
replacementClaimPreserved: true,
wrongThreadPreserved: true,
wrongTurnPreserved: true,
rejectedCompletionPreserved: true,
successfulInterruptRequests: state.requestCount
}
}
module.exports = run
@@ -0,0 +1,54 @@
{
"baselineHashes": {
"src/main/codex/codex-prompt-registry.ts": "254c530216f47abfc3572bb1689161ad3c10775af2cdceda7e94f443e52c0691"
},
"namedRefs": [
{
"ref": "HEAD",
"revision": "9e2c137548bf99f91255ab4862c01145e42a0883",
"sha256": "254c530216f47abfc3572bb1689161ad3c10775af2cdceda7e94f443e52c0691",
"matchesBaseline": true
},
{
"ref": "origin/main",
"revision": "77cd61df396f25ec91ee2d5ddcbd1f55aa94f818",
"sha256": "254c530216f47abfc3572bb1689161ad3c10775af2cdceda7e94f443e52c0691",
"matchesBaseline": true
}
],
"historicalRuntimeReproduced": false,
"callbackProvenance": [
{
"path": "src/main/codex/codex-structured-session-acquire.ts",
"sha256": "71cd2bae18944c2aaa3ea2a1d00958890e4c8219d5aae9a4de48dd692562ace0"
},
{
"path": "src/main/codex/codex-structured-session-adapter.ts",
"sha256": "eab8820250ebdb1b3f6b3ab9287e16686bd6766f5af5dd29e081d7e47f083b20"
},
{
"path": "src/main/codex/codex-structured-provider-events.ts",
"sha256": "a411dc378e521ea8fc915e2a2a21476341a30f440e8233ae5649205d2a1e2e44"
},
{
"path": "src/main/codex/codex-structured-prompt-ownership.ts",
"sha256": "8d86f3784b79a1470e4002527bdfa505efe2762cba865de7716442f7d9730013"
},
{
"path": "src/main/codex/codex-structured-turn-cancellation.ts",
"sha256": "518b8e5a2f6dc4f8e46e01366c0a61830d6c805b68101484726bd62bbfc831a8"
},
{
"path": "src/main/codex/codex-structured-journal-translation.ts",
"sha256": "56e17b9649471e804d29d1d54a3e14b4298ca8af2e25c4759dc37fce000b06a1"
},
{
"path": "src/main/codex/codex-structured-journal-settlement.ts",
"sha256": "761616c292e15894b337fdd171acd7c2c1272fcabdbad3ea460f8831a923c797"
},
{
"path": "src/main/codex/codex-structured-session-close.ts",
"sha256": "f85aa2cdcfd2ddf34ae0be8397a3896128f04a989eaff750f8e72eee3398b361"
}
]
}
@@ -0,0 +1,29 @@
const assert = require('node:assert/strict')
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const { resolve } = require('node:path')
const { applyPatch, parsePatch, reversePatch } = require('diff')
module.exports = function loadSources() {
const root = resolve(__dirname, '../../..')
const expected = JSON.parse(readFileSync(resolve(__dirname, 'source-versions.json'), 'utf8'))
const parsed = parsePatch(readFileSync(resolve(__dirname, 'fix.patch'), 'utf8'))
const before = new Map()
const after = new Map()
const hashes = {}
assert.equal(parsed.length, 1)
for (const patch of parsed) {
const path = patch.newFileName.replace(/^b\//, '')
assert.ok(Object.hasOwn(expected.baselineHashes, path), `Unexpected patch path: ${path}`)
const absolute = resolve(root, path)
const current = readFileSync(absolute, 'utf8')
const baseline = applyPatch(current, reversePatch(patch))
assert.notEqual(baseline, false, `Source changed; review fix.patch: ${path}`)
const hash = (source) => createHash('sha256').update(source).digest('hex')
assert.equal(hash(baseline), expected.baselineHashes[path], `Baseline drift: ${path}`)
before.set(absolute, baseline)
after.set(absolute, current)
hashes[path] = { before: hash(baseline), after: hash(current) }
}
return { root, before, after, hashes }
}
@@ -0,0 +1,69 @@
{
"backgroundLaunch": true,
"newRegressionCases": 4,
"before": {
"config": "docs/audits/codex-prompt-claim-retention/before.config.mjs",
"passed": 31,
"failed": 3,
"exitCode": 1,
"paths": [
"src/main/codex/codex-prompt-registry-retention.test.ts",
"src/main/codex/codex-structured-prompt-ownership.test.ts",
"src/main/codex/codex-structured-prompt-replies.test.ts"
],
"expectedFailures": [
"releases 32 evicted claims only when their exact turn completes",
"preserves a replacement prompt and its active claim when the old turn completes",
"finds an evicted claim through its bounded turn digest"
]
},
"after": {
"config": "config/vitest.config.ts",
"passed": 71,
"failed": 0,
"exitCode": 0,
"paths": [
"src/main/codex/codex-prompt-registry-retention.test.ts",
"src/main/codex/codex-structured-prompt-ownership.test.ts",
"src/main/codex/codex-structured-prompt-replies.test.ts",
"src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts",
"src/main/codex/codex-structured-journal-translation-settlement.test.ts",
"src/main/codex/codex-structured-session-close.test.ts"
]
},
"typechecks": {
"command": "node config/scripts/run-typecheck-projects-in-parallel.mjs",
"projects": [
"config/tsconfig.node.json",
"config/tsconfig.tc.cli.json",
"config/tsconfig.tc.web.json"
],
"exitCode": 0
},
"focusedOxlint": {
"ordinaryExitCode": 0,
"typeAwareExitCode": 0,
"noIgnore": true,
"files": 6
},
"changedCodeQuality": {
"command": "node config/scripts/check-changed-code-quality.mjs",
"base": "2fccacadbe23",
"changedFiles": 310,
"newFindings": 0,
"exitCode": 0
},
"proof": {
"command": "node --expose-gc --max-old-space-size=192 docs/audits/codex-prompt-claim-retention/reproduce.cjs",
"node": "26.6.0",
"electron": "43.7.0",
"electronNode": "24.21.0",
"bothExitCode": 0,
"beforeRetainedAfterExactCompletion": 32,
"afterRetainedAfterExactCompletion": 0,
"ordinaryPromptBytes": "not measured",
"payloadAmplification": false,
"ordering": "injected delayed child-turn completion, not an affected-host capture"
},
"formatCheckExitCode": 0
}
+70
View File
@@ -0,0 +1,70 @@
# Delayed daemon output after a synthetic exit
A daemon stop response can arrive on its control socket before its final DATA and
EXIT events arrive on the separate stream socket. Main then emits a synthetic exit.
The delayed DATA recreates a headless model and marks the runtime PTY connected;
the old duplicate-exit check suppresses the physical EXIT before runtime cleanup.
The host has no live session, but main retains the connected record, title tracker
and headless terminal.
## Reproduce
From the checkout, using its installed dependencies:
```sh
ORCA_BACKGROUND_LAUNCH=1 node docs/audits/daemon-late-exit/reproduce.mjs /tmp/daemon-late-exit-results.json
ORCA_BACKGROUND_LAUNCH=1 node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/ipc/pty/daemon-late-exit.test.ts
```
The script uses the real daemon server, client, provider, socket pair, kill IPC
handler, listener binding, and runtime. The native subprocess boundary is a fixture
whose force-kill callback reports exit. Pausing only the stream reader makes the
independent control/stream ordering deterministic. No renderer or visible app is
launched. Temporary Vitest files are removed afterward.
The before case moves duplicate suppression back ahead of runtime cleanup in the
loaded module only. It retains the current incarnation-aware marker representation,
which does not affect the same-incarnation race. The on-disk source stays unchanged.
The script verifies its transform boundary and records the current source hash.
## Results
[results.json](./results.json) contains four before/after controls:
| Scenario | Before | After |
| -------------------------------------------------------- | ------------------------------------------------------ | -------------------------------------- |
| Kill reply overtakes queued DATA and EXIT | Connected; headless model and title tracker retained | Disconnected; both released |
| Host inventory verifies exit before queued DATA and EXIT | Connected despite an `exited` verdict; models retained | Disconnected; models released |
| Kill reply overtakes EXIT with no queued DATA | Disconnected; cause remains `stop_unverified` | Disconnected; confirmed requested stop |
| Natural DATA then EXIT | Disconnected; models released | Same |
All eight runs receive one physical provider exit, deliver all final output to the
renderer admission boundary, send one renderer exit, and call the runtime exit
listener once. Fresh daemon inventory is empty in all cases. Additional regression
tests cover same-ID replacement, stale provider callbacks, legacy unstamped exits,
and one-time dispatch settlement with the real SQLite orchestration database.
The fix always processes the current incarnation's provider exit in main. Duplicate
suppression applies only to the renderer notification. When the provider supplies an
incarnation, its marker names that stopped incarnation and cannot suppress a
differently stamped replacement's exit. Legacy unstamped events retain their
existing matching behavior. A matching marker restores the original stop intent
while normal exit-cause resolution still handles negative,
unconfirmed exits. No output is dropped and no wire fields or opcodes change.
## Report correlation and limits
The early-return listener and synthetic renderer-kill exit are present in both
`v1.4.197` (#19018) and `v1.4.192` (#17344). The reproduced `connected: true` plus
`stop_unverified` state matches #19018's reported contradiction and provides a
concrete main-process retaining path relevant to #19831. This does not prove which
ordering occurred in either user's session, explain #19018's failed subsequent
inventory/close reconciliation, or by itself prove persisted tab resurrection in
#17344. A missing `diagnostics.memory` row is not process-exit evidence; this proof
uses the owning daemon's physical exit and fresh session inventory.
A second runtime cleanup may advance an already-retired surface's topology revision
once more. It does not republish a removed surface. Existing exit listeners and
waiters remove themselves on settlement; completed dispatches are no longer active.
The existing marker timeout remains 30 seconds. The separate asynchronous shutdown
call's ownership across its await is outside this change.
+120
View File
@@ -0,0 +1,120 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { startVitest } from 'vitest/node'
if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') {
throw new Error('Run with ORCA_BACKGROUND_LAUNCH=1.')
}
const root = fileURLToPath(new URL('../../../', import.meta.url))
const sourcePath = join(root, 'src/main/ipc/pty/provider/bind-listeners.ts')
const source = await readFile(sourcePath, 'utf8')
const declaration =
' const syntheticExit = session.consumeSyntheticKillExit(payload.id, payload.incarnationId)'
const notificationFence =
' // The control reply can overtake stream data; the physical exit must retire that late output.\n' +
' if (syntheticExit) {\n return\n }'
const restoreIntent =
' if (syntheticExit) {\n session.runtime?.markPtyStopRequested(payload.id)\n }\n'
for (const boundary of [declaration, notificationFence, restoreIntent]) {
assert(source.includes(boundary), 'Source changed: review the baseline transform.')
}
const before = source
.replace(notificationFence, '')
.replace(restoreIntent, '')
.replace(declaration, `${declaration}\n if (syntheticExit) {\n return\n }`)
const fixturePath = join(root, 'src/main/ipc/pty/daemon-late-exit-test-fixture.ts')
const scratch = await mkdtemp(join(tmpdir(), 'orca-daemon-late-exit-proof-'))
const phases = []
try {
for (const phase of ['before', 'after']) {
const resultPath = join(scratch, `${phase}.json`)
const testPath = join(scratch, `${phase}.test.ts`)
const configPath = join(scratch, `${phase}.config.mjs`)
await writeFile(
testPath,
`
import { afterAll, it } from ${JSON.stringify(join(root, 'node_modules/vitest/dist/index.js'))}
import { writeFileSync } from 'node:fs'
import { startLateExitHarness } from ${JSON.stringify(fixturePath)}
const rows = []
for (const scenario of ['queued-data', 'verified-stop', 'no-queued-data', 'natural-exit']) {
it(scenario, async () => {
const harness = await startLateExitHarness()
try {
if (scenario !== 'natural-exit') harness.pauseStream()
if (scenario !== 'no-queued-data') harness.subprocess._simulateData('final output\\r\\n')
if (scenario === 'natural-exit') harness.subprocess._simulateExit(0)
else if (scenario === 'verified-stop') { if (!await harness.stopAndWait()) throw new Error('Stop was not verified') }
else await harness.kill()
const beforeDrain = harness.runtime.captureState()
harness.resumeStream()
await harness.waitForExit()
const result = await harness.capture()
delete result.incarnationId
delete beforeDrain.incarnationId
rows.push({ scenario, beforeDrain, afterDrain: result })
} finally { await harness.dispose() }
})
}
afterAll(() => writeFileSync(${JSON.stringify(resultPath)}, JSON.stringify(rows)))
`
)
await writeFile(
configPath,
`
import base from ${JSON.stringify(pathToFileURL(join(root, 'config/vitest.config.ts')).href)}
export default {
...base,
plugins: [{ name: 'late-exit-baseline', enforce: 'pre', transform(code, id) {
if (${JSON.stringify(phase)} === 'before' && id.replaceAll('\\\\', '/').endsWith('/src/main/ipc/pty/provider/bind-listeners.ts')) return ${JSON.stringify(before)}
} }],
test: { ...base.test, include: [${JSON.stringify(testPath)}], maxWorkers: 1, fileParallelism: false }
}
`
)
const runner = await startVitest('test', [], {
root,
config: configPath,
watch: false,
reporters: ['dot']
})
assert(runner, 'Vitest did not start')
await runner.close()
const samples = JSON.parse(await readFile(resultPath, 'utf8'))
assert.equal(samples.length, 4)
for (const sample of samples) {
const leaked =
phase === 'before' && ['queued-data', 'verified-stop'].includes(sample.scenario)
assert.equal(sample.afterDrain.connected, leaked)
assert.equal(sample.afterDrain.headlessModelRetained, leaked)
assert.equal(sample.afterDrain.titleTrackerRetained, leaked)
assert.equal(sample.afterDrain.providerHasPty, false)
assert.equal(sample.afterDrain.hostInventoryCount, 0)
assert.equal(sample.afterDrain.rendererExitCount, 1)
assert.equal(sample.afterDrain.providerExitCount, 1)
assert.equal(sample.afterDrain.exitListenerCalls, 1)
assert.deepEqual(
sample.afterDrain.deliveredData,
sample.scenario === 'no-queued-data' ? [] : ['final output\r\n']
)
}
phases.push({ phase, samples })
}
const results = {
sourceSha256: createHash('sha256').update(source).digest('hex'),
baselineTransform:
'Restore duplicate suppression before provider/runtime cleanup only; keep current incarnation-fenced markers.',
phases
}
const output = `${JSON.stringify(results, null, 2)}\n`
if (process.argv[2]) {
await writeFile(resolve(process.argv[2]), output)
}
process.stdout.write(output)
} finally {
await rm(scratch, { recursive: true, force: true })
}
+236
View File
@@ -0,0 +1,236 @@
{
"sourceSha256": "8da1df8409b4e1555894546df9e2fd41351b4f5620340310c2373d22bb56a0b6",
"baselineTransform": "Restore duplicate suppression before provider/runtime cleanup only; keep current incarnation-fenced markers.",
"phases": [
{
"phase": "before",
"samples": [
{
"scenario": "queued-data",
"beforeDrain": {
"connected": false,
"exitCause": {
"kind": "unknown",
"reason": "stop_unverified"
},
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": null
},
"afterDrain": {
"connected": true,
"exitCause": {
"kind": "unknown",
"reason": "stop_unverified"
},
"headlessModelRetained": true,
"titleTrackerRetained": true,
"liveness": null,
"providerHasPty": false,
"hostInventoryCount": 0,
"deliveredData": ["final output\r\n"],
"rendererExitCount": 1,
"providerExitCount": 1,
"exitListenerCalls": 1
}
},
{
"scenario": "verified-stop",
"beforeDrain": {
"connected": false,
"exitCause": {
"kind": "operator_close"
},
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": "exited"
},
"afterDrain": {
"connected": true,
"exitCause": {
"kind": "operator_close"
},
"headlessModelRetained": true,
"titleTrackerRetained": true,
"liveness": "exited",
"providerHasPty": false,
"hostInventoryCount": 0,
"deliveredData": ["final output\r\n"],
"rendererExitCount": 1,
"providerExitCount": 1,
"exitListenerCalls": 1
}
},
{
"scenario": "no-queued-data",
"beforeDrain": {
"connected": false,
"exitCause": {
"kind": "unknown",
"reason": "stop_unverified"
},
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": null
},
"afterDrain": {
"connected": false,
"exitCause": {
"kind": "unknown",
"reason": "stop_unverified"
},
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": null,
"providerHasPty": false,
"hostInventoryCount": 0,
"deliveredData": [],
"rendererExitCount": 1,
"providerExitCount": 1,
"exitListenerCalls": 1
}
},
{
"scenario": "natural-exit",
"beforeDrain": {
"connected": true,
"exitCause": null,
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": null
},
"afterDrain": {
"connected": false,
"exitCause": {
"kind": "exited",
"exitCode": 0
},
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": "exited",
"providerHasPty": false,
"hostInventoryCount": 0,
"deliveredData": ["final output\r\n"],
"rendererExitCount": 1,
"providerExitCount": 1,
"exitListenerCalls": 1
}
}
]
},
{
"phase": "after",
"samples": [
{
"scenario": "queued-data",
"beforeDrain": {
"connected": false,
"exitCause": {
"kind": "unknown",
"reason": "stop_unverified"
},
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": null
},
"afterDrain": {
"connected": false,
"exitCause": {
"kind": "operator_close"
},
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": "exited",
"providerHasPty": false,
"hostInventoryCount": 0,
"deliveredData": ["final output\r\n"],
"rendererExitCount": 1,
"providerExitCount": 1,
"exitListenerCalls": 1
}
},
{
"scenario": "verified-stop",
"beforeDrain": {
"connected": false,
"exitCause": {
"kind": "operator_close"
},
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": "exited"
},
"afterDrain": {
"connected": false,
"exitCause": {
"kind": "operator_close"
},
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": "exited",
"providerHasPty": false,
"hostInventoryCount": 0,
"deliveredData": ["final output\r\n"],
"rendererExitCount": 1,
"providerExitCount": 1,
"exitListenerCalls": 1
}
},
{
"scenario": "no-queued-data",
"beforeDrain": {
"connected": false,
"exitCause": {
"kind": "unknown",
"reason": "stop_unverified"
},
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": null
},
"afterDrain": {
"connected": false,
"exitCause": {
"kind": "operator_close"
},
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": "exited",
"providerHasPty": false,
"hostInventoryCount": 0,
"deliveredData": [],
"rendererExitCount": 1,
"providerExitCount": 1,
"exitListenerCalls": 1
}
},
{
"scenario": "natural-exit",
"beforeDrain": {
"connected": true,
"exitCause": null,
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": null
},
"afterDrain": {
"connected": false,
"exitCause": {
"kind": "exited",
"exitCode": 0
},
"headlessModelRetained": false,
"titleTrackerRetained": false,
"liveness": "exited",
"providerHasPty": false,
"hostInventoryCount": 0,
"deliveredData": ["final output\r\n"],
"rendererExitCount": 1,
"providerExitCount": 1,
"exitListenerCalls": 1
}
}
]
}
]
}
@@ -0,0 +1,67 @@
# Shared daemon owner incarnation retention
A degraded daemon provider creates two owner resolvers with one shared route map. On an authenticated daemon identity change, the attach resolver removes that daemons routes first. The liveness resolver then sees no corresponding routes and previously left its private session-to-incarnation entries behind. Repeating replacements with newly discovered session IDs grows that private map for the lifetime of the degraded provider.
The fix removes private incarnation entries whose shared route is absent after provider invalidation. It preserves every remaining route, including another providers live session and a same-ID successor. It does not change process liveness, stop remote work, change the wire protocol, or depend on a git workspace.
## Actual ownership and trigger
- `src/main/daemon/daemon-provider-init.ts:123` selects `DegradedDaemonPtyProvider` for `degraded-new-pty-fallback`; startup discovery runs at line 139.
- `src/main/daemon/degraded-daemon-owner-recovery.ts:15` constructs both resolvers with the same map; public discovery and liveness probes populate their private indexes. Startup reconciliation can also record both routes.
- `src/main/daemon/degraded-daemon-owner-recovery.ts:70` subscribes to each daemons identity publication and invalidates the attach resolver before the liveness resolver.
- `src/main/daemon/daemon-pty-connection-lifecycle.ts:41` publishes only after a different authenticated identity replaces a previous identity. Repeated observation of the same identity does not retire anything.
- `src/main/daemon/daemon-pty-daemon-recovery.ts:268` can replace the daemon while retaining its adapter and the degraded provider.
- `src/main/daemon/daemon-session-owner-resolution.ts:44` performs the invalidation and the new private-metadata prune.
This is a local desktop main-process degraded-provider path. Loss of SSH contact is not its retirement trigger. Entry counts below do not establish retained bytes, RSS, an OOM, or causation for #19831.
## Bounded actual-source proof
The fixture uses the actual degraded provider, recovery controller, resolvers, daemon adapter inventory, authenticated identity publication, and direct attach implementation. Only finite authenticated transport replies and the empty fallback provider are inert; it starts no native PTY, socket, network connection, or application window. It does not depend on garbage-collection timing or a never-settling promise.
Each of 32 cycles discovers a new current-daemon session, populates both resolvers through public calls, observes an unchanged identity, then publishes a replacement identity. An unrelated legacy-daemon session remains live throughout. Finally, an ordinary legacy exit removes its route from both resolvers.
| After 32 replacements and the legacy exit | Baseline | Fixed |
| ----------------------------------------- | -------: | ----: |
| Shared routes | 0 | 0 |
| Attach resolver incarnation entries | 0 | 0 |
| Liveness resolver incarnation entries | 32 | 0 |
Additional assertions preserve a same-ID successor on another provider, an unchanged authenticated identity, direct attach with a matching authoritative incarnation without inventory, refusal of a mismatched authoritative incarnation, and ordinary exit cleanup. The permanent tests include the repetition regression and three compatibility controls; the portable fixture additionally exercises the actual adapter attach transport path.
## Reproduce
Run from the repository root with its dependencies installed:
```sh
ORCA_BACKGROUND_LAUNCH=1 node docs/audits/daemon-shared-owner-incarnation-retention/reproduce.cjs
ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts src/main/daemon/daemon-session-owner-resolution.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts
```
The runner accepts an optional output filename as its first argument. On macOS, the Electron runtime control is:
```sh
ORCA_BACKGROUND_LAUNCH=1 ELECTRON_RUN_AS_NODE=1 node_modules/electron/dist/Electron.app/Contents/MacOS/Electron docs/audits/daemon-shared-owner-incarnation-retention/reproduce.cjs
```
On Linux or Windows, use the corresponding installed Electron binary with the same environment variables. It runs as Node and never displays a window.
The baseline test overlay reverses only the fenced product patch in memory:
```sh
ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config docs/audits/daemon-shared-owner-incarnation-retention/before.config.mjs src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts src/main/daemon/daemon-session-owner-resolution.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts
```
Expected: exactly the new repeated-retirement assertion fails before the fix; the other 53 tests pass. All 54 pass with the fix.
## Source identities and publication independence
`sources.cjs` checks the exact fixed source hash, reverses `fix.patch`, checks the baseline hash, and fences every evaluated TypeScript dependency. It records actual evaluated and input hashes and a bundle hash in each report. A CRLF control checks source and patch normalization. The default runner needs neither Git history nor ignored audit notes.
`source-versions.json` records the audited source graph (276 modules) and the independent main graph at `291b4ddd6f1c1af480169885e0fda7f9c78ff053` (274 modules). Both graphs are accepted explicitly; ten surrounding modules differ because of unrelated audit fixes. The proof therefore does not require those fixes to be stacked. The publication reports were produced through the exported `run({ readSource, output, sourceLabel })` API, reading each non-target source from that named main revision and applying only this product change. The default command also runs directly on that publication tree with the fix and artifact installed.
Node 26.6.0 and Electron 43.7.0 / Node 24.21.0 both produced the table above against both source graphs. All four executions used the working installations external packages. These are source overlays, not historical application or dependency installations.
At reported v1.4.198 (`e0826956fcfc532f5a1e55b5e081f2e57e553c43`), the resolver, shared recovery controller, and authenticated identity publication match the recorded baseline exactly. The surrounding degraded provider differs, as recorded in `historicalCore`; no whole-v1.4.198 execution or incident attribution is claimed.
`validation.json` records tests, typecheck, full-file artifact quality, and limits. The four result files contain measured entry counts and exact source/artifact identities.
@@ -0,0 +1,23 @@
import { resolve } from 'node:path'
import { createRequire } from 'node:module'
import { defineConfig, mergeConfig } from 'vitest/config'
import baseConfig from '../../../config/vitest.config.ts'
const { loadSources } = createRequire(import.meta.url)('./sources.cjs')
const { before } = loadSources()
const sourcePath = resolve('src/main/daemon/daemon-session-owner-resolution.ts')
export default mergeConfig(
baseConfig,
defineConfig({
plugins: [
{
name: 'shared-owner-incarnation-before-fix',
enforce: 'pre',
transform(_code, id) {
return resolve(id.split('?')[0]) === sourcePath ? { code: before, map: null } : undefined
}
}
]
})
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
diff --git a/src/main/daemon/daemon-session-owner-resolution.ts b/src/main/daemon/daemon-session-owner-resolution.ts
index 1906ebfb35..47fe531ec7 100644
--- a/src/main/daemon/daemon-session-owner-resolution.ts
+++ b/src/main/daemon/daemon-session-owner-resolution.ts
@@ -54,0 +55,6 @@ export class DaemonSessionOwnerResolver<T extends IPtyProvider> {
+ // Another resolver may already have removed this provider's shared routes.
+ for (const sessionId of this.routeIncarnations.keys()) {
+ if (!this.routes.has(sessionId)) {
+ this.routeIncarnations.delete(sessionId)
+ }
+ }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const { load, loadSources, read, sha } = require('./sources.cjs')
const { exercise } = require('./scenario.cjs')
async function run({ readSource = read, output, sourceLabel = 'working-tree' } = {}) {
assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1')
const phases = {}
for (const phase of ['before', 'fixed']) {
const loaded = await load(phase, readSource)
phases[phase] = { ...(await exercise(loaded.api, phase)), provenance: loaded.provenance }
}
let crlfReads = 0
const crlf = loadSources((file) => {
crlfReads += 1
return readSource(file).replaceAll('\n', '\r\n')
})
assert.deepEqual(crlf, loadSources(readSource))
assert.equal(crlfReads, 2)
const artifacts = [
'sources.cjs',
'scenario.cjs',
'reproduce.cjs',
'before.config.mjs',
'source-versions.json',
'fix.patch'
]
const result = {
scope:
'Actual degraded provider/recovery/resolvers, adapter inventory, identity publication and direct attach; finite inert authenticated transport replies, no native PTY or network',
runtime: process.versions,
sourceLabel,
crlfReads,
artifactHashes: Object.fromEntries(
artifacts.map((file) => [file, sha(read(path.join(__dirname, file)))])
),
phases
}
const filename =
output ??
path.join(__dirname, process.versions.electron ? 'electron-results.json' : 'node-results.json')
fs.writeFileSync(filename, `${JSON.stringify(result, null, 2)}\n`)
console.log(
JSON.stringify({
output: filename,
before: phases.before.afterLegacyExit,
fixed: phases.fixed.afterLegacyExit,
sourceLabel
})
)
return result
}
module.exports = { run }
if (require.main === module) {
run({ output: process.argv[2] }).catch((error) => {
console.error(error)
process.exitCode = 1
})
}
@@ -0,0 +1,182 @@
const assert = require('node:assert/strict')
const path = require('node:path')
function identity(epoch, pid) {
return { pid, startedAtMs: epoch + 1, launchNonce: `daemon-${pid}-${epoch}` }
}
function makeAdapter(api, name, pid) {
const adapter = new api.DaemonPtyAdapter({
socketPath: path.join(__dirname, `${name}.sock`),
tokenPath: path.join(__dirname, `${name}.token`)
})
let sessions = []
const requests = []
adapter.client.daemonIdentity = identity(0, pid)
// Only the authenticated transport ports are inert; inventory and identity publication are actual methods.
adapter.client.ensureConnected = async () => {}
adapter.client.ensureConnectedWithin = async () => {}
adapter.client.request = async (type, payload) => {
requests.push(type)
if (type === 'listSessions') {
return { sessions }
}
assert.equal(type, 'createOrAttach')
assert.equal(payload.attachOnly, true)
const found = sessions.find((item) => item.sessionId === payload.sessionId)
assert(found)
return {
isNew: false,
snapshot: null,
pid: found.pid,
incarnationId: found.incarnationId,
shellState: 'unsupported'
}
}
return {
adapter,
requests,
setSessions(value) {
sessions = value
},
publishIdentity(epoch) {
adapter.client.daemonIdentity = identity(epoch, pid)
return adapter.establishLifecycleLease()
}
}
}
function session(id, incarnationId) {
return {
sessionId: id,
incarnationId,
isAlive: true,
pid: 999999999,
cwd: '/fixture',
cols: 80,
rows: 24
}
}
async function exercise(api, phase) {
const current = makeAdapter(api, 'current', 999999997)
const legacy = makeAdapter(api, 'legacy', 999999998)
const fallback = {
onData: () => () => {},
onExit: () => () => {},
hasPty: () => false,
listProcesses: async () => []
}
const provider = new api.DegradedDaemonPtyProvider({
current: current.adapter,
legacy: [legacy.adapter],
fallback
})
const recovery = provider.ownerRecovery
const attach = recovery.attachResolver
const liveness = recovery.livenessResolver
const rows = []
try {
await current.publishIdentity(0)
await legacy.publishIdentity(0)
legacy.setSessions([session('legacy-live', 'legacy-incarnation')])
for (let cycle = 0; cycle < 32; cycle++) {
const id = `current-${cycle}`
current.setSessions([session(id, `incarnation-${cycle}`)])
// Public discovery populates attach authority; public liveness fills the other resolver.
await provider.discoverDaemonSessions()
assert.equal(await provider.probePtyLiveness(`unmapped-probe-${cycle}`), false)
assert.equal(attach.routeIncarnations.get(id), `incarnation-${cycle}`)
assert.equal(liveness.routeIncarnations.get(id), `incarnation-${cycle}`)
assert.equal(provider.sessionProviders.get(id), current.adapter)
const beforeDuplicate = liveness.routeIncarnations.size
await current.publishIdentity(cycle)
assert.equal(liveness.routeIncarnations.size, beforeDuplicate)
// A new authenticated identity retires the old daemon's routes through actual listeners.
current.setSessions([])
await current.publishIdentity(cycle + 1)
assert.equal(provider.sessionProviders.has(id), false)
assert.equal(attach.routeIncarnations.has(id), false)
assert.equal(liveness.routeIncarnations.has(id), phase === 'before')
assert.equal(provider.sessionProviders.get('legacy-live'), legacy.adapter)
assert.equal(attach.routeIncarnations.get('legacy-live'), 'legacy-incarnation')
assert.equal(liveness.routeIncarnations.get('legacy-live'), 'legacy-incarnation')
rows.push({
cycle,
sharedRoutes: provider.sessionProviders.size,
attachEntries: attach.routeIncarnations.size,
livenessEntries: liveness.routeIncarnations.size
})
}
assert.equal(provider.sessionProviders.size, 1)
assert.equal(attach.routeIncarnations.size, 1)
assert.equal(liveness.routeIncarnations.size, phase === 'before' ? 33 : 1)
legacy.adapter.client.eventListeners.each((listener) =>
listener({
type: 'event',
event: 'exit',
sessionId: 'legacy-live',
payload: { code: 0, incarnationId: 'legacy-incarnation' }
})
)
assert.equal(provider.sessionProviders.size, 0)
assert.equal(attach.routeIncarnations.size, 0)
assert.equal(liveness.routeIncarnations.size, phase === 'before' ? 32 : 0)
const afterLegacyExit = {
sharedRoutes: provider.sessionProviders.size,
attachEntries: attach.routeIncarnations.size,
livenessEntries: liveness.routeIncarnations.size
}
legacy.setSessions([])
current.setSessions([session('same-id', 'old-incarnation')])
await provider.discoverDaemonSessions()
await provider.probePtyLiveness('unmapped-old')
current.setSessions([])
legacy.setSessions([session('same-id', 'new-incarnation')])
await provider.discoverDaemonSessions()
await provider.probePtyLiveness('unmapped-new')
await current.publishIdentity(33)
assert.equal(provider.sessionProviders.get('same-id'), legacy.adapter)
assert.equal(attach.routeIncarnations.get('same-id'), 'new-incarnation')
assert.equal(liveness.routeIncarnations.get('same-id'), 'new-incarnation')
current.requests.length = 0
legacy.requests.length = 0
const attached = await provider.spawn({
sessionId: 'same-id',
attachOnly: true,
cols: 80,
rows: 24,
expectedIncarnationId: 'new-incarnation',
expectedIncarnationIsAuthoritative: true
})
assert.equal(attached.id, 'same-id')
assert.equal(attached.incarnationId, 'new-incarnation')
assert.equal(attached.isReattach, true)
assert.deepEqual(current.requests, [])
assert.deepEqual(legacy.requests, ['createOrAttach'])
await assert.rejects(
provider.spawn({
sessionId: 'same-id',
attachOnly: true,
cols: 80,
rows: 24,
expectedIncarnationId: 'retired-incarnation',
expectedIncarnationIsAuthoritative: true
}),
{ name: 'TerminalSessionOwnerUnverifiedError' }
)
assert.equal(legacy.requests.filter((type) => type === 'createOrAttach').length, 1)
return {
cycles: 32,
rows,
afterLegacyExit,
sameIdSuccessorPreserved: true,
matchingDirectAttachWithoutInventory: true,
authoritativeIncarnationMismatchRefused: true,
unchangedIdentityPreserved: true,
otherLiveProviderPreserved: true,
ordinaryExitRetiresBoth: true
}
} finally {
provider.dispose()
}
}
module.exports = { exercise }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,100 @@
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const Module = require('node:module')
const { createHash } = require('node:crypto')
const { build } = require('esbuild')
const { applyPatch, parsePatch, reversePatch } = require('diff')
const root = path.resolve(__dirname, '../../..')
const canonicalLf = (value) => value.replaceAll('\r\n', '\n')
const read = (file) => canonicalLf(fs.readFileSync(file, 'utf8'))
const sha = (value) => createHash('sha256').update(value).digest('hex')
const versions = JSON.parse(read(path.join(__dirname, 'source-versions.json')))
const relative = (file) => path.relative(root, file).split(path.sep).join('/')
function loadSources(readSource = read) {
const fixed = canonicalLf(readSource(path.join(root, versions.sourcePath)))
assert.equal(sha(fixed), versions.fixedSha256)
const patches = parsePatch(canonicalLf(readSource(path.join(__dirname, 'fix.patch'))))
assert.equal(patches.length, 1)
assert.equal(patches[0].newFileName, `b/${versions.sourcePath}`)
const before = applyPatch(fixed, reversePatch(patches[0]))
assert.notEqual(before, false)
assert.equal(sha(before), versions.baselineSha256)
return { before, fixed }
}
async function load(phase, readSource = read) {
assert.ok(['before', 'fixed'].includes(phase))
const checked = loadSources(readSource)
const evaluatedSources = {}
const provenanceSources = {}
const built = await build({
stdin: {
contents: [
"export { DaemonPtyAdapter } from './src/main/daemon/daemon-pty-adapter'",
"export { DegradedDaemonPtyProvider } from './src/main/daemon/degraded-daemon-pty-provider'"
].join('\n'),
resolveDir: root,
loader: 'ts'
},
absWorkingDir: root,
bundle: true,
platform: 'node',
format: 'cjs',
packages: 'external',
write: false,
plugins: [
{
name: 'hash-fenced-owner-incarnation-sources',
setup(builder) {
builder.onResolve({ filter: /^\./ }, (args) => {
const base = path.resolve(args.resolveDir, args.path)
for (const file of [base, `${base}.ts`, path.join(base, 'index.ts')]) {
const key = relative(file)
if (key === versions.sourcePath || Object.hasOwn(versions.dependencies, key)) {
return { path: file }
}
}
return undefined
})
builder.onLoad({ filter: /\.ts$/ }, ({ path: file }) => {
const key = relative(file)
let contents = canonicalLf(readSource(file))
const actual = sha(contents)
provenanceSources[key] = actual
if (key === versions.sourcePath) {
assert.equal(actual, versions.fixedSha256)
contents = checked[phase]
} else {
assert.ok(versions.dependencies[key]?.includes(actual), `Dependency drift: ${key}`)
}
evaluatedSources[key] = sha(contents)
return { contents, loader: 'ts' }
})
}
}
]
})
const evaluatedKeys = Object.keys(evaluatedSources).sort()
const recognizedGraph = [versions.workingEvaluated, versions.publicationEvaluated].some(
(known) => JSON.stringify(Object.keys(known).sort()) === JSON.stringify(evaluatedKeys)
)
assert.equal(recognizedGraph, true, 'Unreviewed evaluated module graph')
const filename = path.join(__dirname, `${phase}-bundle.cjs`)
const loaded = new Module(filename, module)
loaded.filename = filename
loaded.paths = Module._nodeModulePaths(root)
loaded._compile(built.outputFiles[0].text, filename)
return {
api: loaded.exports,
provenance: {
evaluatedSources,
provenanceSources,
bundleSha256: sha(built.outputFiles[0].text)
}
}
}
module.exports = { load, loadSources, read, sha, root, versions }
@@ -0,0 +1,88 @@
{
"backgroundLaunch": "All tests and proofs used ORCA_BACKGROUND_LAUNCH=1; Electron additionally used ELECTRON_RUN_AS_NODE=1. No application, native PTY, socket or network.",
"fixedTests": {
"passed": 54,
"failed": 0,
"files": 3,
"newTests": 4,
"config": "config/vitest.config.ts"
},
"baselineOverlay": {
"passed": 53,
"failed": 1,
"config": "docs/audits/daemon-shared-owner-incarnation-retention/before.config.mjs",
"intendedFailure": "releases both private indexes after repeated authenticated daemon replacements",
"actualLivenessKeys": ["retired-0", "legacy-live"],
"expectedLivenessKeys": ["legacy-live"]
},
"portableProofs": {
"reports": [
"node-results.json",
"electron-results.json",
"publication-node-results.json",
"publication-electron-results.json"
],
"phasesPerReport": ["before", "fixed"],
"replacementCyclesPerPhase": 32,
"afterLegacyExit": {
"before": {
"sharedRoutes": 0,
"attachEntries": 0,
"livenessEntries": 32
},
"fixed": {
"sharedRoutes": 0,
"attachEntries": 0,
"livenessEntries": 0
}
},
"workingEvaluatedModules": 276,
"publicationEvaluatedModules": 274,
"crlfSourceAndPatchReads": 2,
"controls": [
"unchanged authenticated identity",
"other live provider",
"ordinary exit",
"same-ID successor",
"matching direct attach without inventory",
"authoritative incarnation mismatch refusal"
]
},
"typechecks": {
"node": "Passed full pnpm tc:node."
},
"fullPublicationQuality": {
"paths": [
"src/main/daemon/daemon-session-owner-resolution.ts",
"src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts",
"docs/audits/daemon-shared-owner-incarnation-retention/sources.cjs",
"docs/audits/daemon-shared-owner-incarnation-retention/scenario.cjs",
"docs/audits/daemon-shared-owner-incarnation-retention/reproduce.cjs",
"docs/audits/daemon-shared-owner-incarnation-retention/before.config.mjs"
],
"scans": [
"default rules and unused suppression",
"casting",
"type-aware quality",
"React Doctor",
"design system",
"default type-aware rules"
],
"result": "All six full-file scans passed with --no-ignore --deny-warnings, including CJS/MJS artifact files."
},
"changedQuality": {
"base": "6ab6802df90553d2a26e9a4c7a519ae11d4b5e09",
"result": "Passed all changed-code scans plus SAFETY rationale across two changed source files. Artifacts separately covered by explicit full-file scans."
},
"productHashes": [
{
"path": "src/main/daemon/daemon-session-owner-resolution.ts",
"sha256": "8f62a181a398dc91d20d269a19e3266e199191486fddfee6dcaf3b2a570c76eb"
},
{
"path": "src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts",
"sha256": "b88680e374c054143c344444b98e80368c05736add90698f9e540d2a4420265f"
}
],
"limits": "Entry-count retention proof, no byte/RSS/OOM/incident claim. Finite inert transport replies. Named main source overlays use working external dependencies; v1.4.198 checked core source parity only."
}
@@ -0,0 +1,36 @@
# Empty streamed deltas retain array entries
The text coalescer charged streamed text by UTF-8 bytes but appended an array entry for every empty delta. A live stream receiving repeated empty updates could retain an increasing number of entries while both byte counters stayed zero. Flushing published a joined string and kept the entries. The actual Codex notification path accepts `delta: ''`; this diagnostic exercises its stream handler and coalescer.
The fix skips only the empty `chunks.push` operation. Empty-stream creation, snapshots, dirty state, scheduled publication, callback receiver, backpressure and eviction remain unchanged.
## Reproduce
```sh
ORCA_BACKGROUND_LAUNCH=1 node docs/audits/empty-streamed-delta-retention/reproduce.cjs
```
The runner reverses hash-checked patches in memory and checks every bundled source dependency. It changes no product files and starts no native process or UI. A bounded CRLF control checks source and patch loading. Reports were recorded on Node 26.6.0 and Electron 43.7.0's Node 24.21.0.
| Control | Before | Fixed |
| -------------------------------------------------------------------- | ------------------------------ | ------------------------- |
| Four batches of 16,384 empty Codex deltas, flushing each batch | 16,384 → 65,536 retained slots | 0 slots after every batch |
| Logical stream count | 1 | 1 |
| Accounted / observed text bytes | 0 / 0 | 0 / 0 |
| Scheduled callbacks / published rows in the complete caller scenario | 6 / 5 | 6 / 5 |
| Append `hé` after empty updates | Same 3-byte text | Same 3-byte text |
| Forget and disposal | Clear retained state | Clear retained state |
The runner compares the entire recorded publication and scheduling behavior before/after. Controls also cover first-empty snapshots, failed publication and retry, rejection of a new empty key while the previous stream is backpressured, accepted eviction, callback receiver, UTF-8 truncation and an empty update after truncation. The two runtimes each execute four source phases: current/main before and fixed, plus the v1.4.198 coalescer before and with the same narrow guard.
The permanent regression invokes the actual Codex stream caller. A temporary `Array.prototype.join` observer measures the matching chunk array only during synchronous snapshot creation, then restores the method. The baseline fails with 65,537 slots versus the expected single nonempty prefix; the other 14 coalescer controls pass. All 64 focused compatibility tests pass with the fix. See [validation.json](./validation.json).
## Source and incident scope
The current baseline is byte-identical to the coalescer at named main commit `291b4ddd6f1c1af480169885e0fda7f9c78ff053`. The exact v1.4.198 coalescer contains the same unconditional empty append; its surrounding implementation differs. Historical phases replace only that module and use the recorded current Codex caller/dependencies. This is a source overlay, not a packaged historical application replay. [source-versions.json](./source-versions.json) records these distinctions and named caller hashes.
Claude's generic checkpoint API also uses the coalescer, but its ordinary provider path rejects empty text in `claude-streamed-block-identity.ts` before calling it. This artifact demonstrates the Codex path and preserves Claude compatibility; it does not claim an ordinary Claude trigger.
Measurement instrumentation reads private map and array cardinalities without changing their contents. These are retained-entry counts, not heap, RSS or byte measurements. The fixture keeps the live stream owned until forget/disposal; it does not establish retention after all owners collect. Nonempty one-byte deltas can still have substantial array overhead within the text-byte allowance, and overflow concatenation has its own transient cost.
No affected-host data establishes how often Codex emitted empty updates in #19831 or another incident. The finding is a reproducible code-level growth mechanism present in the reported release. It does not attribute an app-scope OOM total to this mechanism or establish its incident magnitude. No remote protocol, process liveness, process termination or terminal ownership behavior changes.
@@ -0,0 +1,30 @@
import base from '../../../config/vitest.config.ts'
import { createRequire } from 'node:module'
import { join } from 'node:path'
const require = createRequire(import.meta.url)
const { loadSources, root, versions } = require('./sources.cjs')
const baseline = loadSources().baseline
const target = join(root, versions.sourcePath).replaceAll('\\', '/')
export default {
...base,
test: {
...base.test,
include: [
'src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.test.ts',
'src/main/native-chat/agent-session-wire/agent-session-empty-delta-retention.test.ts'
]
},
plugins: [
{
name: 'exact-baseline-coalescer',
enforce: 'pre',
transform(_code, id) {
return id.replaceAll('\\', '/').split('?')[0] === target
? { code: baseline, map: null }
: null
}
}
]
}
@@ -0,0 +1,865 @@
{
"runtime": {
"node": "24.21.0",
"acorn": "8.18.0",
"ada": "4.0.0",
"amaro": "1.1.11",
"ares": "1.34.8",
"brotli": "1.2.0",
"cldr": "48.0",
"icu": "78.2",
"llhttp": "9.4.3",
"merve": "1.2.2",
"modules": "148",
"napi": "10",
"nbytes": "0.1.4",
"ncrypto": "0.0.1",
"nghttp2": "1.70.0",
"nghttp3": "",
"ngtcp2": "",
"openssl": "0.0.0",
"simdjson": "4.6.7",
"simdutf": "7.7.0",
"sqlite": "3.53.4",
"tz": "2025c",
"undici": "7.29.1",
"unicode": "17.0",
"uv": "1.52.1",
"uvwasi": "0.0.23",
"v8": "15.0.245.31-electron.0",
"zlib": "1.3.2.1-motley",
"zstd": "1.6.0",
"electron": "43.7.0",
"chrome": "150.0.7871.250"
},
"sourceVersions": {
"main": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"reported": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"reportedTag": "v1.4.198"
},
"scope": "Exact current/main coalescer before/after plus exact v1.4.198 coalescer and same local guard, using current Codex stream callers/dependencies. Not a packaged historical release replay.",
"crlfLoaderControl": {
"reads": 3,
"equal": true
},
"artifactHashes": {
"sources.cjs": "7902098e4cd0e07825684c778d7d2af74ba1438a55ccf0b9ffe6eb96f76698c8",
"scenario.cjs": "66cfb53b821f631d2f57988c2fab5d28aa0da95fb4a6a3a2b8b643a82d233550",
"reproduce.cjs": "e55b45abda1a7c6078cc3fc867f2553cf9ed6ffea10fbffdb61c99494d797fe1",
"before.config.mjs": "a55e7576be2348edddc137af9c34fca4323bd060d325a1140ab0ede66618c6e9",
"source-versions.json": "1038900fcbc3310ae4b38eb8603d0eea3d6a417c3835a70f3537d27eee6debc9",
"fix.patch": "06f11750dcd64042b7f208b00ae0feb3e0bdea6d5db66b3823ce063fce8ab97a",
"reported.patch": "9d340925bd2a875334a1a4c3ce58c4ba0f977c66bf0c6f5d66848f862fc95d59"
},
"phases": {
"baseline": {
"sourceSha256": "48828e4ee21858075cbb87ec1caa4a82991a55f80928482615d1df7ec5dc0fb2",
"bundleSha256": "cbdd64b9e21f410645660ac33afe3bede8a58b7680641d5d98883facc0e6a120",
"samples": [
{
"streams": 1,
"slots": 16384,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 32768,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 49152,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 65536,
"retainedBytes": 0,
"observedBytes": 0
}
],
"behavior": {
"scheduled": 6,
"published": 5,
"publications": [
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": "hé"
}
]
}
}
],
"directScheduled": 7,
"emitted": [
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "two",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "two",
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"snapshot": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
}
],
"truncated": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
},
"controls": [
"actual Codex empty notification stream",
"empty snapshot remains present",
"four explicit flushes",
"Unicode text retained",
"forget clears",
"dispose clears",
"first empty publication and retries",
"emit receiver preserved",
"new empty key rejected under backpressure",
"accepted eviction",
"UTF-8 truncation",
"already-truncated empty append keeps no-new-publication behavior"
]
},
"fixed": {
"sourceSha256": "7caf0f24250b42e0ac4fe99e93bcbe0a4acb7cb3465fdde6b248d8bf6ba0d2d0",
"bundleSha256": "3e8b7e8430737f4cb0ca8454add7730770d8cd19ab175266d8b7626a059f3912",
"samples": [
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
}
],
"behavior": {
"scheduled": 6,
"published": 5,
"publications": [
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": "hé"
}
]
}
}
],
"directScheduled": 7,
"emitted": [
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "two",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "two",
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"snapshot": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
}
],
"truncated": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
},
"controls": [
"actual Codex empty notification stream",
"empty snapshot remains present",
"four explicit flushes",
"Unicode text retained",
"forget clears",
"dispose clears",
"first empty publication and retries",
"emit receiver preserved",
"new empty key rejected under backpressure",
"accepted eviction",
"UTF-8 truncation",
"already-truncated empty append keeps no-new-publication behavior"
]
},
"reported": {
"sourceSha256": "bfff14bd820a2d7be90e82db6403d2415c0fb3de998cf61d7c3830ff4c415605",
"bundleSha256": "c8e429f7f35a4a61c3189fcedcac5abc9658dab841ea927cf285f90eeccd381c",
"samples": [
{
"streams": 1,
"slots": 16384,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 32768,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 49152,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 65536,
"retainedBytes": 0,
"observedBytes": 0
}
],
"behavior": {
"scheduled": 6,
"published": 5,
"publications": [
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": "hé"
}
]
}
}
],
"directScheduled": 7,
"emitted": [
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "two",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "two",
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"snapshot": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
}
],
"truncated": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
},
"controls": [
"actual Codex empty notification stream",
"empty snapshot remains present",
"four explicit flushes",
"Unicode text retained",
"forget clears",
"dispose clears",
"first empty publication and retries",
"emit receiver preserved",
"new empty key rejected under backpressure",
"accepted eviction",
"UTF-8 truncation",
"already-truncated empty append keeps no-new-publication behavior"
]
},
"reportedFixed": {
"sourceSha256": "2f00aec9b7a4fb24cad4e01d1bf7e1d0b881076f9760b322cdf72c39594ae89c",
"bundleSha256": "3749aae7c5eb00f3eea98c66a7b0c4e0e7114cde1d983d89951bd4c21b33e680",
"samples": [
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
}
],
"behavior": {
"scheduled": 6,
"published": 5,
"publications": [
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": "hé"
}
]
}
}
],
"directScheduled": 7,
"emitted": [
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "two",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "two",
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"snapshot": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
}
],
"truncated": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
},
"controls": [
"actual Codex empty notification stream",
"empty snapshot remains present",
"four explicit flushes",
"Unicode text retained",
"forget clears",
"dispose clears",
"first empty publication and retries",
"emit receiver preserved",
"new empty key rejected under backpressure",
"accepted eviction",
"UTF-8 truncation",
"already-truncated empty append keeps no-new-publication behavior"
]
}
},
"measurement": "Read-only closure observes private Map and chunk-array cardinalities in source overlay. No heap/RSS measurement, native process or affected-host inference."
}
@@ -0,0 +1,7 @@
--- a/src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.ts
+++ b/src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.ts
@@ -233 +233,3 @@
- current.push(delta)
+ if (delta.length > 0) {
+ current.push(delta)
+ }
@@ -0,0 +1,864 @@
{
"runtime": {
"node": "26.6.0",
"acorn": "8.17.0",
"ada": "4.0.0",
"amaro": "1.1.11",
"ares": "1.34.8",
"brotli": "1.2.0",
"cldr": "48.0",
"icu": "78.3",
"libffi": "3.7.1",
"llhttp": "9.4.3",
"merve": "1.2.2",
"modules": "147",
"napi": "10",
"nbytes": "0.1.4",
"ncrypto": "0.0.1",
"nghttp2": "1.70.0",
"nghttp3": "",
"ngtcp2": "",
"openssl": "3.6.3",
"simdjson": "4.6.6",
"simdutf": "7.7.0",
"sqlite": "3.53.4",
"tz": "2026a",
"undici": "8.9.0",
"unicode": "17.0",
"uv": "1.52.1",
"uvwasi": "0.0.23",
"v8": "14.6.202.34-node.26",
"zlib": "1.2.12",
"zstd": "1.5.7"
},
"sourceVersions": {
"main": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"reported": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"reportedTag": "v1.4.198"
},
"scope": "Exact current/main coalescer before/after plus exact v1.4.198 coalescer and same local guard, using current Codex stream callers/dependencies. Not a packaged historical release replay.",
"crlfLoaderControl": {
"reads": 3,
"equal": true
},
"artifactHashes": {
"sources.cjs": "7902098e4cd0e07825684c778d7d2af74ba1438a55ccf0b9ffe6eb96f76698c8",
"scenario.cjs": "66cfb53b821f631d2f57988c2fab5d28aa0da95fb4a6a3a2b8b643a82d233550",
"reproduce.cjs": "e55b45abda1a7c6078cc3fc867f2553cf9ed6ffea10fbffdb61c99494d797fe1",
"before.config.mjs": "a55e7576be2348edddc137af9c34fca4323bd060d325a1140ab0ede66618c6e9",
"source-versions.json": "1038900fcbc3310ae4b38eb8603d0eea3d6a417c3835a70f3537d27eee6debc9",
"fix.patch": "06f11750dcd64042b7f208b00ae0feb3e0bdea6d5db66b3823ce063fce8ab97a",
"reported.patch": "9d340925bd2a875334a1a4c3ce58c4ba0f977c66bf0c6f5d66848f862fc95d59"
},
"phases": {
"baseline": {
"sourceSha256": "48828e4ee21858075cbb87ec1caa4a82991a55f80928482615d1df7ec5dc0fb2",
"bundleSha256": "cbdd64b9e21f410645660ac33afe3bede8a58b7680641d5d98883facc0e6a120",
"samples": [
{
"streams": 1,
"slots": 16384,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 32768,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 49152,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 65536,
"retainedBytes": 0,
"observedBytes": 0
}
],
"behavior": {
"scheduled": 6,
"published": 5,
"publications": [
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": "hé"
}
]
}
}
],
"directScheduled": 7,
"emitted": [
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "two",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "two",
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"snapshot": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
}
],
"truncated": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
},
"controls": [
"actual Codex empty notification stream",
"empty snapshot remains present",
"four explicit flushes",
"Unicode text retained",
"forget clears",
"dispose clears",
"first empty publication and retries",
"emit receiver preserved",
"new empty key rejected under backpressure",
"accepted eviction",
"UTF-8 truncation",
"already-truncated empty append keeps no-new-publication behavior"
]
},
"fixed": {
"sourceSha256": "7caf0f24250b42e0ac4fe99e93bcbe0a4acb7cb3465fdde6b248d8bf6ba0d2d0",
"bundleSha256": "3e8b7e8430737f4cb0ca8454add7730770d8cd19ab175266d8b7626a059f3912",
"samples": [
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
}
],
"behavior": {
"scheduled": 6,
"published": 5,
"publications": [
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": "hé"
}
]
}
}
],
"directScheduled": 7,
"emitted": [
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "two",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "two",
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"snapshot": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
}
],
"truncated": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
},
"controls": [
"actual Codex empty notification stream",
"empty snapshot remains present",
"four explicit flushes",
"Unicode text retained",
"forget clears",
"dispose clears",
"first empty publication and retries",
"emit receiver preserved",
"new empty key rejected under backpressure",
"accepted eviction",
"UTF-8 truncation",
"already-truncated empty append keeps no-new-publication behavior"
]
},
"reported": {
"sourceSha256": "bfff14bd820a2d7be90e82db6403d2415c0fb3de998cf61d7c3830ff4c415605",
"bundleSha256": "c8e429f7f35a4a61c3189fcedcac5abc9658dab841ea927cf285f90eeccd381c",
"samples": [
{
"streams": 1,
"slots": 16384,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 32768,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 49152,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 65536,
"retainedBytes": 0,
"observedBytes": 0
}
],
"behavior": {
"scheduled": 6,
"published": 5,
"publications": [
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": "hé"
}
]
}
}
],
"directScheduled": 7,
"emitted": [
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "two",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "two",
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"snapshot": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
}
],
"truncated": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
},
"controls": [
"actual Codex empty notification stream",
"empty snapshot remains present",
"four explicit flushes",
"Unicode text retained",
"forget clears",
"dispose clears",
"first empty publication and retries",
"emit receiver preserved",
"new empty key rejected under backpressure",
"accepted eviction",
"UTF-8 truncation",
"already-truncated empty append keeps no-new-publication behavior"
]
},
"reportedFixed": {
"sourceSha256": "2f00aec9b7a4fb24cad4e01d1bf7e1d0b881076f9760b322cdf72c39594ae89c",
"bundleSha256": "3749aae7c5eb00f3eea98c66a7b0c4e0e7114cde1d983d89951bd4c21b33e680",
"samples": [
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
},
{
"streams": 1,
"slots": 0,
"retainedBytes": 0,
"observedBytes": 0
}
],
"behavior": {
"scheduled": 6,
"published": 5,
"publications": [
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": ""
}
]
}
},
{
"identity": {
"provider": "codex",
"threadId": "thread-a",
"turnId": "turn-a",
"ordinal": 0
},
"body": {
"kind": "message",
"role": "assistant",
"blocks": [
{
"type": "text",
"text": "hé"
}
]
}
}
],
"directScheduled": 7,
"emitted": [
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "one",
"text": "unchanged",
"snapshot": {
"text": "unchanged",
"observedBytes": 9,
"truncated": false
}
},
{
"key": "two",
"text": "",
"snapshot": {
"text": "",
"observedBytes": 0,
"truncated": false
}
},
{
"key": "two",
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"snapshot": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
}
],
"truncated": {
"text": "😀😀😀😀😀😀😀\n[Orca: streamed output truncated]",
"observedBytes": 400,
"truncated": true
}
},
"controls": [
"actual Codex empty notification stream",
"empty snapshot remains present",
"four explicit flushes",
"Unicode text retained",
"forget clears",
"dispose clears",
"first empty publication and retries",
"emit receiver preserved",
"new empty key rejected under backpressure",
"accepted eviction",
"UTF-8 truncation",
"already-truncated empty append keeps no-new-publication behavior"
]
}
},
"measurement": "Read-only closure observes private Map and chunk-array cardinalities in source overlay. No heap/RSS measurement, native process or affected-host inference."
}
@@ -0,0 +1,60 @@
--- a/src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.ts
+++ b/src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.ts
@@ -39,0 +40,2 @@
+ /** The caller byte-bounds protected metadata; only ordinary streams use the count cap. */
+ isProtected?: (key: string) => boolean
@@ -86,2 +88 @@
- const streamOrder = new Map<string, number>()
- let nextOrder = 0
+ const evictable = new Set<string>()
@@ -133,2 +134,2 @@
- if (streams.size >= maxStreams) {
- const oldest = [...streamOrder.entries()].sort((a, b) => a[1] - b[1])[0]?.[0]
+ while (!deps.isProtected?.(key) && evictable.size >= maxStreams) {
+ const oldest = evictable.values().next().value
@@ -135,0 +137,4 @@
+ if (deps.isProtected?.(oldest)) {
+ evictable.delete(oldest)
+ continue
+ }
@@ -146 +151 @@
- streamOrder.delete(oldest)
+ evictable.delete(oldest)
@@ -147,0 +153 @@
+ break
@@ -156 +162,5 @@
- streamOrder.set(key, nextOrder++)
+ if (!deps.isProtected?.(key)) {
+ evictable.add(key)
+ }
+ } else if (deps.isProtected?.(key)) {
+ evictable.delete(key)
@@ -158 +168,2 @@
- stream.observedBytes += Buffer.byteLength(delta, 'utf8')
+ const deltaBytes = Buffer.byteLength(delta, 'utf8')
+ stream.observedBytes += deltaBytes
@@ -165,0 +177 @@
+ deltaBytes,
@@ -187 +199 @@
- streamOrder.delete(key)
+ evictable.delete(key)
@@ -194 +206 @@
- streamOrder.clear()
+ evictable.clear()
@@ -213,0 +226 @@
+ deltaBytes: number,
@@ -217,2 +230 @@
- const deltaBuffer = Buffer.from(delta, 'utf8')
- if (deltaBuffer.byteLength <= available) {
+ if (deltaBytes <= available) {
@@ -221 +233,3 @@
- current.push(delta)
+ if (delta.length > 0) {
+ current.push(delta)
+ }
@@ -224 +238 @@
- retainedBytes: currentBytes + deltaBuffer.byteLength,
+ retainedBytes: currentBytes + deltaBytes,
@@ -232 +246 @@
- deltaBuffer
+ Buffer.from(delta, 'utf8')
@@ -0,0 +1,66 @@
const assert = require('node:assert/strict')
const { readFileSync, writeFileSync } = require('node:fs')
const path = require('node:path')
const { scenario } = require('./scenario.cjs')
const { loadSources, sha, versions } = require('./sources.cjs')
assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1', 'Run with ORCA_BACKGROUND_LAUNCH=1')
;(async () => {
const canonical = loadSources()
let crlfReads = 0
const crlf = loadSources((file) => {
crlfReads += 1
return readFileSync(file, 'utf8').replaceAll('\r\n', '\n').replaceAll('\n', '\r\n')
})
assert.deepEqual(crlf, canonical)
assert.equal(crlfReads, 3)
const phases = {}
for (const phase of ['baseline', 'fixed', 'reported', 'reportedFixed']) {
phases[phase] = await scenario(phase)
}
assert.deepEqual(phases.baseline.behavior, phases.fixed.behavior)
assert.deepEqual(phases.reported.behavior, phases.reportedFixed.behavior)
const artifactHashes = Object.fromEntries(
[
'sources.cjs',
'scenario.cjs',
'reproduce.cjs',
'before.config.mjs',
'source-versions.json',
'fix.patch',
'reported.patch'
].map((file) => [file, sha(readFileSync(path.join(__dirname, file)))])
)
const result = {
runtime: process.versions,
sourceVersions: versions.namedReferences,
scope: versions.scope,
crlfLoaderControl: { reads: crlfReads, equal: true },
artifactHashes,
phases,
measurement:
'Read-only closure observes private Map and chunk-array cardinalities in source overlay. No heap/RSS measurement, native process or affected-host inference.'
}
const output = process.argv[2] ?? path.join(__dirname, 'node-results.json')
writeFileSync(output, `${JSON.stringify(result, null, 2)}\n`)
console.log(
JSON.stringify({
output,
phases: Object.fromEntries(
Object.entries(phases).map(([phase, result]) => [
phase,
{
samples: result.samples,
scheduled: result.behavior.scheduled,
published: result.behavior.published
}
])
),
behaviorEqual: true
})
)
})().catch((error) => {
console.error(error)
process.exitCode = 1
})
@@ -0,0 +1,156 @@
const assert = require('node:assert/strict')
const { load } = require('./sources.cjs')
async function scenario(phase) {
const readers = []
globalThis.__orcaEmptyDeltaReaders = readers
const {
createCodexStructuredItemStreams,
createAgentSessionDeltaCoalescer,
sourceSha256,
bundleSha256
} = await load(phase)
const fixed = phase === 'fixed' || phase === 'reportedFixed'
let scheduled = 0
let published = 0
const publications = []
const streams = createCodexStructuredItemStreams({
sink: {
appendItem(identity, body) {
published += 1
publications.push({ identity, body })
},
publish() {}
},
identityFor: () => ({ provider: 'codex', threadId: 'thread-a', turnId: 'turn-a', ordinal: 0 }),
schedule: () => {
scheduled += 1
return () => {}
}
})
assert.equal(readers.length, 1)
const read = readers[0]
const samples = []
for (let batch = 0; batch < 4; batch += 1) {
for (let index = 0; index < 16384; index += 1) {
assert.deepEqual(
streams.handle('thread-a', 'item/agentMessage/delta', { itemId: 'item-a', delta: '' }),
{ handled: true, admission: { accepted: true } }
)
}
assert.equal(streams.flush(), true)
samples.push(read())
assert.deepEqual(streams.snapshot('thread-a', 'item-a'), {
text: '',
observedBytes: 0,
truncated: false
})
}
assert.equal(read().slots, fixed ? 0 : 65536)
assert.equal(read().retainedBytes, 0)
assert.equal(read().observedBytes, 0)
streams.handle('thread-a', 'item/agentMessage/delta', { itemId: 'item-a', delta: 'hé' })
assert.equal(streams.flush(), true)
assert.deepEqual(streams.snapshot('thread-a', 'item-a'), {
text: 'hé',
observedBytes: 3,
truncated: false
})
assert.equal(read().slots, fixed ? 1 : 65537)
streams.forget('thread-a', 'item-a')
assert.deepEqual(read(), { streams: 0, slots: 0, retainedBytes: 0, observedBytes: 0 })
streams.handle('thread-a', 'item/agentMessage/delta', {
itemId: 'item-a',
delta: 'retained until dispose'
})
streams.dispose()
assert.deepEqual(read(), { streams: 0, slots: 0, retainedBytes: 0, observedBytes: 0 })
let accepting = false
const pending = new Set()
const emitted = []
let directScheduled = 0
const deps = {
emit(key, text, snapshot) {
assert.equal(this, deps)
if (!accepting) {
return false
}
emitted.push({ key, text, snapshot })
return true
},
schedule(run) {
directScheduled += 1
pending.add(run)
return () => pending.delete(run)
},
maxStreams: 1,
maxRetainedBytes: 64,
maxTotalRetainedBytes: 64
}
const direct = createAgentSessionDeltaCoalescer(deps)
assert.equal(direct.append('one', ''), true)
assert.deepEqual(direct.snapshot('one'), { text: '', observedBytes: 0, truncated: false })
assert.equal(pending.size, 1)
assert.equal(direct.flushAll(), false)
assert.equal(pending.size, 1)
assert.equal(direct.append('two', ''), false)
assert.equal(direct.snapshot('two'), null)
accepting = true
assert.equal(direct.flushAll(), true)
assert.equal(pending.size, 0)
assert.equal(direct.append('one', ''), true)
assert.equal(direct.flushAll(), true)
assert.equal(emitted.length, 2)
assert.equal(direct.append('one', 'unchanged'), true)
assert.equal(direct.flushAll(), true)
accepting = false
direct.append('one', '')
assert.equal(direct.append('two', ''), false)
assert.deepEqual(direct.snapshot('one'), {
text: 'unchanged',
observedBytes: 9,
truncated: false
})
accepting = true
assert.equal(direct.append('two', ''), true)
assert.equal(direct.snapshot('one'), null)
assert.equal(direct.flushAll(), true)
direct.append('two', '😀'.repeat(100))
assert.equal(direct.flushAll(), true)
const truncated = direct.snapshot('two')
assert.ok(Buffer.byteLength(truncated.text, 'utf8') <= 64)
assert.equal(truncated.truncated, true)
assert.equal(truncated.observedBytes, 400)
const publicationsBeforeEmpty = emitted.length
direct.append('two', '')
assert.equal(direct.flushAll(), true)
assert.equal(emitted.length, publicationsBeforeEmpty)
assert.deepEqual(direct.snapshot('two'), truncated)
direct.dispose()
assert.equal(pending.size, 0)
assert.deepEqual(readers[1](), { streams: 0, slots: 0, retainedBytes: 0, observedBytes: 0 })
delete globalThis.__orcaEmptyDeltaReaders
return {
sourceSha256,
bundleSha256,
samples,
behavior: { scheduled, published, publications, directScheduled, emitted, truncated },
controls: [
'actual Codex empty notification stream',
'empty snapshot remains present',
'four explicit flushes',
'Unicode text retained',
'forget clears',
'dispose clears',
'first empty publication and retries',
'emit receiver preserved',
'new empty key rejected under backpressure',
'accepted eviction',
'UTF-8 truncation',
'already-truncated empty append keeps no-new-publication behavior'
]
}
}
module.exports = { scenario }
@@ -0,0 +1,77 @@
{
"sourcePath": "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.ts",
"canonicalization": "CRLF to LF",
"baselineSha256": "48828e4ee21858075cbb87ec1caa4a82991a55f80928482615d1df7ec5dc0fb2",
"fixedSha256": "7caf0f24250b42e0ac4fe99e93bcbe0a4acb7cb3465fdde6b248d8bf6ba0d2d0",
"reportedSha256": "bfff14bd820a2d7be90e82db6403d2415c0fb3de998cf61d7c3830ff4c415605",
"reportedFixedSha256": "2f00aec9b7a4fb24cad4e01d1bf7e1d0b881076f9760b322cdf72c39594ae89c",
"namedReferences": {
"main": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
"reported": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
"reportedTag": "v1.4.198"
},
"commonDependencies": {
"src/shared/agent-session-journal-item-key.ts": "09ebe4758e3f38b5b7591f07f7b4dbfd04c7cc01713c9b0afe03b07b656d1fe8",
"src/main/codex/codex-command-lifecycle.ts": "7b480d3b111304e3caff71768fb3b11ab6c36e1fa8f5b0440623a77cac2382c2",
"src/shared/native-chat-turn-status.ts": "4ce2b64fa7818106814c135e5daf77bb183f601c35f9f08e5b7ff847fba01378",
"src/shared/native-chat-tool-identity.ts": "8c98eec1a53b26a67ed48a0859c5863f5accbe67e013c4eb5a0f8b32441ee925",
"src/main/codex/codex-structured-item-stream-bounds.ts": "88e9efa8a9c163905f1a5eb5eaf316fba2f194d76c5a94ed7f55636cc0d74ae0",
"src/main/codex/codex-item-stream-retention.ts": "66d1f1d51ed6711e20f81698b14be2d003e899dbc587ac1306b6a656cf951b96",
"src/main/native-chat/agent-session-journal/journal-payload-bounds.ts": "a421da6c6eee9346746a69f8ba7ff966ce81969dfa0a93f58784cac27cf8cdad",
"src/main/codex/codex-goal-journal-rows.ts": "415846ca105300349778da7637216ca6ce978c7036219f6b3038f7228ba8724e",
"src/main/codex/codex-subagent-activity.ts": "5cdf18a4ea2c68178a67a845d4c95672a0d94bb282e8e15c3a930c6dd59151e4",
"src/main/native-chat/agent-session-wire/provider-frame-disposition.ts": "f24008cebb1b8a77b0cbd091e49140a4bf99c676bc61eedeb0a82f1263244a7c",
"src/main/native-chat/agent-session-wire/unhandled-provider-frame.ts": "f8169a024a573adfc97e4eb8c99ecc4e80f82e9f94a04a29011a6b8051cc7626",
"src/shared/raster-image-dimensions.ts": "0d7462b8b2e53bfcdcbd32803edcfb85ed0063f56d45cf230fb59aac4783d063",
"src/shared/raster-image-preview-limits.ts": "f2d836b354b3951912f1ec689d84b82aa1aade89ed8e9c6f76be2321d600e9a7",
"src/shared/raster-image-base64-preview.ts": "89baf40f4637615456888e0f35115517a8445a3b1cb640b7bc495dd9a98fcedb",
"src/shared/image-data-uri.ts": "3eb9d2bda499b8f10c74783f9de766041469b140a9b96b2b5be7d97528688ce0",
"src/main/codex/codex-item-field-readers.ts": "aa4b178c562a63995b7de63c6f5daaf7b6b610fb163fb0a357cc230314076323",
"src/main/codex/codex-image-item-translation.ts": "c4f88236c707472cbcfcdfb1a04e51c738f8741aa5847a33f730759499522f5e",
"src/main/codex/codex-command-action-class.ts": "93b7bceb66a9e5bfae680e018b61bddb5c1117fe26243088ceccc8738841f966",
"src/main/codex/codex-thread-item-identity.ts": "04b98b367a4d658fb759ebc4fc73ea4ae731ae3f238f191c8ce002b2a2c9e2a8",
"src/main/codex/codex-turn-ordinals.ts": "9b7cf66986235bcacd6c198950452c7f40bd559a3a0ce20995aac496a1e32d7f",
"src/main/codex/codex-structured-item-translation.ts": "8ab9debb279baf3a5043ffe754c8911755c6e4ca9edfc0a094671c4d5d166639",
"src/main/codex/codex-structured-item-stream-events.ts": "da2a9da9025af354ce705a190badadf822f9ac3b403eef04aa0ef8aaf6f023de",
"src/main/codex/codex-structured-item-streams.ts": "b70c3306e8bec3587342afc3bc156f40903513fa7ff3865b217ba4cc912cd89f"
},
"callerSourceHashes": [
{
"path": "src/main/codex/codex-structured-item-streams.ts",
"working": "b70c3306e8bec3587342afc3bc156f40903513fa7ff3865b217ba4cc912cd89f",
"main": "b70c3306e8bec3587342afc3bc156f40903513fa7ff3865b217ba4cc912cd89f",
"reported": "0f05fd8232d5f9b7a928abdd97ea04846ffade9512d61371f120dbcff00c09b6"
},
{
"path": "src/main/codex/codex-structured-journal-translation.ts",
"working": "56e17b9649471e804d29d1d54a3e14b4298ca8af2e25c4759dc37fce000b06a1",
"main": "56e17b9649471e804d29d1d54a3e14b4298ca8af2e25c4759dc37fce000b06a1",
"reported": "7a6409082b977e481b137b19f446ee3e17d530f4f72c4d141263a49d3ca7722c"
},
{
"path": "src/main/codex/codex-structured-provider-events.ts",
"working": "a411dc378e521ea8fc915e2a2a21476341a30f440e8233ae5649205d2a1e2e44",
"main": "a411dc378e521ea8fc915e2a2a21476341a30f440e8233ae5649205d2a1e2e44",
"reported": "69af127e2d2ee6b6d648028f16f3e6d25ea45ed61919d14642a7a554e3ad05a5"
},
{
"path": "src/main/codex/codex-app-server-notification-schema.ts",
"working": "d551b5fb47aad42fb09d9cee2c68e1e4438d209e13869875fed68c3679ef66c8",
"main": "d551b5fb47aad42fb09d9cee2c68e1e4438d209e13869875fed68c3679ef66c8",
"reported": "d551b5fb47aad42fb09d9cee2c68e1e4438d209e13869875fed68c3679ef66c8"
},
{
"path": "src/main/claude/claude-streamed-text-checkpoints.ts",
"working": "a0e435f5fcf73f37e48e5363f8f9245d992a0d6c0a80abdf5cc5c72b2595fe6d",
"main": "a0e435f5fcf73f37e48e5363f8f9245d992a0d6c0a80abdf5cc5c72b2595fe6d",
"reported": "a0e435f5fcf73f37e48e5363f8f9245d992a0d6c0a80abdf5cc5c72b2595fe6d"
},
{
"path": "src/main/claude/claude-streamed-block-identity.ts",
"working": "8bc2d4a18fdecd4e27fdf2e8ea3274fcf2e3353edb75607bd0d1133546bfbdc0",
"main": "8bc2d4a18fdecd4e27fdf2e8ea3274fcf2e3353edb75607bd0d1133546bfbdc0",
"reported": "8bc2d4a18fdecd4e27fdf2e8ea3274fcf2e3353edb75607bd0d1133546bfbdc0"
}
],
"scope": "Exact current/main coalescer before/after plus exact v1.4.198 coalescer and same local guard, using current Codex stream callers/dependencies. Not a packaged historical release replay."
}
@@ -0,0 +1,111 @@
const assert = require('node:assert/strict')
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const path = require('node:path')
const Module = require('node:module')
const esbuild = require('esbuild')
const { applyPatch, parsePatch, reversePatch } = require('diff')
const root = path.resolve(__dirname, '../../..')
const canonical = (value) => value.replaceAll('\r\n', '\n')
const sha = (value) => createHash('sha256').update(value).digest('hex')
const readText = (file) => canonical(readFileSync(file, 'utf8'))
const versions = JSON.parse(readText(path.join(__dirname, 'source-versions.json')))
function loadSources(read = readText) {
const fixed = canonical(read(path.join(root, versions.sourcePath)))
assert.equal(sha(fixed), versions.fixedSha256, 'Fixed source drift')
const reverse = (name) => {
const patches = parsePatch(canonical(read(path.join(__dirname, name))))
assert.equal(patches.length, 1)
assert.equal(patches[0].newFileName, `b/${versions.sourcePath}`)
const source = applyPatch(fixed, reversePatch(patches[0]))
assert.notEqual(source, false)
return source
}
const baseline = reverse('fix.patch')
const reported = reverse('reported.patch')
const marker = ' current.push(delta)'
assert.equal(reported.split(marker).length, 2)
const reportedFixed = reported.replace(
marker,
' if (delta.length > 0) {\n current.push(delta)\n }'
)
assert.equal(sha(baseline), versions.baselineSha256, 'Baseline source drift')
assert.equal(sha(reported), versions.reportedSha256, 'Reported source drift')
assert.equal(sha(reportedFixed), versions.reportedFixedSha256)
return { baseline, fixed, reported, reportedFixed }
}
async function load(phase) {
const sources = loadSources()
assert.ok(Object.hasOwn(sources, phase))
for (const [file, expected] of Object.entries(versions.commonDependencies)) {
assert.equal(sha(readText(path.join(root, file))), expected, `Dependency drift: ${file}`)
}
for (const caller of versions.callerSourceHashes) {
assert.equal(
sha(readText(path.join(root, caller.path))),
caller.working,
`Caller drift: ${caller.path}`
)
}
const marker = ' const flushKey = (key: string): boolean => {'
const source = sources[phase]
assert.equal(source.split(marker).length, 2)
// Measurement only reads cardinalities; it never changes stream ownership or contents.
const measured = source.replace(
marker,
` globalThis.__orcaEmptyDeltaReaders.push(() => ({
streams: streams.size,
slots: [...streams.values()].reduce((count, stream) => count + stream.chunks.length, 0),
retainedBytes: totalRetainedBytes,
observedBytes: [...streams.values()].reduce((count, stream) => count + stream.observedBytes, 0)
}))\n${marker}`
)
const build = await esbuild.build({
stdin: {
contents:
"export { createCodexStructuredItemStreams } from './src/main/codex/codex-structured-item-streams'; export { createAgentSessionDeltaCoalescer } from './src/main/native-chat/agent-session-wire/agent-session-delta-coalescer'",
resolveDir: root,
loader: 'ts'
},
absWorkingDir: root,
bundle: true,
platform: 'node',
format: 'cjs',
packages: 'external',
write: false,
metafile: true,
plugins: [
{
name: 'read-private-array-cardinality',
setup(builder) {
builder.onLoad({ filter: /agent-session-delta-coalescer\.ts$/ }, (args) => {
assert.equal(args.path, path.join(root, versions.sourcePath))
return { contents: measured, loader: 'ts' }
})
}
}
]
})
const actualInputs = Object.keys(build.metafile.inputs)
.filter((file) => file.startsWith('src/'))
.sort()
assert.deepEqual(
actualInputs,
[...Object.keys(versions.commonDependencies), versions.sourcePath].sort()
)
const filename = path.join(__dirname, `in-memory-${phase}.cjs`)
const loaded = new Module(filename, module)
loaded.filename = filename
loaded.paths = Module._nodeModulePaths(root)
loaded._compile(build.outputFiles[0].text, filename)
return {
...loaded.exports,
sourceSha256: sha(source),
bundleSha256: sha(build.outputFiles[0].contents)
}
}
module.exports = { load, loadSources, root, sha, versions }
@@ -0,0 +1,50 @@
{
"reviewedHead": "a8c4bed3fa4191d731bb28826d00318f18a5db0a",
"backgroundLaunch": "ORCA_BACKGROUND_LAUNCH=1 on every check",
"productHashes": {
"src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.ts": "7caf0f24250b42e0ac4fe99e93bcbe0a4acb7cb3465fdde6b248d8bf6ba0d2d0",
"src/main/native-chat/agent-session-wire/agent-session-empty-delta-retention.test.ts": "2e4ed51b8c205e650cd0d3d4fcb968fdf8f509be4c4013ca1dc6e70b59703a55"
},
"focusedTests": {
"files": 6,
"passed": 64,
"failed": 0,
"paths": [
"src/main/native-chat/agent-session-wire/agent-session-empty-delta-retention.test.ts",
"src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.test.ts",
"src/main/codex/codex-persistent-command-retention.test.ts",
"src/main/codex/codex-structured-journal-translation.test.ts",
"src/main/codex/codex-structured-journal-translation-streams.test.ts",
"src/main/claude/claude-streamed-text-checkpoints.test.ts"
]
},
"baselineTests": {
"config": "docs/audits/empty-streamed-delta-retention/before.config.mjs",
"passed": 14,
"expectedFailures": 1,
"failureName": "empty streamed deltas does not retain empty array slots through repeated Codex publications",
"assertion": "expected 65537 to be 1",
"scope": "Only the new retained-slot regression fails; all publication/backpressure controls pass."
},
"checks": {
"nodeTypecheck": "passed: pnpm tc:node",
"ordinaryLint": "passed: oxlint on two product files",
"typeAwareLint": "passed: oxlint --type-aware --config config/oxlint-code-quality-type-aware.json --deny-warnings on two product files",
"changedQuality": "passed: pnpm run check:code-quality:changed HEAD, two files, zero new findings",
"format": "passed: oxfmt product and artifact files",
"diffWhitespace": "git diff --check on exact product paths; zero-context historical/fix patches"
},
"proofReports": {
"node-results.json": "a858b703a1c94c2bc4bc817f244a76fb84bd4ea4cb2dbbb6688acef91d25905d",
"electron-results.json": "1ba7c0dd598244cc8c5af9643823470198bf4409deae60939f13dc5dc4b716d5"
},
"independentReview": "rpc_queue_retention reviewed actual source, tests, named hashes and behavior parity; separately reran all 15 coalescer/new tests.",
"ciArtifactCorrection": {
"pullRequest": 21142,
"failedHead": "9ed7c4f5c880ab64dda8a1a8a04ca8455af42b5d",
"failedJob": "https://github.com/stablyai/orca/actions/runs/35178713175/job/105066129816",
"cause": "One-line if in scenario.cjs lacked braces. Product-only local quality omitted durable proof sources; CI correctly rejected it.",
"change": "Add braces; product code unchanged. Rerun both actual-source runtime reports and all five quality scan configurations over all six published code files with --no-ignore.",
"result": "Both runtime proofs and all five quality scans pass. CI status remains separately recorded at the observed head."
}
}
@@ -0,0 +1,39 @@
# Retire obsolete GitLab known-host generations
Each successful `getGlabKnownHosts` probe previously stored a host-list array under a connection ID plus its SSH provider generation. Reconnecting under the same ID created a new entry while every earlier successful generation remained cached until an explicit preflight reset. The cache now keeps one successful generation per observed execution identity.
Async publication also uses the existing coalescer's `ownsKey()` and checks the current SSH generation. A result completing after reconnect, explicit reset, or replacement by a newer probe cannot recreate retired cache state. Original callers can still receive their own completed result. Explicitly remembered hosts, native/WSL separation, command routing and existing probe timeouts are preserved.
## Evidence
The runner bundles the actual cache, coalescer and parser. Only command-result and SSH-generation ports are controlled; it opens no SSH connection and runs no GitLab command. It reverses `fix.patch` in memory, verifies the original source hash, and compares that baseline against the unmodified current product source. Reports include product, dependency, regression-test and fixture hashes.
| Control | Original | Fixed |
| --- | --- | --- |
| Successful result arrays retained after 128 generations | 128 | 1 current array |
| Remembered result arrays retained after 16 generations | 16 | 1 current array |
| Delayed old-generation result after a successor answers | Still retained | Collectable; successor preserved |
| Explicit reset followed by old completion | Old result repopulates cache | Next lookup executes a fresh probe |
| Abandoned probe finishes after its replacement | Old host added to replacement cache | Replacement remains unchanged |
| Explicit reset after retention exercise | 0 original arrays retained | 0 original arrays retained |
Both phases preserve remembered-host updates while probes succeed or fail and isolate native, Ubuntu WSL, Debian WSL and two connection IDs. `results.json` records Node26.6; `electron-results.json` records installed Electron43.7 / Node24.21 running without an app window. Both runs pass all controls. This is compatibility evidence, not a historical packaged-binary reproduction.
```sh
ORCA_BACKGROUND_LAUNCH=1 node --expose-gc docs/audits/gitlab-known-host-retirement/reproduce.cjs
ORCA_BACKGROUND_LAUNCH=1 node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/gitlab/gitlab-known-host-retirement.test.ts src/main/gitlab/gitlab-known-host-probe.test.ts src/main/gitlab/gitlab-known-host-probe-wsl-fallback.test.ts src/main/git/coalesced-probe.test.ts src/main/gitlab/client-mr-auth-rate-limit.test.ts
```
For the installed macOS Electron binary:
```sh
ELECTRON_RUN_AS_NODE=1 ORCA_BACKGROUND_LAUNCH=1 node_modules/electron/dist/Electron.app/Contents/MacOS/Electron --expose-gc docs/audits/gitlab-known-host-retirement/reproduce.cjs docs/audits/gitlab-known-host-retirement/electron-results.json
```
The runner is portable; that executable path is macOS-specific. Thirty-two focused tests pass, including eight new retention/lifecycle/scope controls. Running those eight against the original source produces six failures and two passing controls. Node typecheck, focused lint (including artifact type-aware/casting scans), and the changed-code quality gate pass. The original product module and reused coalescer match named main `291b4ddd6f1c1af480169885e0fda7f9c78ff053`.
## Limits and incident mapping
This removes small metadata retained across SSH generations. Distinct historical execution identities may still keep one entry each until reset; this change does not impose a new cache cap or alter connection/provider lifetime. One generation's host list remains input-sized.
The demonstrated accumulation requires changing SSH provider generations, so it cannot explain [#19831](https://github.com/stablyai/orca/issues/19831)'s reported all-local session. No affected-host observation ties it to another OOM report. The proof measures reachable result arrays, not RSS or gigabytes of incident memory.
@@ -0,0 +1,89 @@
{
"sourceHashes": {
"src/main/gitlab/gitlab-known-host-probe.ts": {
"baseline": "4f9651c5a383438aa968aec082c200a3371e2f4317372125aa11aa6938793c25",
"fixed": "ecf0d67f68cf4b7b1bc7d6ff19a1815a8b95fc9721962d3cd873702f98831bad"
},
"src/main/git/coalesced-probe.ts": "e5a13820a7d8b5f501a3804961ea26526bd6ad9144ecf597d5d83a70d81885e1",
"src/main/git/remote-ref-probe-cache.ts": "d5cbfd97b30e72b03c0d28d8b9e75a2ae1f9e3efc9f5b5570c78e0742c0582dd",
"src/main/gitlab/project-ref-parser.ts": "0f8b6758e6a162a58f436ca4213addc42bf6ceb3fcc2e63e8c7402c844af9303",
"src/main/gitlab/gitlab-known-host-retirement.test.ts": "c6dedbac0462cae22903805d0a89be397a2b9d122a29640b6a07a2d274f8e98e"
},
"proofHashes": {
"reproduce.cjs": "19f49406685923b050a99fbc92b02b9f6f5f8d8f308dcc14336415a0988679cc",
"sources.cjs": "2ee8ac0f295d65f16489da2b0c03800b9db4e88c83ecfb63ae7be1ec3ea6c148",
"fix.patch": "695c929087006b3406ce2c30a7ab783d88af3f9675f4ea320dc7f98b8476be9f",
"original-source-hashes.json": "f9770d9f0a87afc9231eef6af99e8ded33348fdc21ba46a70b1d5d3388874b97"
},
"runtime": {
"node": "24.21.0",
"acorn": "8.18.0",
"ada": "4.0.0",
"amaro": "1.1.11",
"ares": "1.34.8",
"brotli": "1.2.0",
"cldr": "48.0",
"icu": "78.2",
"llhttp": "9.4.3",
"merve": "1.2.2",
"modules": "148",
"napi": "10",
"nbytes": "0.1.4",
"ncrypto": "0.0.1",
"nghttp2": "1.70.0",
"nghttp3": "",
"ngtcp2": "",
"openssl": "0.0.0",
"simdjson": "4.6.7",
"simdutf": "7.7.0",
"sqlite": "3.53.4",
"tz": "2025c",
"undici": "7.29.1",
"unicode": "17.0",
"uv": "1.52.1",
"uvwasi": "0.0.23",
"v8": "15.0.245.31-electron.0",
"zlib": "1.3.2.1-motley",
"zstd": "1.6.0",
"electron": "43.7.0",
"chrome": "150.0.7871.250"
},
"phases": {
"baseline": {
"retained": {
"retained": 128,
"afterReset": 0
},
"rememberedGenerationsRetained": 16,
"oldGeneration": {
"oldResultRetained": true,
"replacementHostsPreserved": true
},
"reset": {
"hosts": ["gitlab.com", "old-before-reset.test"],
"calls": 1
},
"abandoned": ["gitlab.com", "replacement.test", "abandoned.test"],
"rememberedSuccessAndFailure": "passed",
"nativeWslConnectionIsolation": "passed"
},
"fixed": {
"retained": {
"retained": 1,
"afterReset": 0
},
"rememberedGenerationsRetained": 1,
"oldGeneration": {
"oldResultRetained": false,
"replacementHostsPreserved": true
},
"reset": {
"hosts": ["gitlab.com", "fresh-after-reset.test"],
"calls": 2
},
"abandoned": ["gitlab.com", "replacement.test"],
"rememberedSuccessAndFailure": "passed",
"nativeWslConnectionIsolation": "passed"
}
}
}
@@ -0,0 +1,103 @@
diff --git a/src/main/gitlab/gitlab-known-host-probe.ts b/src/main/gitlab/gitlab-known-host-probe.ts
index 752e1b0291..d328cd38ca 100644
--- a/src/main/gitlab/gitlab-known-host-probe.ts
+++ b/src/main/gitlab/gitlab-known-host-probe.ts
@@ -12,7 +12,10 @@ export type LocalGitExecOptions = {
const GLAB_KNOWN_HOSTS_TIMEOUT_MS = 10_000
const UNAUTHENTICATED_HOSTS_MAX_ENTRIES = 128
-const knownHostsCacheByExecutionContext = new Map<string, readonly string[]>()
+const knownHostsCacheByExecutionContext = new Map<
+ string,
+ { key: string; hosts: readonly string[] }
+>()
const knownHostsInFlightByExecutionContext: CoalescedProbes<readonly string[]> = new Map()
const unauthenticatedHostExpiries = new Map<string, number>()
@@ -27,6 +30,19 @@ function knownHostsExecutionKey(
return localGitOptions.wslDistro ? `wsl:${localGitOptions.wslDistro}` : 'native'
}
+function knownHostsCacheContext(
+ connectionId?: string | null,
+ localGitOptions: LocalGitExecOptions = {}
+): { key: string; cacheKey: string } {
+ const key = knownHostsExecutionKey(connectionId, localGitOptions)
+ const cacheKey = connectionId ? `connection:${connectionId}` : key
+ const cached = knownHostsCacheByExecutionContext.get(cacheKey)
+ if (cached && cached.key !== key) {
+ knownHostsCacheByExecutionContext.delete(cacheKey)
+ }
+ return { key, cacheKey }
+}
+
/** @internal - exposed for tests only */
export function _resetKnownHostsCache(): void {
knownHostsCacheByExecutionContext.clear()
@@ -103,8 +119,8 @@ export function rememberGlabKnownHosts(
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
): void {
- const key = knownHostsExecutionKey(connectionId, localGitOptions)
- const cached = knownHostsCacheByExecutionContext.get(key) ?? DEFAULT_GITLAB_HOSTS
+ const { key, cacheKey } = knownHostsCacheContext(connectionId, localGitOptions)
+ const cached = knownHostsCacheByExecutionContext.get(cacheKey)?.hosts ?? DEFAULT_GITLAB_HOSTS
const seen = new Set(cached.map(normalizeGitLabHost))
const additions: string[] = []
for (const host of hosts) {
@@ -121,27 +137,29 @@ export function rememberGlabKnownHosts(
if (additions.length === 0) {
return
}
- knownHostsCacheByExecutionContext.set(key, [...cached, ...additions])
+ knownHostsCacheByExecutionContext.set(cacheKey, { key, hosts: [...cached, ...additions] })
}
export async function getGlabKnownHosts(
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
): Promise<readonly string[]> {
- const key = knownHostsExecutionKey(connectionId, localGitOptions)
- const cached = knownHostsCacheByExecutionContext.get(key)
+ const { key, cacheKey } = knownHostsCacheContext(connectionId, localGitOptions)
+ const cached = knownHostsCacheByExecutionContext.get(cacheKey)?.hosts
if (cached) {
return cached
}
// Why: only join a probe still young enough to answer, so a wedged one cannot
// pin every later retry for the life of the process (P1-D).
- return runCoalescedProbe(knownHostsInFlightByExecutionContext, key, () =>
- probeGlabKnownHosts(key, connectionId, localGitOptions)
+ return runCoalescedProbe(knownHostsInFlightByExecutionContext, key, (ownsKey) =>
+ probeGlabKnownHosts(key, cacheKey, ownsKey, connectionId, localGitOptions)
)
}
async function probeGlabKnownHosts(
key: string,
+ cacheKey: string,
+ ownsKey: () => boolean,
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
): Promise<readonly string[]> {
@@ -160,13 +178,17 @@ async function probeGlabKnownHosts(
...(localGitOptions.admissionTier ? { admissionTier: localGitOptions.admissionTier } : {})
})
const hosts = parseGlabAuthStatusHosts(`${stdout}\n${stderr}`)
- const remembered = knownHostsCacheByExecutionContext.get(key) ?? []
+ const cached = knownHostsCacheByExecutionContext.get(cacheKey)
+ const remembered = cached?.key === key ? cached.hosts : []
const merged = Array.from(new Set([...DEFAULT_GITLAB_HOSTS, ...remembered, ...hosts]))
- knownHostsCacheByExecutionContext.set(key, merged)
+ if (ownsKey() && knownHostsExecutionKey(connectionId, localGitOptions) === key) {
+ knownHostsCacheByExecutionContext.set(cacheKey, { key, hosts: merged })
+ }
return merged
} catch {
// Keep failures uncached so auth or tunnel recovery is discovered later.
- return knownHostsCacheByExecutionContext.get(key) ?? [...DEFAULT_GITLAB_HOSTS]
+ const cached = knownHostsCacheByExecutionContext.get(cacheKey)
+ return cached?.key === key ? cached.hosts : [...DEFAULT_GITLAB_HOSTS]
}
}
@@ -0,0 +1,3 @@
{
"src/main/gitlab/gitlab-known-host-probe.ts": "4f9651c5a383438aa968aec082c200a3371e2f4317372125aa11aa6938793c25"
}
@@ -0,0 +1,257 @@
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const Module = require('node:module')
const esbuild = require('esbuild')
const { sourcePath, baseline, fixed, sourceHashes, hash } = require('./sources.cjs')
assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1')
assert.equal(typeof global.gc, 'function')
const symbol = Symbol.for('orca-known-host-comparison')
const context = { generation: 1, calls: 0, runner: null }
globalThis[symbol] = context
const resultFor = (host) => ({ stdout: `Logged in to ${host} as user`, stderr: '' })
async function load(phase) {
const source = phase === 'baseline' ? baseline : fixed
const built = await esbuild.build({
entryPoints: [sourcePath],
bundle: true,
platform: 'node',
format: 'cjs',
write: false,
plugins: [
{
name: 'actual-cache-with-fixture-ports',
setup(build) {
build.onLoad({ filter: /gitlab-known-host-probe\.ts$/ }, () => ({
contents: source,
loader: 'ts'
}))
build.onResolve({ filter: /\/(runner|ssh-git-dispatch)$/ }, (args) => ({
path: path.basename(args.path),
namespace: 'ports'
}))
build.onLoad({ filter: /.*/, namespace: 'ports' }, (args) => ({
loader: 'js',
contents: `const context=globalThis[Symbol.for('orca-known-host-comparison')];${
args.path === 'runner'
? `exports.glabExecFileAsync=(...args)=>{context.calls++;return context.runner(...args)};`
: `exports.getSshGitProviderGeneration=()=>context.generation;`
}`
}))
}
}
]
})
const loaded = new Module(sourcePath, module)
loaded.filename = sourcePath
loaded.paths = module.paths
loaded._compile(built.outputFiles[0].text, sourcePath)
return loaded.exports
}
async function collect() {
for (let index = 0; index < 6; index++) {
await new Promise((resolve) => setImmediate(resolve))
global.gc()
}
await new Promise((resolve) => setImmediate(resolve))
}
async function rememberResult(api) {
const hosts = await api.getGlabKnownHosts('same-connection')
assert.deepEqual(hosts, ['gitlab.com', `host${context.generation}.test`])
return new WeakRef(hosts)
}
async function retention(api) {
api._resetKnownHostsCache()
context.calls = 0
context.runner = async () => resultFor(`host${context.generation}.test`)
const refs = []
for (let generation = 1; generation <= 128; generation++) {
context.generation = generation
refs.push(await rememberResult(api))
}
await collect()
const retained = refs.filter((ref) => ref.deref() !== undefined).length
await rememberResult(api)
assert.equal(context.calls, 128)
api._resetKnownHostsCache()
await collect()
const afterReset = refs.filter((ref) => ref.deref() !== undefined).length
assert.equal(afterReset, 0)
return { retained, afterReset }
}
async function afterReset(api) {
api._resetKnownHostsCache()
context.calls = 0
const pending = Promise.withResolvers()
context.runner = () => pending.promise
const old = api.getGlabKnownHosts()
api._resetKnownHostsCache()
context.runner = async () => resultFor('fresh-after-reset.test')
pending.resolve(resultFor('old-before-reset.test'))
assert.deepEqual(await old, ['gitlab.com', 'old-before-reset.test'])
return { hosts: await api.getGlabKnownHosts(), calls: context.calls }
}
async function weakResult(promise) {
return new WeakRef(await promise)
}
async function lateGeneration(api) {
api._resetKnownHostsCache()
context.generation = 1
const pending = Promise.withResolvers()
context.runner = () => pending.promise
let old = api.getGlabKnownHosts('same-connection')
context.generation = 2
context.runner = async () => resultFor('replacement-generation.test')
assert.deepEqual(await api.getGlabKnownHosts('same-connection'), [
'gitlab.com',
'replacement-generation.test'
])
pending.resolve(resultFor('retired-generation.test'))
const oldResult = await weakResult(old)
old = null
await collect()
const retained = oldResult.deref() !== undefined
assert.deepEqual(await api.getGlabKnownHosts('same-connection'), [
'gitlab.com',
'replacement-generation.test'
])
return { oldResultRetained: retained, replacementHostsPreserved: true }
}
async function rememberGeneration(api) {
api._resetKnownHostsCache()
context.runner = () => {
throw new Error('remembered hosts must not probe')
}
const refs = []
for (let generation = 1; generation <= 16; generation++) {
context.generation = generation
api.rememberGlabKnownHost(`host${generation}.test`, 'same-connection')
refs.push(await rememberResult(api))
}
await collect()
return refs.filter((ref) => ref.deref() !== undefined).length
}
async function abandonedProbe(api) {
api._resetKnownHostsCache()
const originalNow = Date.now
let now = 1000
Date.now = () => now
try {
const pending = Promise.withResolvers()
context.runner = () => pending.promise
const old = api.getGlabKnownHosts()
now += 60_001
context.runner = async () => resultFor('replacement.test')
assert.deepEqual(await api.getGlabKnownHosts(), ['gitlab.com', 'replacement.test'])
pending.resolve(resultFor('abandoned.test'))
await old
return await api.getGlabKnownHosts()
} finally {
Date.now = originalNow
}
}
async function rememberWhilePending(api, fail) {
api._resetKnownHostsCache()
const pending = Promise.withResolvers()
context.runner = () => pending.promise
const old = api.getGlabKnownHosts()
api.rememberGlabKnownHosts(['Remembered.TEST', ' remembered.test '])
if (fail) {
pending.reject(new Error('controlled auth failure'))
} else {
pending.resolve(resultFor('gitlab.com'))
}
assert.deepEqual(await old, ['gitlab.com', 'remembered.test'])
assert.deepEqual(await api.getGlabKnownHosts(), ['gitlab.com', 'remembered.test'])
}
async function scopeIsolation(api) {
api._resetKnownHostsCache()
const contexts = [
[undefined, {}],
[undefined, { wslDistro: 'Ubuntu' }],
[undefined, { wslDistro: 'Debian' }],
['connection-a', {}],
['connection-b', {}]
]
for (let index = 0; index < contexts.length; index++) {
context.runner = async () => resultFor(`scope${index}.test`)
assert.deepEqual(await api.getGlabKnownHosts(...contexts[index]), [
'gitlab.com',
`scope${index}.test`
])
}
context.runner = () => {
throw new Error('cached contexts must not probe')
}
for (let index = 0; index < contexts.length; index++) {
assert.deepEqual(await api.getGlabKnownHosts(...contexts[index]), [
'gitlab.com',
`scope${index}.test`
])
}
}
async function main() {
const proofHashes = Object.fromEntries(
['reproduce.cjs', 'sources.cjs', 'fix.patch', 'original-source-hashes.json'].map((file) => [
file,
hash(fs.readFileSync(path.join(__dirname, file)))
])
)
const report = { sourceHashes, proofHashes, runtime: process.versions, phases: {} }
for (const phase of ['baseline', 'fixed']) {
const api = await load(phase)
const retained = await retention(api)
const rememberedGenerationsRetained = await rememberGeneration(api)
const oldGeneration = await lateGeneration(api)
const reset = await afterReset(api)
const abandoned = await abandonedProbe(api)
await rememberWhilePending(api, false)
await rememberWhilePending(api, true)
await scopeIsolation(api)
assert.equal(retained.retained, phase === 'baseline' ? 128 : 1)
assert.equal(rememberedGenerationsRetained, phase === 'baseline' ? 16 : 1)
assert.equal(oldGeneration.oldResultRetained, phase === 'baseline')
assert.deepEqual(
reset.hosts,
phase === 'baseline'
? ['gitlab.com', 'old-before-reset.test']
: ['gitlab.com', 'fresh-after-reset.test']
)
assert.deepEqual(
abandoned,
phase === 'baseline'
? ['gitlab.com', 'replacement.test', 'abandoned.test']
: ['gitlab.com', 'replacement.test']
)
report.phases[phase] = {
retained,
rememberedGenerationsRetained,
oldGeneration,
reset,
abandoned,
rememberedSuccessAndFailure: 'passed',
nativeWslConnectionIsolation: 'passed'
}
api._resetKnownHostsCache()
}
fs.writeFileSync(
process.argv[2] || path.join(__dirname, 'results.json'),
`${JSON.stringify(report, null, 2)}\n`
)
console.log(JSON.stringify(report.phases, null, 2))
}
main()
.catch((error) => {
console.error(error)
process.exitCode = 1
})
.finally(() => {
delete globalThis[symbol]
})
setTimeout(() => {
console.error('fixture deadline')
process.exit(2)
}, 10000).unref()
@@ -0,0 +1,88 @@
{
"sourceHashes": {
"src/main/gitlab/gitlab-known-host-probe.ts": {
"baseline": "4f9651c5a383438aa968aec082c200a3371e2f4317372125aa11aa6938793c25",
"fixed": "ecf0d67f68cf4b7b1bc7d6ff19a1815a8b95fc9721962d3cd873702f98831bad"
},
"src/main/git/coalesced-probe.ts": "e5a13820a7d8b5f501a3804961ea26526bd6ad9144ecf597d5d83a70d81885e1",
"src/main/git/remote-ref-probe-cache.ts": "d5cbfd97b30e72b03c0d28d8b9e75a2ae1f9e3efc9f5b5570c78e0742c0582dd",
"src/main/gitlab/project-ref-parser.ts": "0f8b6758e6a162a58f436ca4213addc42bf6ceb3fcc2e63e8c7402c844af9303",
"src/main/gitlab/gitlab-known-host-retirement.test.ts": "c6dedbac0462cae22903805d0a89be397a2b9d122a29640b6a07a2d274f8e98e"
},
"proofHashes": {
"reproduce.cjs": "19f49406685923b050a99fbc92b02b9f6f5f8d8f308dcc14336415a0988679cc",
"sources.cjs": "2ee8ac0f295d65f16489da2b0c03800b9db4e88c83ecfb63ae7be1ec3ea6c148",
"fix.patch": "695c929087006b3406ce2c30a7ab783d88af3f9675f4ea320dc7f98b8476be9f",
"original-source-hashes.json": "f9770d9f0a87afc9231eef6af99e8ded33348fdc21ba46a70b1d5d3388874b97"
},
"runtime": {
"node": "26.6.0",
"acorn": "8.17.0",
"ada": "4.0.0",
"amaro": "1.1.11",
"ares": "1.34.8",
"brotli": "1.2.0",
"cldr": "48.0",
"icu": "78.3",
"libffi": "3.7.1",
"llhttp": "9.4.3",
"merve": "1.2.2",
"modules": "147",
"napi": "10",
"nbytes": "0.1.4",
"ncrypto": "0.0.1",
"nghttp2": "1.70.0",
"nghttp3": "",
"ngtcp2": "",
"openssl": "3.6.3",
"simdjson": "4.6.6",
"simdutf": "7.7.0",
"sqlite": "3.53.4",
"tz": "2026a",
"undici": "8.9.0",
"unicode": "17.0",
"uv": "1.52.1",
"uvwasi": "0.0.23",
"v8": "14.6.202.34-node.26",
"zlib": "1.2.12",
"zstd": "1.5.7"
},
"phases": {
"baseline": {
"retained": {
"retained": 128,
"afterReset": 0
},
"rememberedGenerationsRetained": 16,
"oldGeneration": {
"oldResultRetained": true,
"replacementHostsPreserved": true
},
"reset": {
"hosts": ["gitlab.com", "old-before-reset.test"],
"calls": 1
},
"abandoned": ["gitlab.com", "replacement.test", "abandoned.test"],
"rememberedSuccessAndFailure": "passed",
"nativeWslConnectionIsolation": "passed"
},
"fixed": {
"retained": {
"retained": 1,
"afterReset": 0
},
"rememberedGenerationsRetained": 1,
"oldGeneration": {
"oldResultRetained": false,
"replacementHostsPreserved": true
},
"reset": {
"hosts": ["gitlab.com", "fresh-after-reset.test"],
"calls": 2
},
"abandoned": ["gitlab.com", "replacement.test"],
"rememberedSuccessAndFailure": "passed",
"nativeWslConnectionIsolation": "passed"
}
}
}
@@ -0,0 +1,30 @@
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const crypto = require('node:crypto')
const { parsePatch, reversePatch, applyPatch } = require('diff')
const root = path.resolve(__dirname, '../../..')
const relativePath = 'src/main/gitlab/gitlab-known-host-probe.ts'
const sourcePath = path.join(root, relativePath)
const fixed = fs.readFileSync(sourcePath, 'utf8')
const patches = parsePatch(fs.readFileSync(path.join(__dirname, 'fix.patch'), 'utf8'))
assert.equal(patches.length, 1)
assert.equal(patches[0].newFileName, `b/${relativePath}`)
const baseline = applyPatch(fixed, reversePatch(patches[0]))
assert.notEqual(baseline, false, 'Current source must reverse exactly to the original cache')
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex')
assert.equal(hash(baseline), require('./original-source-hashes.json')[relativePath])
const sourceHashes = {
[relativePath]: { baseline: hash(baseline), fixed: hash(fixed) },
...Object.fromEntries(
[
'src/main/git/coalesced-probe.ts',
'src/main/git/remote-ref-probe-cache.ts',
'src/main/gitlab/project-ref-parser.ts',
'src/main/gitlab/gitlab-known-host-retirement.test.ts'
].map((file) => [file, hash(fs.readFileSync(path.join(root, file)))])
)
}
module.exports = { root, sourcePath, baseline, fixed, sourceHashes, hash }

Some files were not shown because too many files have changed in this diff Show More